Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions apps/publish/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,20 @@ CFP row is left out entirely where the destination would never ask.

## An invalid feed stops everything, before any destination

`main.ts` runs `validateFeed` first and shows the errors *instead of* the
console. Destinations are pure mappings, not validators: broadcasting from a
broken feed would carry the same errors to every destination at once. Don't
`main.ts` validates first and shows the errors *instead of* the console.
Destinations are pure mappings, not validators: broadcasting from a broken
feed would carry the same errors to every destination at once. Don't
"improve" this into best-effort output with a warning.

It validates with **`validateDocument`, not `validateFeed`** — this feed is
one somebody else published, so it is judged against the version it declares.
`validateFeed` is latest-only by design, and under it a perfectly valid 0.4
feed came back invalid over its `specVersion` alone, so Broadcast refused to
publish a healthy feed (#69). Being behind inside the support window is a
**notice**, never a defect: `showNotices` puts those above `#main` and the
feed still broadcasts. Never let a version notice take the early return the
error path takes.

## Online-only events are not eligible for Google rich results — say so

Google requires a physical location for event rich results ("Virtual
Expand Down
13 changes: 12 additions & 1 deletion apps/publish/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ const serve = process.argv.includes("--serve");
// it here means the snippet can never drift from the deployed assets.
const embedVersion = JSON.parse(readFileSync("../embed/package.json", "utf8")).version;

// `splitting` with an outdir instead of a single outfile, the same shape and
// for the same reason as apps/validator: validating a feed against the
// version it declares (#69) means @opentechevents/validate can reach any
// published version's compiled validators, and each one is a few hundred
// kilobytes. They sit behind a dynamic import there, so esbuild gives each
// its own chunk — without `splitting` they are all inlined and `main.js`
// goes from 464 KB to 1.3 MB for versions this page will usually never meet.
const options = {
entryPoints: ["src/main.ts"],
bundle: true,
splitting: true,
format: "esm",
platform: "browser",
target: "es2022",
outfile: "dist/main.js",
outdir: "dist",
// The page loads ./main.js by name; only the shared chunks get hashes.
entryNames: "[name]",
chunkNames: "chunks/[name]-[hash]",
sourcemap: true,
minify: !serve,
logLevel: "info",
Expand Down
40 changes: 35 additions & 5 deletions apps/publish/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type OteFeed } from "@opentechevents/export-jsonld";
import { validateFeed } from "@opentechevents/validate";
import { validateDocument, type SpecVersionNotice } from "@opentechevents/validate";

import { feedUrls, parseFeedSource, type FeedSource } from "./lib/feed-source.js";
import { DEFAULT_EMBED_OPTIONS } from "./lib/site-snippets.js";
Expand All @@ -11,7 +11,7 @@ import {
} from "./lib/store.js";
import { renderAppBar, setFeedLabel, wireBrand, wireEventChip, wireTheme } from "./ui/app-bar.js";
import type { AppContext, State } from "./ui/context.js";
import { el } from "./ui/dom.js";
import { el, link, sentence } from "./ui/dom.js";
import { renderDestination, wireDestinationChrome } from "./ui/destination.js";
import { wireEventPicker } from "./ui/event-picker.js";
import { renderHome } from "./ui/home.js";
Expand All @@ -27,6 +27,26 @@ function showMessage(heading: string, body: string, isError: boolean): void {
message.hidden = false;
}

/**
* Version notices, which are news rather than findings. The feed broadcasts
* either way, so this never returns early on the caller's behalf, and it
* renders above `#main` instead of in place of it.
*
* A notice keeps its `links` out of its sentence so a page can render them as
* links; this one does, rather than printing a bare URL mid-paragraph.
*/
function showNotices(notices: SpecVersionNotice[]): void {
if (notices.length === 0) return;
message.className = "panel";
message.replaceChildren(el("h2", undefined, "Worth knowing about this feed"));
for (const notice of notices) {
const parts: (string | Node)[] = [notice.message];
for (const target of notice.links) parts.push(" ", link(target.href, target.label));
message.append(sentence(...parts));
}
message.hidden = false;
}

/**
* The whole navigation model: two views, one function that owns every piece of
* chrome either of them needs. No router and no hash — the same shape
Expand Down Expand Up @@ -104,8 +124,14 @@ async function boot(): Promise<void> {
// Every destination maps the feed's own fields onto someone else's format;
// none of them validates. An invalid feed would be broadcast, errors and
// all, to every destination at once — so it stops here.
const validation = validateFeed(json);
if (!validation.valid) {
//
// `validateDocument`, not `validateFeed`: this is a feed somebody else
// published, so it is judged against the version it declares. Against
// "latest" a valid 0.4 feed came back broken over its `specVersion` alone
// and Broadcast refused to publish it — being behind is a notice, never a
// defect (#69).
const report = await validateDocument(json, { kind: "feed" });
if (!report.valid) {
message.className = "panel error";
message.replaceChildren(
el("h2", undefined, "This feed is not valid OTE"),
Expand All @@ -116,7 +142,7 @@ async function boot(): Promise<void> {
),
);
const list = el("ul");
for (const error of validation.errors) {
for (const error of report.errors) {
const item = el("li");
item.append(el("code", undefined, error.path), document.createTextNode(` — ${error.message}`));
list.append(item);
Expand All @@ -126,6 +152,10 @@ async function boot(): Promise<void> {
return;
}

// Said, but not in the way of anything: a supported-but-older release is a
// legitimate choice, so this sits above the app instead of replacing it.
showNotices(report.notices);

const feed = json as OteFeed;
setFeedLabel(
`${feed.title} · ${feed.events.length} event${feed.events.length === 1 ? "" : "s"}`,
Expand Down
Loading