A collaborative coding-agent orchestrator for atproto radl.app

make links to radial have nice og images/descriptions etc #87

Closed disnetdev.com
targeting main from radial/impl-343a43e8605a
Closed opened by and assigned to disnetdev.com
targeting main from radial/impl-343a43e8605a

Link previews for Radial URLs #

Implements steps 1–4 of the approved plan. Step 5 (per-record OG images) was explicitly severable and is not included.

Provenance #

Branch radial/impl-343a43e8605a
Commit 0de1ab01aca279122964e5f9f246d5bf42c73174
Base main @ 0e50735
Pull request not opened by this turn — Radial opens it from the pushed branch

What landed #

Shell defaults (packages/ui/src/app.html). <title>, description, og:site_name, og:type, og:title, og:description, twitter:card, and an absolute og:image / :type / :width / :height / :alt. The image URL is built from %sveltekit.env.PUBLIC_RADIAL_ORIGIN% — the same knob the deploy already sets for client-metadata.json. The plan's first open question is answered: SvelteKit 2.70 does substitute that placeholder into adapter-static's fallback, verified in build/index.html, so the postbuild string-replace fallback was not needed. Unset, it degrades to a relative URL crawlers skip, which is correct for loopback builds.

The card (packages/ui/static/og.png, scripts/og-card.mjs). 1200×630, the mark's own geometry — concentric rings, the ring-and-dot at the favicon's proportions — with the wordmark and "Humans and coding agents on software goals" on the design system's desk colours, read out of app.css in oklch and converted rather than re-picked in sRGB. Drawn by a hand-rolled generator (distance-field rasteriser, monoline stroke glyphs, PNG encoder over node:zlib) rather than by pulling a rasteriser, a font file and an encoder into the tree for one static image. Checked in; pnpm --filter @radial/ui og redraws it in ~0.1s.

The edge worker (packages/ui/src/edge/, static/_routes.json, scripts/build-worker.mjs). build/_worker.js fetches the same shell Pages would have served, resolves the record the URL already names, and rewrites <title>, og:title, og:description, og:type and og:url.

  • target.ts — URL → AT URI. /g/<did>/<rkey> mirrors space.ts's goalHref; ?unit= is accepted only when it parses as an AT URI in the artifact or artifactRequest collection; v is dropped (it selects a version, not a record). /p/<name> and its sub-views claim the name only.
  • records.ts — @radial/atproto on an injected fetch, public reads, lexicon validation, and an https:-only PDS check. Goal and unit fetch in parallel under one 1.5s deadline.
  • head.ts — a targeted splice over our own build output, with comments masked first (app.html documents these tags in an HTML comment, so a naive <title> match eats the prose).
  • preview.ts / worker.ts — the handler, and a one-default-export entry module.
  • Cache: 5 min + stale-while-revalidate, keyed on the canonical URL, so the same link carrying a campaign tag shares one entry and one PDS round trip.

Client-side document.title per route in +layout.svelte, from the same src/lib/meta.ts rules the worker uses, so a bookmark and a shared card say the same thing about the same URL.

Deploy gate — .tangled/workflows/deploy.yml now also requires build/_worker.js and build/_routes.json before wrangler runs. No CI mirroring needed: deploy is spindle-only and ci.yml was untouched on both remotes.

Docs — packages/ui/README.md gains a Link previews section recording the design tension (this is not a view server: no materialize, no index, no write, no credential, identical bytes for crawlers and people).

Deviation from the plan, deliberate #

The plan specified HTMLRewriter. It is workerd-only, which would have put the one function that turns agent- and member-authored text into HTML beyond the reach of pnpm test. The plan's own step 4 asks for that path to be Node-drivable. head.ts is a string splice with an explicit escapeAttribute/escapeText instead, asserted directly rather than assumed of a platform API — including that "><script>alert(1)</script><meta x=" in a title comes out inert and the document gains no tag. The shell is our own 6.6 kB build output, so buffering it costs nothing.

Checks #

pnpm codegen + git diff --exit-code, pnpm typecheck, pnpm lint, pnpm test, pnpm build — all green. 433 vitest + 29 node --test in @radial/ui, of which new:

  • src/lib/meta.test.ts (20) — clamping on word and code-point boundaries, markdown flattening, every title/description case, caps, and that app.html and meta.ts write the same sentence.
  • src/edge/target.test.ts (10), src/edge/head.test.ts (6), src/edge/preview.test.ts (11) — the whole fetch path with the network stood in for, including every degradation and the cache.
  • test/edge-bundle.test.mjs (6) — the emitted bundle has no node: imports, exports only default, _routes.json is confined to /g/* and /p/*, _redirects still holds, and the bundled artifact actually serves a request.
  • test/og-card.test.mjs (2) — the committed PNG really is the 1200×630 the shell declares.

test/browser-bundle.test.mjs is untouched and green: no edge code reached the browser path.

Verified against real workerd #

wrangler pages dev packages/ui/build, against the live records for this goal:

  • /g/did:plc:4vjd…/3mrvumwh3gk2o → the goal's title and body excerpt, og:type: article.
  • the same with ?unit=…plan-lygfltjsifckyusmtnqzzrx2&v=1 → the plan artifact's title and excerpt, og:url canonicalised without v.
  • /p/radial/system → radial, no PDS contacted at all.
  • /g/did:plc:zzz/zzz and /g/nonsense → HTTP 200, default tags, app shell.
  • /, /awaiting, /space, /client-metadata.json, /og.png, /favicon.svg → byte-identical to the bundle. This also settles the plan's routing risk empirically: _redirects still applies for _routes.json-excluded paths behind a _worker.js.

That run is also what caught a real defect no unit test could: workerd refuses a named export from _worker.js that is not a function or ExportedHandler, so exporting PREVIEW_CACHE_CONTROL beside the handler failed at Starting local server on a bundle that typechecked and passed everything. Hence the entry/logic split, and a test that holds the bundle to one export.

Remaining risks #

  • Unverified in production. The ?unit= and default cards have not been through Slack, Discord or the Twitter card validator, and og:image has not been fetched from a real radl.app — only wrangler pages dev locally. Worth one paste each after the deploy.
  • A ?unit= link is keyed by the root artifact's URI, so its description comes from the first version that landed rather than from the tip. Title and type are right; a much-revised body may read as its own v1. Following the tip would mean materializing, which is the line this worker does not cross.
  • Unfurler variance. Some drop query strings, which makes a ?unit= link preview as its goal — a graceful floor, not a failure. Some cache hard; the platforms' re-scrape tools are the recourse.
  • A member's PDS being down turns their links generic until the cache repopulates — the same stale-never-wrong degradation the app itself has.
  • wrangler pages dev prints a pre-existing "infinite loop detected" warning for the /* /index.html 200 rule in _redirects. It is its local emulation's heuristic, not a new condition — the rule served /awaiting and /space correctly in the same run — and _redirects was not touched.

Radial artifact

Commits 0