diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml index 1d418c5..ba011f2 100644 --- a/.tangled/workflows/deploy.yml +++ b/.tangled/workflows/deploy.yml @@ -52,6 +52,12 @@ steps: # The origin has to have reached the bundle; without it the app still runs but signs in as a # loopback client, which no deployed origin can do. test -f packages/ui/build/client-metadata.json + # …and the two files that give a goal or project deep link a link preview of its own. Both are + # ordinary files in the upload, so wrangler needs no argument for them — which also means a + # build that quietly stopped emitting one would deploy a site whose links unfurl as the + # generic card again, with nothing to say so. Hence the gate. + test -f packages/ui/build/_worker.js + test -f packages/ui/build/_routes.json - name: "Deploy" command: | diff --git a/packages/ui/README.md b/packages/ui/README.md index fa237d5..d41b2a3 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -8,6 +8,7 @@ pnpm --filter @radial/ui dev # vite dev server on 127.0.0.1 pnpm --filter @radial/ui build # static bundle in build/, plus the radial-ui binary in dist/ pnpm --filter @radial/ui preview # serve the built bundle with vite pnpm --filter @radial/ui start # build, then serve it with radial-ui on 127.0.0.1:4321 +pnpm --filter @radial/ui og # redraw static/og.png, the default link-preview card ``` It is a **pure SPA with no view server** (`docs/phase6-ui-plan.md` §1): `adapter-static` emits one @@ -33,6 +34,8 @@ if a `node:*` import creeps back onto that path. | `src/lib/keys.ts`, `focus.ts` | The keyboard map, and focus restoration. | | `src/lib/directory.ts` | Membership → the row an actor gets: kind, initials, disc color. Color is derived from the DID, never authored. | | `src/lib/prose.ts` | The comp's small `## ` + backtick markup grammar. Parses to a structure the component interpolates — no `{@html}` anywhere near an agent-authored body. | +| `src/lib/meta.ts` | What a URL says about itself: the tab title, and the `og:*` a link preview draws. Pure text, shared by the browser and the edge worker so the two cannot disagree. | +| `src/edge/` | The Cloudflare Pages worker that upgrades those tags per record for `/g/*` and `/p/*` — see *Link previews* below. Beside the browser path, never on it. | | `src/lib/components/` | Rail, pane bar, space picker, state circle, pie, badges, discs, unit rows and drawers, compose cards, smart lists. | | `bin/serve.ts` | `radial-ui`: serves the built bundle on loopback, nothing else. | @@ -68,6 +71,53 @@ served at — so `PUBLIC_RADIAL_ORIGIN=https://radial.example pnpm build` emits into the build. Without it the bundle is complete and still signs in on a loopback host, where the client id encodes the metadata instead of pointing at it. +### Link previews + +A crawler does not run the app. Paste a Radial URL into Slack, Discord or a social feed and the +unfurler fetches the page, reads `` out of the HTML, and shows what it finds — +so a pure SPA, which answers every URL with one shell, used to unfurl as nothing at all. + +Two layers, and the first is the one that matters: + +1. **The shell's defaults**, in `src/app.html`: a title, a description, and a 1200×630 `og:image`. + True of the product and true of every route, so *any* host serving this bundle — Pages, the + `radial-ui` loopback server, a directory on a laptop — unfurls every URL with the branded card. + `og:image` has to be absolute or it is ignored, so it is built from `PUBLIC_RADIAL_ORIGIN`, the + same knob `client-metadata.json` uses; unset, it degrades to a relative URL no crawler reaches + anyway. The card itself is `static/og.png`, drawn from the mark's own geometry by + `pnpm --filter @radial/ui og` and checked in. +2. **Per-record tags at the edge**, for the two path families whose URLs name a record. `_worker.js` + (bundled from `src/edge/worker.ts`) fetches the same static shell Pages would have served, reads + the public record the URL already carries — `/g//` *is* a goal's AT URI, and `?unit=` + carries the unit's — and replaces ``, `og:title`, `og:description`, `og:type` and `og:url` + before handing the document back. A project route carries only a name, so that is all it claims. + +**This is not a view server coming back** (`docs/phase6-ui-plan.md` §1). The worker never +materializes, never reads an index, never writes, and holds no credential of any kind: it decorates +a head with what one public record says about itself, over the same unauthenticated read anyone can +make. The app in the browser still resolves everything for itself, and `src/lib/meta.ts` — the pure +text rules both use — is what keeps the tab title and the shared card saying the same thing about +the same URL. Crawlers and people are served identical bytes; there is no user-agent branch, because +cloaking is how link previews get distrusted. + +Every failure is the same failure: the shell, with its defaults. A DID that will not resolve, a PDS +that is down, a record of the wrong shape, a malformed link, a deadline missed (1.5s) — all of them +end in a generic preview and a working app, never an error page, because real people follow these +links too. Answers are cached for five minutes on the canonical URL, so a burst of unfurls costs one +round trip to a member's PDS rather than one each. + +`static/_routes.json` confines the worker to `/g/*` and `/p/*`. Everything else — every asset, +`/client-metadata.json`, and the per-viewer views, whose previews are the shell's defaults by nature +— keeps the byte-for-byte static path it had before the worker existed. Inside those two families +the worker always answers with the shell it fetched by name, rather than relying on `_redirects` +applying behind a function, so the deep links it owns do not depend on that question having a +particular answer. + +Two things worth knowing. 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 — the title and type are right, a +much-revised body may read as its own v1. And some unfurlers drop query strings entirely, which +makes a `?unit=` link preview as its goal; that is a graceful floor rather than a failure. + ### Deploying `.tangled/workflows/deploy.yml` publishes `build/` to Cloudflare Pages on every push to `main`, @@ -78,7 +128,10 @@ Two things the bundle needs from a static host, both already in the repo. `PUBLI set in the deploy workflow's `environment` — it is public, and the built `client-metadata.json` is only valid for the origin it names, so it moves the same day a custom domain does. `static/_redirects` answers every unmatched path with the shell, which is what makes `/g/<did>/<rkey>` resolve on a -host that has never heard of a goal; a real file still wins over the rule. +host that has never heard of a goal; a real file still wins over the rule. Alongside them the build +emits `_worker.js` and copies `_routes.json` into `build/`, which is all Pages needs to run the link +preview worker — the wrangler invocation is unchanged, and the Build step gates on all three files +being there. The credentials are spindle repo secrets, never the workflow: `CLOUDFLARE_API_TOKEN` (a token with *Cloudflare Pages: Edit*, and nothing else) and `CLOUDFLARE_ACCOUNT_ID`. Neither reaches the app — diff --git a/packages/ui/package.json b/packages/ui/package.json index 4667cdd..5c97288 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,9 +7,10 @@ "radial-ui": "./dist/serve.js" }, "scripts": { - "build": "tsc -p tsconfig.node.json && vite build", + "build": "tsc -p tsconfig.node.json && vite build && node scripts/build-worker.mjs", "dev": "vite dev", "lint": "pnpm typecheck", + "og": "node scripts/og-card.mjs", "prepare": "svelte-kit sync", "preview": "vite preview", "start": "pnpm build && node dist/serve.js", diff --git a/packages/ui/scripts/build-worker.mjs b/packages/ui/scripts/build-worker.mjs new file mode 100644 index 0000000..eab3e36 --- /dev/null +++ b/packages/ui/scripts/build-worker.mjs @@ -0,0 +1,41 @@ +// Bundles `src/edge/worker.ts` to `build/_worker.js`, the Cloudflare Pages function that gives a +// goal or project deep link a link preview of its own. +// +// It runs after `vite build` and writes into the directory `adapter-static` just filled, so the +// deploy's `wrangler pages deploy packages/ui/build` picks it up with no change: `_worker.js` and +// the `_routes.json` that confines it (copied out of `static/`) are both just files in the upload. +// +// esbuild rather than another vite pass because the target is not a browser and not Node — it is +// workerd, which has `fetch`, `URL` and the web streams and nothing else. `platform: 'neutral'` is +// what keeps the bundle honest about that: a `node:` import fails here rather than at deploy time, +// and `test/edge-bundle.test.mjs` asserts the same thing about the emitted file. + +import { rm } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import * as esbuild from 'esbuild' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const outfile = resolve(root, 'build', '_worker.js') + +await rm(outfile, { force: true }) + +const result = await esbuild.build({ + entryPoints: [resolve(root, 'src', 'edge', 'worker.ts')], + outfile, + bundle: true, + format: 'esm', + platform: 'neutral', + // What workerd runs. `mainFields`/`conditions` keep resolution on the browser-safe entries of the + // workspace packages — the same ones the app itself gets — rather than a `node` condition. + target: 'es2022', + mainFields: ['browser', 'module', 'main'], + conditions: ['browser', 'import', 'default'], + minify: true, + sourcemap: false, + logLevel: 'info', + metafile: true, +}) + +const bytes = Object.values(result.metafile.outputs)[0]?.bytes ?? 0 +console.log(`build/_worker.js — ${(bytes / 1024).toFixed(1)} kB`) diff --git a/packages/ui/scripts/og-card.mjs b/packages/ui/scripts/og-card.mjs new file mode 100644 index 0000000..c263887 --- /dev/null +++ b/packages/ui/scripts/og-card.mjs @@ -0,0 +1,322 @@ +// Draws `static/og.png`, the 1200×630 card an unfurler shows for any Radial link that has no +// record of its own to describe (§ the shell defaults in `src/app.html`). +// +// It is a script rather than a build step, and its output is checked in: the card changes when the +// brand does, which is roughly never, and a build that has to redraw it is a build that can drift +// from what shipped. `pnpm --filter @radial/ui og` redraws it; `test/og-card.test.mjs` only holds +// the committed file to being a real 1200×630 PNG. +// +// Everything here is hand-rolled because it has to be: this package may take third-party deps, but +// dragging a rasteriser, a font file and a PNG encoder into the tree to draw one static image is a +// bad trade. So the card is built out of what the mark already is — circles, arcs and monoline +// strokes on the design system's desk colours, read straight out of `app.css` in oklch. + +import { deflateSync } from 'node:zlib' +import { writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const WIDTH = 1200 +const HEIGHT = 630 + +// ── colour ─────────────────────────────────────────────────────────────────────────────────────── +// The tokens are authored in oklch (`app.css`), so they are converted rather than re-picked in sRGB: +// a hex literal here would be a second, drifting definition of the same colour. + +/** oklch → sRGB bytes. Out-of-gamut components are clipped, which none of these tokens need. */ +function oklch(l, c, hDegrees) { + const h = (hDegrees * Math.PI) / 180 + const a = c * Math.cos(h) + const b = c * Math.sin(h) + const lp = l + 0.3963377774 * a + 0.2158037573 * b + const mp = l - 0.1055613458 * a - 0.0638541728 * b + const sp = l - 0.0894841775 * a - 1.291485548 * b + const [L, M, S] = [lp * lp * lp, mp * mp * mp, sp * sp * sp] + const linear = [ + 4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S, + -1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S, + -0.0041960863 * L - 0.7034186147 * M + 1.707614701 * S, + ] + return linear.map((value) => { + const encoded = value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(Math.max(value, 0), 1 / 2.4) - 0.055 + return Math.max(0, Math.min(255, Math.round(encoded * 255))) + }) +} + +const DESK = oklch(0.921, 0.008, 258) +const DESK_EDGE = oklch(0.878, 0.01, 258) +const INK = oklch(0.255, 0.016, 262) +const INK_2 = oklch(0.475, 0.014, 262) +const ACCENT = oklch(0.475, 0.15, 264) + +// ── the canvas ─────────────────────────────────────────────────────────────────────────────────── +// One RGB buffer and one operation: composite a colour over it with per-pixel coverage. Coverage is +// a signed distance to the shape, which is where the antialiasing comes from — `1px` of feather at +// the edge, no supersampling pass. + +const pixels = new Uint8Array(WIDTH * HEIGHT * 3) + +function fill(color) { + for (let i = 0; i < pixels.length; i += 3) { + pixels[i] = color[0] + pixels[i + 1] = color[1] + pixels[i + 2] = color[2] + } +} + +function blend(x, y, color, alpha) { + if (alpha <= 0 || x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return + const a = Math.min(1, alpha) + const i = (y * WIDTH + x) * 3 + pixels[i] = Math.round(pixels[i] * (1 - a) + color[0] * a) + pixels[i + 1] = Math.round(pixels[i + 1] * (1 - a) + color[1] * a) + pixels[i + 2] = Math.round(pixels[i + 2] * (1 - a) + color[2] * a) +} + +/** + * Paint every pixel whose distance to `shape` puts it inside `radius`, over the bounding box only. + * `shape(x, y)` returns a distance in pixels; `radius` is the half-width of a stroke or the radius + * of a disc. Distances are analytic rather than sampled — a 470px ring flattened to a polyline + * would be a million segments tested against half a million pixels. + */ +function paint(box, shape, radius, color) { + const x0 = Math.max(0, Math.floor(box[0] - radius - 1)) + const y0 = Math.max(0, Math.floor(box[1] - radius - 1)) + const x1 = Math.min(WIDTH - 1, Math.ceil(box[2] + radius + 1)) + const y1 = Math.min(HEIGHT - 1, Math.ceil(box[3] + radius + 1)) + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + blend(x, y, color, radius + 0.5 - shape(x + 0.5, y + 0.5)) + } + } +} + +function disc(cx, cy, radius, color) { + paint([cx, cy, cx, cy], (x, y) => Math.hypot(x - cx, y - cy), radius, color) +} + +/** A stroked polyline in device pixels, round-capped and round-joined by construction. */ +function polyline(points, width, color) { + if (points.length < 2) return + const xs = points.map((point) => point[0]) + const ys = points.map((point) => point[1]) + const box = [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)] + const distance = (px, py) => { + let best = Infinity + for (let i = 1; i < points.length; i++) { + const [ax, ay] = points[i - 1] + const [bx, by] = points[i] + const dx = bx - ax + const dy = by - ay + const length = dx * dx + dy * dy + const t = length === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / length)) + best = Math.min(best, Math.hypot(px - (ax + t * dx), py - (ay + t * dy))) + } + return best + } + paint(box, distance, width / 2, color) +} + +/** + * A circular arc, swept from `from` to `to` in degrees — counter-clockwise when `to` is the larger, + * clockwise when it is not, and a whole circle when they are 360 apart. Angles are measured the way + * the glyph table reads: 0° at the right, rising anti-clockwise, y upwards. + */ +function arc(cx, cy, radius, from, to, width, color) { + const span = to - from + const point = (degrees) => { + const angle = (degrees * Math.PI) / 180 + return [cx + radius * Math.cos(angle), cy - radius * Math.sin(angle)] + } + const [ax, ay] = point(from) + const [bx, by] = point(to) + const distance = (px, py) => { + if (Math.abs(span) >= 360) return Math.abs(Math.hypot(px - cx, py - cy) - radius) + const here = (Math.atan2(cy - py, px - cx) * 180) / Math.PI + let delta = (((here - from) % 360) + 360) % 360 + if (span < 0) delta -= 360 + if (Math.abs(delta) <= Math.abs(span)) return Math.abs(Math.hypot(px - cx, py - cy) - radius) + // Outside the sweep the nearest point on the arc is one of its two ends — this is what rounds + // the caps of a `c` or an `e`. + return Math.min(Math.hypot(px - ax, py - ay), Math.hypot(px - bx, py - by)) + } + paint([cx - radius, cy - radius, cx + radius, cy + radius], distance, width / 2, color) +} + +const ring = (cx, cy, radius, width, color) => arc(cx, cy, radius, 0, 360, width, color) + +// ── the letters ────────────────────────────────────────────────────────────────────────────────── +// A monoline geometric face, defined the way the mark is: circles and straight runs. Coordinates are +// in em, x rightwards from the glyph origin and y *upwards* from the baseline, with the cap height +// at 1 and the x-height at 0.72. `L x y x y …` is a polyline; `A cx cy r from to` is an arc, swept +// counter-clockwise when `to` is the larger. +// +// Only the glyphs the card actually sets are here, and `glyph()` throws on anything else rather than +// dropping a character silently — add the letter when the copy needs it. + +const GLYPHS = { + ' ': { advance: 0.34, paths: [] }, + '.': { advance: 0.3, paths: [['A', 0.1, 0.0, 0, 0, 360]] }, + ',': { advance: 0.3, paths: [['L', 0.13, 0.03, 0.05, -0.16]] }, + H: { advance: 0.94, paths: [['L', 0, 1, 0, 0], ['L', 0.78, 1, 0.78, 0], ['L', 0, 0.5, 0.78, 0.5]] }, + R: { + advance: 0.92, + paths: [ + ['L', 0, 1, 0, 0], + ['L', 0, 1, 0.37, 1], + ['A', 0.37, 0.75, 0.25, 90, -90], + ['L', 0.37, 0.5, 0, 0.5], + ['L', 0.35, 0.5, 0.8, 0], + ], + }, + a: { advance: 0.86, paths: [['A', 0.36, 0.36, 0.36, 0, 360], ['L', 0.72, 0.72, 0.72, 0]] }, + c: { advance: 0.8, paths: [['A', 0.36, 0.36, 0.36, 55, 305]] }, + d: { advance: 0.86, paths: [['A', 0.36, 0.36, 0.36, 0, 360], ['L', 0.72, 1, 0.72, 0]] }, + e: { advance: 0.82, paths: [['L', 0, 0.36, 0.72, 0.36], ['A', 0.36, 0.36, 0.36, 0, 305]] }, + f: { + advance: 0.6, + paths: [['L', 0.36, 0, 0.36, 0.9], ['A', 0.62, 0.9, 0.26, 180, 90], ['L', 0.05, 0.72, 0.7, 0.72]], + }, + g: { + advance: 0.86, + paths: [ + ['A', 0.36, 0.36, 0.36, 0, 360], + ['L', 0.72, 0.72, 0.72, -0.1], + ['A', 0.36, -0.1, 0.36, 0, -160], + ], + }, + i: { advance: 0.3, paths: [['L', 0.12, 0.72, 0.12, 0], ['A', 0.12, 0.93, 0.02, 0, 360]] }, + l: { advance: 0.3, paths: [['L', 0.12, 1, 0.12, 0]] }, + m: { + advance: 1.34, + paths: [ + ['L', 0, 0.72, 0, 0], + ['A', 0.3, 0.42, 0.3, 180, 0], + ['L', 0.6, 0.42, 0.6, 0], + ['A', 0.9, 0.42, 0.3, 180, 0], + ['L', 1.2, 0.42, 1.2, 0], + ], + }, + n: { + advance: 0.86, + paths: [['L', 0, 0.72, 0, 0], ['A', 0.36, 0.36, 0.36, 180, 0], ['L', 0.72, 0.36, 0.72, 0]], + }, + o: { advance: 0.86, paths: [['A', 0.36, 0.36, 0.36, 0, 360]] }, + r: { advance: 0.55, paths: [['L', 0, 0.72, 0, 0], ['A', 0.36, 0.36, 0.36, 180, 90]] }, + s: { + advance: 0.7, + paths: [ + ['A', 0.3, 0.53, 0.19, 0, 270], + ['A', 0.3, 0.19, 0.19, 90, -170], + ], + }, + t: { + advance: 0.62, + paths: [['L', 0.3, 1, 0.3, 0.14], ['A', 0.44, 0.14, 0.14, 180, 270], ['L', 0.04, 0.72, 0.6, 0.72]], + }, + u: { + advance: 0.86, + paths: [['L', 0, 0.72, 0, 0.36], ['A', 0.36, 0.36, 0.36, 180, 360], ['L', 0.72, 0.72, 0.72, 0]], + }, + w: { advance: 1.06, paths: [['L', 0, 0.72, 0.24, 0, 0.48, 0.6, 0.72, 0, 0.96, 0.72]] }, +} + +function glyph(character) { + const found = GLYPHS[character] + if (!found) throw new Error(`og-card: no glyph for ${JSON.stringify(character)} — add it to GLYPHS`) + return found +} + +/** The width of `text` at cap height `size`, in pixels. `tracking` is in em. */ +function measure(text, size, tracking) { + let em = 0 + for (const character of text) em += glyph(character).advance + tracking + return (em - tracking) * size +} + +/** Set `text` with its baseline at `y` and its first glyph origin at `x`. */ +function write(text, x, y, size, weight, color, tracking) { + let pen = x + for (const character of text) { + const { advance, paths } = glyph(character) + for (const [kind, ...values] of paths) { + if (kind === 'L') { + const points = [] + for (let i = 0; i < values.length; i += 2) points.push([pen + values[i] * size, y - values[i + 1] * size]) + polyline(points, weight, color) + } else { + const [cx, cy, radius, from, to] = values + arc(pen + cx * size, y - cy * size, radius * size, from, to, weight, color) + } + } + pen += (advance + tracking) * size + } + return pen - tracking * size +} + +// ── the card ───────────────────────────────────────────────────────────────────────────────────── + +const WORDMARK = 'Radial' +const TAGLINE = 'Humans and coding agents on software goals' + +fill(DESK) + +// The mark, enlarged: concentric rings running off the right edge. Radial's shape, and the reason +// the card reads as this product from a thumbnail rather than from its text. +for (let radius = 96; radius <= 470; radius += 62) ring(978, 315, radius, 3, DESK_EDGE) + +// The mark itself, at the favicon's proportions (r 9.2 / stroke 1.8 / dot 3.4 on a 24 box). +ring(142, 196, 46, 9, ACCENT) +disc(142, 196, 17, ACCENT) + +write(WORDMARK, 96, 402, 132, 15, INK, 0.035) +write(TAGLINE, 99, 476, 31, 4.5, INK_2, 0.02) + +// ── PNG ────────────────────────────────────────────────────────────────────────────────────────── + +const CRC_TABLE = Array.from({ length: 256 }, (_, n) => { + let c = n + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 + return c >>> 0 +}) + +function crc32(bytes) { + let c = 0xffffffff + for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8) + return (c ^ 0xffffffff) >>> 0 +} + +function chunk(type, data) { + const length = Buffer.alloc(4) + length.writeUInt32BE(data.length) + const body = Buffer.concat([Buffer.from(type, 'latin1'), data]) + const crc = Buffer.alloc(4) + crc.writeUInt32BE(crc32(body)) + return Buffer.concat([length, body, crc]) +} + +const header = Buffer.alloc(13) +header.writeUInt32BE(WIDTH, 0) +header.writeUInt32BE(HEIGHT, 4) +header[8] = 8 // bit depth +header[9] = 2 // colour type: truecolour, no alpha +// Filter byte 0 on every row: the image is flat fields and thin strokes, so deflate's own matching +// already collapses it and a filter would only cost a pass. +const raw = Buffer.alloc(HEIGHT * (1 + WIDTH * 3)) +for (let y = 0; y < HEIGHT; y++) { + raw.set(pixels.subarray(y * WIDTH * 3, (y + 1) * WIDTH * 3), y * (1 + WIDTH * 3) + 1) +} + +const png = Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk('IHDR', header), + chunk('IDAT', deflateSync(raw, { level: 9 })), + chunk('IEND', Buffer.alloc(0)), +]) + +const out = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'static', 'og.png') +writeFileSync(out, png) +console.log( + `${out} — ${WIDTH}×${HEIGHT}, ${(png.length / 1024).toFixed(1)} kB` + + `\nwordmark ${measure(WORDMARK, 132, 0.035).toFixed(0)}px, tagline ${measure(TAGLINE, 31, 0.02).toFixed(0)}px`, +) diff --git a/packages/ui/src/app.html b/packages/ui/src/app.html index 5c54b32..1af4c20 100644 --- a/packages/ui/src/app.html +++ b/packages/ui/src/app.html @@ -4,6 +4,35 @@ <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> + + <!-- + The card an unfurler draws for a Radial link. A crawler does not run the app, so everything it + can learn about a URL has to already be in the HTML it is handed — and `adapter-static` hands + every route this one shell. These are therefore the *defaults*: true of the product, true of + every route, and correct on any host that serves the bundle. The Pages worker (`src/edge/`) + overwrites the four per-record ones — `og:title`, `og:description`, `og:url`, `og:type`, plus + `<title>` — for `/g/*` and `/p/*`; everything else, everywhere else, keeps what is written + here. The strings live in `src/lib/meta.ts` too, and `test/og-meta.test.mjs` holds the two + copies to each other. + + `og:image` must be absolute or it is ignored, and a static bundle cannot know its own origin — + so it is the same `PUBLIC_RADIAL_ORIGIN` the deploy already sets for `client-metadata.json`. + Unset, this degrades to a relative URL that crawlers skip, which is the right answer: the + builds without it are loopback builds, and no crawler reaches loopback. + --> + <title>Radial + + + + + + + + + + + + `, rewritten to say what one record says about itself. + * + * The document being edited is our own build output — `src/app.html` with the tags already in it — + * not arbitrary markup, so this is a targeted splice rather than a parse: find the `` and + * the `content` of the four `<meta>` tags the preview owns, replace those spans, and leave every + * other byte alone. Humans and crawlers are served the identical document; there is no user-agent + * branch anywhere, because cloaking is how link previews get distrusted. + * + * **Why not `HTMLRewriter`.** workerd ships one, and its `setAttribute` encodes for you. It is also + * unavailable in Node, which would put the one function that turns agent-authored text into HTML + * beyond the reach of `pnpm test`. A string splice with an explicit `escapeAttribute` is testable + * on the same runtime CI already runs, and the escaping it depends on is asserted directly + * (`head.test.ts`) rather than assumed of a platform API. + * + * Comments are masked before anything is searched. `app.html` documents these very tags, in + * backticks, inside an HTML comment — a naive `<title>` match would find the prose about the title + * before the title. + */ + +import { pageTitle, type Preview } from '../lib/meta.js' + +export interface HeadMeta extends Preview { + /** The canonical absolute URL this preview is for. */ + url: string +} + +/** `&`, `<` and `>` — enough for text content, which cannot escape its element without them. */ +export const escapeText = (value: string): string => + value.replace(/&/gu, '&').replace(/</gu, '<').replace(/>/gu, '>') + +/** Attribute values additionally cannot carry a quote of either kind. */ +export const escapeAttribute = (value: string): string => + escapeText(value).replace(/"/gu, '"').replace(/'/gu, ''') + +interface Edit { + start: number + end: number + text: string +} + +/** Comment bodies, blanked to spaces so offsets still line up with the real document. */ +const maskComments = (html: string): string => + html.replace(/<!--[\s\S]*?-->/gu, (comment) => ' '.repeat(comment.length)) + +export function rewriteHead(html: string, meta: HeadMeta): string { + const masked = maskComments(html) + const edits: Edit[] = [] + + const title = /<title[^>]*>([\s\S]*?)<\/title>/iu.exec(masked) + if (title) { + const start = title.index + title[0].length - ''.length - (title[1] as string).length + edits.push({ start, end: start + (title[1] as string).length, text: escapeText(pageTitle(meta.title)) }) + } + + const content = (attribute: 'property' | 'name', key: string, value: string) => { + const span = findContent(masked, attribute, key) + if (span) edits.push({ start: span[0], end: span[1], text: escapeAttribute(value) }) + return Boolean(span) + } + + content('property', 'og:title', meta.title) + content('property', 'og:description', meta.description) + content('property', 'og:type', meta.type) + content('name', 'description', meta.description) + + // `og:url` has no default worth shipping — the shell is served for every route, so any value it + // carried would be a lie on all but one of them. It is added here, where the URL is known. + if (!content('property', 'og:url', meta.url)) { + const head = /<\/head\s*>/iu.exec(masked) + if (head) { + // Written to land on the indentation `` already sits on rather than at column zero. + const tag = ` \n ` + edits.push({ start: head.index, end: head.index, text: tag }) + } + } + + // Applied back to front so an earlier edit cannot move a later one's offsets. + return edits + .sort((left, right) => right.start - left.start) + .reduce((out, edit) => out.slice(0, edit.start) + edit.text + out.slice(edit.end), html) +} + +/** + * The `[start, end)` of the `content` attribute's *value* on the first `` tag carrying + * `attribute="key"`. Only double-quoted attributes are recognised, which is what the shell writes. + */ +function findContent(masked: string, attribute: string, key: string): [number, number] | undefined { + for (const tag of masked.matchAll(/]*>/giu)) { + const text = tag[0] + if (!new RegExp(`\\b${attribute}\\s*=\\s*"${escapeRegExp(key)}"`, 'iu').test(text)) continue + const value = /\bcontent\s*=\s*"([^"]*)"/iu.exec(text) + if (!value) continue + const start = (tag.index ?? 0) + value.index + value[0].length - 1 - (value[1] as string).length + return [start, start + (value[1] as string).length] + } + return undefined +} + +const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&') diff --git a/packages/ui/src/edge/preview.test.ts b/packages/ui/src/edge/preview.test.ts new file mode 100644 index 0000000..cebd5ad --- /dev/null +++ b/packages/ui/src/edge/preview.test.ts @@ -0,0 +1,267 @@ +import { COLLECTIONS, type ArtifactRecord, type ArtifactRequestRecord, type GoalRecord } from '@radial/core' +import { describe, expect, it } from 'vitest' +import shell from '../app.html?raw' +import { PREVIEW_CACHE_CONTROL, handlePreview, type EdgeCache, type Env } from './preview.js' + +// The whole path a crawler takes, end to end, with the network stood in for: request → shell → +// DID document → PDS read → rewritten head. `@radial/atproto`'s client is built around an injected +// `fetch` for exactly this reason, so nothing here is a mock of our own code — the transport, the +// resolution and the validation are the real ones. + +const DID = 'did:plc:n6ku5xddiuguwze3f356evla' +const PDS = 'https://pds.example' +const GOAL_URI = `at://${DID}/${COLLECTIONS.goal}/g1` +const ARTIFACT_URI = `at://${DID}/${COLLECTIONS.artifact}/a1` +const REQUEST_URI = `at://${DID}/${COLLECTIONS.artifactRequest}/r1` + +const ref = (uri: string) => ({ uri, cid: 'bafyfixture' }) + +const goal: GoalRecord = { + $type: 'com.disnetdev.radial.goal', + space: ref(`at://${DID}/${COLLECTIONS.space}/self`), + project: ref(`at://${DID}/${COLLECTIONS.project}/radial`), + title: 'make links to radial have nice og images', + body: 'including deep links to goals and artifacts.', + createdAt: '2026-07-31T00:00:00.000Z', +} + +const artifact: ArtifactRecord = { + $type: 'com.disnetdev.radial.artifact', + request: ref(REQUEST_URI), + // Exactly one of `goal` or `project`, which the lexicon enforces and `getRecord` checks. + goal: ref(GOAL_URI), + type: 'plan', + title: 'Plan: OG link previews', + body: 'Shell defaults plus a Pages edge worker.', + links: {}, + createdAt: '2026-07-31T01:00:00.000Z', +} + +const artifactRequest: ArtifactRequestRecord = { + $type: 'com.disnetdev.radial.artifactRequest', + goal: ref(GOAL_URI), + type: 'implementation', + basedOn: [ref(ARTIFACT_URI)], + brief: 'ship steps 1 through 4.', + createdAt: '2026-07-31T02:00:00.000Z', +} + +const DID_DOCUMENT = { + id: DID, + alsoKnownAs: ['at://alice.test'], + service: [ + { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: PDS }, + ], +} + +interface NetworkOptions { + document?: unknown + records?: Record + fail?: (url: string) => Response | undefined +} + +/** A `fetch` that answers with canned atproto responses and records what it was asked for. */ +function network(options: NetworkOptions = {}) { + const calls: string[] = [] + const records = options.records ?? { + [GOAL_URI]: goal, + [ARTIFACT_URI]: artifact, + [REQUEST_URI]: artifactRequest, + } + const fetcher = async (input: string | URL): Promise => { + const url = new URL(input) + calls.push(url.toString()) + const forced = options.fail?.(url.toString()) + if (forced) return forced + if (url.hostname === 'plc.directory') { + return json(options.document ?? DID_DOCUMENT) + } + if (url.pathname === '/xrpc/com.atproto.repo.getRecord') { + const uri = `at://${url.searchParams.get('repo')}/${url.searchParams.get('collection')}/${url.searchParams.get('rkey')}` + const value = records[uri] + return value ? json({ uri, cid: 'bafyfixture', value }) : new Response('no', { status: 400 }) + } + return new Response('unexpected', { status: 500 }) + } + return { fetcher, calls } +} + +const json = (value: unknown) => + new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } }) + +/** The static origin Pages would have served from, standing in for `env.ASSETS`. */ +function assets(): Env & { served: string[] } { + const served: string[] = [] + return { + served, + ASSETS: { + fetch: async (request: Request) => { + const url = new URL(request.url) + served.push(url.pathname) + if (url.pathname !== '/index.html') return new Response('not found', { status: 404 }) + return new Response(shell, { headers: { 'content-type': 'text/html' } }) + }, + }, + } +} + +const get = (path: string, init?: RequestInit) => + new Request(new URL(path, 'https://radl.app'), init) + +const content = (html: string, attribute: string, key: string): string | undefined => + new RegExp(`]*\\b${attribute}="${key}"[^>]*>`, 'i') + .exec(html)?.[0] + ?.match(/content="([^"]*)"/i)?.[1] + +const title = (html: string): string | undefined => /([^<]*)<\/title>/i.exec(html)?.[1] + +describe('a goal deep link', () => { + it('unfurls as the goal', async () => { + const env = assets() + const { fetcher } = network() + const response = await handlePreview(get(`/g/${DID}/g1`), env, { fetcher }) + const html = await response.text() + + expect(response.status).toBe(200) + expect(response.headers.get('cache-control')).toBe(PREVIEW_CACHE_CONTROL) + expect(title(html)).toBe(`${goal.title} · Radial`) + expect(content(html, 'property', 'og:title')).toBe(goal.title) + expect(content(html, 'property', 'og:description')).toBe(goal.body) + expect(content(html, 'property', 'og:type')).toBe('article') + expect(content(html, 'property', 'og:url')).toBe(`https://radl.app/g/${DID}/g1`) + // The shell it upgraded is the one the static origin would have served, fetched by name rather + // than by leaning on `_redirects` applying behind a function. + expect(env.served).toEqual(['/index.html']) + }) + + it('unfurls a `?unit=` link as the artifact it opens', async () => { + const { fetcher } = network() + const url = `/g/${DID}/g1?unit=${encodeURIComponent(ARTIFACT_URI)}&v=2` + const html = await (await handlePreview(get(url), assets(), { fetcher })).text() + expect(content(html, 'property', 'og:title')).toBe('Plan: OG link previews') + expect(content(html, 'property', 'og:description')).toBe(artifact.body) + // `v` selects a version, not a record: it is not part of what this URL is about. + expect(content(html, 'property', 'og:url')).toBe( + `https://radl.app/g/${DID}/g1?unit=${encodeURIComponent(ARTIFACT_URI)}`, + ) + }) + + it('unfurls a unit with nothing landed as the request that drives it', async () => { + const { fetcher } = network() + const url = `/g/${DID}/g1?unit=${encodeURIComponent(REQUEST_URI)}` + const html = await (await handlePreview(get(url), assets(), { fetcher })).text() + expect(content(html, 'property', 'og:title')).toBe(`implementation requested — ${goal.title}`) + expect(content(html, 'property', 'og:description')).toBe(artifactRequest.brief) + }) + + it('keeps the goal when the unit cannot be read', async () => { + const { fetcher } = network({ + fail: (url) => (url.includes('rkey=a1') ? new Response('gone', { status: 500 }) : undefined), + }) + const url = `/g/${DID}/g1?unit=${encodeURIComponent(ARTIFACT_URI)}` + const html = await (await handlePreview(get(url), assets(), { fetcher })).text() + expect(content(html, 'property', 'og:title')).toBe(goal.title) + }) +}) + +describe('a project link', () => { + it('claims the name, and asks nobody anything', async () => { + const { fetcher, calls } = network() + for (const path of ['/p/radial', '/p/radial/system', '/p/my%20project/settings']) { + const html = await (await handlePreview(get(path), assets(), { fetcher })).text() + expect(content(html, 'property', 'og:title')).toBe(path.includes('my%20') ? 'my project' : 'radial') + expect(content(html, 'property', 'og:type')).toBe('website') + } + // A project route carries no DID, so there is no record to fetch and no PDS to bother. + expect(calls).toEqual([]) + }) +}) + +describe('when there is nothing to say', () => { + it('serves the shell with its defaults rather than an error', async () => { + // A goal that does not exist, a DID that does not resolve, a PDS that is down, a record of the + // wrong shape: every one of these is a real person following a link, so every one of them gets + // the app and the branded card. + const cases: NetworkOptions[] = [ + { records: {} }, + { fail: (url) => (url.includes('plc.directory') ? new Response('no', { status: 404 }) : undefined) }, + { fail: (url) => (url.includes(PDS) ? new Response('down', { status: 503 }) : undefined) }, + { records: { [GOAL_URI]: { ...goal, title: 42 } } }, + { document: { id: DID, service: [] } }, + ] + for (const options of cases) { + const { fetcher } = network(options) + const response = await handlePreview(get(`/g/${DID}/g1`), assets(), { fetcher }) + const html = await response.text() + expect(response.status).toBe(200) + expect(title(html)).toBe('Radial') + expect(content(html, 'property', 'og:type')).toBe('website') + expect(html).toContain('%sveltekit.body%') + } + }) + + it('refuses a PDS that is not https, whoever the DID document names', async () => { + const { fetcher } = network({ + document: { + id: DID, + service: [{ type: 'AtprotoPersonalDataServer', serviceEndpoint: 'http://pds.example' }], + }, + }) + const html = await (await handlePreview(get(`/g/${DID}/g1`), assets(), { fetcher })).text() + expect(title(html)).toBe('Radial') + }) + + it('serves the shell for a path inside its routes that names no record', async () => { + // `_routes.json` sends all of `/g/*` and `/p/*` here, including the malformed ones. They are + // still the app, and still have to load it. + const { fetcher } = network() + for (const path of ['/g/nonsense', '/g/did:plc:abc/rk/extra', '/p/']) { + const response = await handlePreview(get(path), assets(), { fetcher }) + expect(response.status).toBe(200) + expect(await response.text()).toContain('%sveltekit.body%') + } + }) +}) + +describe('the cache', () => { + it('answers a repeat unfurl without touching a PDS', async () => { + const entries = new Map<string, Response>() + const cache: EdgeCache = { + match: async (request) => entries.get(request.url)?.clone(), + put: async (request, response) => void entries.set(request.url, response), + } + const first = network() + await handlePreview(get(`/g/${DID}/g1`), assets(), { fetcher: first.fetcher, cache }) + expect(first.calls.length).toBeGreaterThan(0) + expect([...entries.keys()]).toEqual([`https://radl.app/g/${DID}/g1`]) + + // The same link with a tracking parameter is the same link: one entry, one round trip. + const second = network() + const env = assets() + const html = await ( + await handlePreview(get(`/g/${DID}/g1?utm_source=slack`), env, { fetcher: second.fetcher, cache }) + ).text() + expect(second.calls).toEqual([]) + expect(env.served).toEqual([]) + expect(content(html, 'property', 'og:title')).toBe(goal.title) + }) +}) + +describe('methods', () => { + it('answers HEAD with the headers and no body', async () => { + const { fetcher } = network() + const response = await handlePreview(get(`/g/${DID}/g1`, { method: 'HEAD' }), assets(), { fetcher }) + expect(response.status).toBe(200) + expect(response.body).toBeNull() + expect(response.headers.get('cache-control')).toBe(PREVIEW_CACHE_CONTROL) + }) + + it('hands anything else straight to the static origin', async () => { + const env = assets() + const response = await handlePreview(get(`/g/${DID}/g1`, { method: 'POST' }), env, { + fetcher: network().fetcher, + }) + expect(env.served).toEqual([`/g/${DID}/g1`]) + expect(response.status).toBe(404) + }) +}) diff --git a/packages/ui/src/edge/preview.ts b/packages/ui/src/edge/preview.ts new file mode 100644 index 0000000..3663486 --- /dev/null +++ b/packages/ui/src/edge/preview.ts @@ -0,0 +1,122 @@ +/** + * What the Pages worker does — everything except being the entry point, which is `worker.ts`. + * + * **This is not a view server coming back.** It never materializes, never reads an index, never + * writes, and holds no credential. It fetches the same static shell Pages would have served, reads + * the one or two public records the URL already names, and replaces five strings in the `<head>` + * before handing the identical document to whoever asked — crawler or person, no branch between + * them. The app in the browser still resolves everything for itself; every other host serving this + * bundle serves the plain static files and gets the shell's defaults, which are true. + * + * `static/_routes.json` confines it to two path families, so assets, `/client-metadata.json` and + * every other route never reach it and keep today's byte-for-byte static path. Inside them the + * worker always answers with the shell — it does not lean on `_redirects` applying behind a + * function — so the failure mode of everything below is a generic preview, never an error page. + */ + +import { goalPreview, projectPreview, unitPreview, type Preview } from '../lib/meta.js' +import { rewriteHead } from './head.js' +import { previewTransport, readGoalPreview } from './records.js' +import { canonicalUrl, previewTarget, type PreviewTarget } from './target.js' + +/** Long enough for a DID resolution and a repo read, short enough that nobody waits on it. */ +export const RECORD_TIMEOUT_MS = 1500 + +/** + * Five minutes, because a goal's title is immutable — a goal is never edited in place — so this is + * about a new version having landed, not about the preview being wrong. `stale-while-revalidate` + * keeps a burst of unfurls off members' PDSes without pinning anyone to a stale card for long. + */ +export const PREVIEW_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=3600' + +export interface AssetFetcher { + fetch(request: Request): Promise<Response> +} + +export interface Env { + ASSETS: AssetFetcher +} + +/** The slice of the Cache API this uses. `caches.default` in workerd; absent everywhere else. */ +export interface EdgeCache { + match(request: Request): Promise<Response | undefined> + put(request: Request, response: Response): Promise<void> +} + +export interface PreviewDeps { + /** The `fetch` used for DID resolution and record reads. Injected so tests can stand in for it. */ + fetcher?: (input: string | URL, init?: RequestInit) => Promise<Response> + cache?: EdgeCache | undefined + waitUntil?: (work: Promise<unknown>) => void + timeoutMs?: number +} + +export async function handlePreview( + request: Request, + env: Env, + deps: PreviewDeps = {}, +): Promise<Response> { + if (request.method !== 'GET' && request.method !== 'HEAD') return env.ASSETS.fetch(request) + + const url = new URL(request.url) + const target = previewTarget(url) + const canonical = canonicalUrl(url, target) + + // Cached by the canonical URL rather than the request's, so a link that picked up a tracking + // parameter on its way through a chat app shares the entry with the link it was made from. + const key = new Request(canonical, { method: 'GET' }) + const hit = target ? await deps.cache?.match(key).catch(() => undefined) : undefined + if (hit) return bodyFor(request, hit) + + const shell = await env.ASSETS.fetch(new Request(new URL('/index.html', url), { method: 'GET' })) + if (!target || !shell.ok || !isHtml(shell)) return bodyFor(request, shell) + + const preview = await previewFor(target, deps).catch(() => undefined) + if (!preview) return bodyFor(request, shell) + + const html = rewriteHead(await shell.text(), { ...preview, url: canonical }) + const headers = new Headers(shell.headers) + headers.set('content-type', 'text/html; charset=utf-8') + headers.set('cache-control', PREVIEW_CACHE_CONTROL) + headers.delete('content-length') + const response = new Response(html, { status: shell.status, headers }) + + if (deps.cache) { + const stored = deps.cache.put(key, response.clone()).catch(() => undefined) + if (deps.waitUntil) deps.waitUntil(stored) + else await stored + } + return bodyFor(request, response) +} + +/** + * What this URL is about — or `undefined`, meaning the shell's defaults already say everything + * that can honestly be said. + */ +export async function previewFor( + target: PreviewTarget, + deps: PreviewDeps = {}, +): Promise<Preview | undefined> { + if (target.kind === 'project') return projectPreview(target.name) + + const transport = previewTransport(deps.fetcher ?? ((input, init) => fetch(input, init))) + const signal = AbortSignal.timeout(deps.timeoutMs ?? RECORD_TIMEOUT_MS) + const records = await readGoalPreview(transport, target, signal) + if (!records) return undefined + return records.unit ? unitPreview(records.goal, records.unit) : goalPreview(records.goal) +} + +const isHtml = (response: Response): boolean => + (response.headers.get('content-type') ?? '').toLowerCase().includes('text/html') + +/** A `HEAD` gets the headers and none of the body, whatever the body was going to be. */ +const bodyFor = (request: Request, response: Response): Response => + request.method === 'HEAD' + ? new Response(null, { status: response.status, headers: response.headers }) + : response + +/** `caches.default` is workerd's, and is not the `CacheStorage` the DOM lib describes. */ +export function defaultCache(): EdgeCache | undefined { + const storage = (globalThis as { caches?: { default?: EdgeCache } }).caches + return storage?.default +} diff --git a/packages/ui/src/edge/records.ts b/packages/ui/src/edge/records.ts new file mode 100644 index 0000000..c5b159a --- /dev/null +++ b/packages/ui/src/edge/records.ts @@ -0,0 +1,74 @@ +/** + * Reading the one or two public records a preview describes. + * + * This is `@radial/atproto` used exactly as the browser uses it — DID → PDS → `getRecord`, on bare + * `fetch`, unauthenticated, with the lexicon validation `FetchRepoTransport` already does. The + * worker holds no credential of any kind and could not write if it wanted to; everything it reads + * is public data that anyone can read the same way. + * + * The transport is built around an injected `fetch` so the whole path is drivable from a test with + * canned DID documents and records, which is the same shape `@radial/atproto`'s own tests use. + */ + +import { + FetchRepoTransport, + pdsEndpoint, + resolveDidDocument, + type FetchLike, + type ResolvedRecord, +} from '@radial/atproto' +import type { ArtifactRecord, ArtifactRequestRecord, GoalRecord } from '@radial/core' +import { COLLECTIONS } from '@radial/core' +import type { PreviewTarget } from './target.js' + +/** + * A transport that will only talk to an `https:` PDS. + * + * A DID document names its own host, so the set of origins this can be pointed at is open by + * design. There is no internal network behind a Cloudflare worker to reach into, so this is not + * plugging an SSRF hole so much as refusing to do something there is no reason to do: read a + * member's records in the clear on the strength of a document anyone could have published. + */ +export function previewTransport(fetcher: FetchLike): FetchRepoTransport { + return new FetchRepoTransport(async (did, signal) => { + const document = await resolveDidDocument(did, (input, init) => + fetcher(input, signal ? { ...init, signal } : init), + ) + const endpoint = pdsEndpoint(document, did) + if (endpoint.protocol !== 'https:') { + throw new Error(`Refusing a non-https PDS for ${did}: ${endpoint.protocol}`) + } + return endpoint + }, fetcher) +} + +export interface PreviewRecords { + goal: GoalRecord + unit?: ArtifactRecord | ArtifactRequestRecord +} + +/** + * The records behind a goal deep link. Both reads are in flight at once and share one deadline; a + * unit that fails to arrive degrades to the goal on its own rather than sinking the whole preview, + * and a goal that fails to arrive means there is nothing to say and the caller keeps the defaults. + */ +export async function readGoalPreview( + transport: Pick<FetchRepoTransport, 'getRecord'>, + target: Extract<PreviewTarget, { kind: 'goal' }>, + signal?: AbortSignal, +): Promise<PreviewRecords | undefined> { + const [goal, unit] = await Promise.all([ + settled(transport.getRecord(target.goal, signal)), + target.unit ? settled(transport.getRecord(target.unit, signal)) : Promise.resolve(undefined), + ]) + if (goal?.value.$type !== COLLECTIONS.goal) return undefined + const value = unit?.value + const usable = + value?.$type === COLLECTIONS.artifact || value?.$type === COLLECTIONS.artifactRequest + ? value + : undefined + return { goal: goal.value, ...(usable ? { unit: usable } : {}) } +} + +const settled = (pending: Promise<ResolvedRecord>): Promise<ResolvedRecord | undefined> => + pending.catch(() => undefined) diff --git a/packages/ui/src/edge/target.test.ts b/packages/ui/src/edge/target.test.ts new file mode 100644 index 0000000..e4d3b39 --- /dev/null +++ b/packages/ui/src/edge/target.test.ts @@ -0,0 +1,91 @@ +import { COLLECTIONS } from '@radial/core' +import { describe, expect, it } from 'vitest' +import { canonicalUrl, previewTarget, unitUri } from './target.js' + +const at = (path: string) => new URL(path, 'https://radl.app') +const GOAL = 'at://did:plc:n6ku5xddiuguwze3f356evla/com.disnetdev.radial.goal/3mrvumwh3gk2o' +const ARTIFACT = 'at://did:plc:n6ku5xddiuguwze3f356evla/com.disnetdev.radial.artifact/plan-abc' +const REQUEST = + 'at://did:plc:n6ku5xddiuguwze3f356evla/com.disnetdev.radial.artifactRequest/3mrvv5kltnp2o' + +describe('a goal deep link', () => { + it('reconstructs the AT URI the route was minted from', () => { + // The mirror of `space.ts`'s `goalHref`. If these two ever disagree the preview describes a + // different record from the one the app opens. + expect(previewTarget(at('/g/did:plc:n6ku5xddiuguwze3f356evla/3mrvumwh3gk2o'))).toEqual({ + kind: 'goal', + goal: GOAL, + }) + }) + + it('carries the unit a `?unit=` link opens', () => { + expect(previewTarget(at(`/g/did:plc:abc/rk?unit=${encodeURIComponent(ARTIFACT)}`))).toEqual({ + kind: 'goal', + goal: `at://did:plc:abc/${COLLECTIONS.goal}/rk`, + unit: ARTIFACT, + }) + }) + + it('ignores `v`, which selects a version rather than a record', () => { + const target = previewTarget(at(`/g/did:plc:abc/rk?unit=${encodeURIComponent(REQUEST)}&v=2`)) + expect(target).toEqual({ kind: 'goal', goal: `at://did:plc:abc/${COLLECTIONS.goal}/rk`, unit: REQUEST }) + }) + + it('drops a unit that is not a unit, and still previews the goal', () => { + for (const unit of [ + 'not-a-uri', + 'https://evil.test/', + 'at://did:plc:abc/com.disnetdev.radial.review/r1', + 'at://did:plc:abc/app.bsky.feed.post/p1', + 'at://did:plc:abc/com.disnetdev.radial.artifact/a/b', + ]) { + expect(unitUri(unit)).toBeUndefined() + expect(previewTarget(at(`/g/did:plc:abc/rk?unit=${encodeURIComponent(unit)}`))).toEqual({ + kind: 'goal', + goal: `at://did:plc:abc/${COLLECTIONS.goal}/rk`, + }) + } + }) + + it('refuses anything that is not a DID and an rkey', () => { + for (const path of ['/g', '/g/did:plc:abc', '/g/notadid/rk', '/g/did:plc:abc/rk/extra', '/g//rk']) { + expect(previewTarget(at(path))).toBeUndefined() + } + }) +}) + +describe('a project link', () => { + it('takes the name out of the route, decoded', () => { + expect(previewTarget(at('/p/radial'))).toEqual({ kind: 'project', name: 'radial' }) + expect(previewTarget(at('/p/my%20project'))).toEqual({ kind: 'project', name: 'my project' }) + }) + + it('treats the sub-views as the same project, because the URL says nothing else', () => { + expect(previewTarget(at('/p/radial/system'))).toEqual({ kind: 'project', name: 'radial' }) + expect(previewTarget(at('/p/radial/settings'))).toEqual({ kind: 'project', name: 'radial' }) + }) +}) + +describe('everything else', () => { + it('has no record to describe, so the shell already says all there is', () => { + for (const path of ['/', '/space', '/awaiting', '/inflight', '/logbook', '/me', '/og.png']) { + expect(previewTarget(at(path))).toBeUndefined() + } + }) +}) + +describe('the canonical URL', () => { + it('keeps what changes the subject and drops what does not', () => { + const url = at(`/g/did:plc:abc/rk?unit=${encodeURIComponent(ARTIFACT)}&v=2&utm_source=slack`) + const target = previewTarget(url) + expect(canonicalUrl(url, target)).toBe( + `https://radl.app/g/did:plc:abc/rk?unit=${encodeURIComponent(ARTIFACT)}`, + ) + }) + + it('is the bare path when nothing was named', () => { + expect(canonicalUrl(at('/p/radial?frame'), previewTarget(at('/p/radial?frame')))).toBe( + 'https://radl.app/p/radial', + ) + }) +}) diff --git a/packages/ui/src/edge/target.ts b/packages/ui/src/edge/target.ts new file mode 100644 index 0000000..101b9a4 --- /dev/null +++ b/packages/ui/src/edge/target.ts @@ -0,0 +1,76 @@ +/** + * URL → the record a preview should describe. + * + * The whole reason a link preview is possible without a view server: a Radial deep link already + * carries everything needed to find the public record it names. `/g/<did>/<rkey>` reconstructs the + * goal's AT URI exactly as `space.ts`'s `goalHref` minted it, and `?unit=` carries the unit's key, + * which is an AT URI too. Nothing is looked up in an index, and nothing observer-local is consulted + * — this is string parsing, and the fetch that follows is an unauthenticated public read. + * + * Anything unrecognised returns `undefined`, which means "serve the shell as built". That includes + * a malformed DID, a `unit` that is not an AT URI, and a `unit` naming a collection that is not one + * of the two a drawer can open: a preview is never worth an error page, because real people load + * these URLs too. + */ + +import { COLLECTIONS } from '@radial/core' + +export type PreviewTarget = + | { kind: 'goal'; goal: string; unit?: string } + | { kind: 'project'; name: string } + +/** The AT URI grammar `parseAtUri` accepts, applied before anything is fetched. */ +const AT_URI = /^at:\/\/(did:[a-z]+:[A-Za-z0-9._%:-]+)\/([A-Za-z0-9.-]+)\/([A-Za-z0-9._~:-]+)$/ +const DID = /^did:[a-z]+:[A-Za-z0-9._%:-]+$/ +const RKEY = /^[A-Za-z0-9._~:-]+$/ + +/** The two collections a unit key can name: what landed, or what was asked for (§3.7). */ +const UNIT_COLLECTIONS: readonly string[] = [COLLECTIONS.artifact, COLLECTIONS.artifactRequest] + +export function unitUri(value: string | null): string | undefined { + if (!value) return undefined + const parts = AT_URI.exec(value) + return parts && UNIT_COLLECTIONS.includes(parts[2] as string) ? value : undefined +} + +export function previewTarget(url: URL): PreviewTarget | undefined { + const segments = url.pathname.split('/').filter(Boolean).map(decodeSegment) + if (segments.length === 0) return undefined + + if (segments[0] === 'g' && segments.length === 3) { + const [, did, rkey] = segments as [string, string, string] + if (!DID.test(did) || !RKEY.test(rkey)) return undefined + const unit = unitUri(url.searchParams.get('unit')) + return { kind: 'goal', goal: `at://${did}/${COLLECTIONS.goal}/${rkey}`, ...(unit ? { unit } : {}) } + } + + // `/p/<name>`, and the sub-views that hang off it. All of them are that project, seen from a + // different angle; the name is the only thing the URL carries, so it is the only thing they can + // differ by — which is to say they do not. + if (segments[0] === 'p' && segments.length >= 2 && segments.length <= 3) { + const name = segments[1] as string + return name ? { kind: 'project', name } : undefined + } + + return undefined +} + +/** A path segment, undone. A name that is not valid percent-encoding is taken literally. */ +function decodeSegment(segment: string): string { + try { + return decodeURIComponent(segment) + } catch { + return segment + } +} + +/** + * The URL a preview should claim as its own — the request's, stripped of everything that does not + * change what the page is about. Two people sharing the same goal with different campaign tags + * should unfurl as one thing, and should share one cache entry. + */ +export function canonicalUrl(url: URL, target: PreviewTarget | undefined): string { + const canonical = new URL(url.pathname, url.origin) + if (target?.kind === 'goal' && target.unit) canonical.searchParams.set('unit', target.unit) + return canonical.toString() +} diff --git a/packages/ui/src/edge/worker.ts b/packages/ui/src/edge/worker.ts new file mode 100644 index 0000000..d7c54fd --- /dev/null +++ b/packages/ui/src/edge/worker.ts @@ -0,0 +1,26 @@ +/** + * `build/_worker.js` — the entry point Cloudflare Pages runs, and nothing else. + * + * It is this thin on purpose. workerd inspects every *named* export of an entry module and rejects + * any that is not a function or an `ExportedHandler`, so a module that exports both the handler and, + * say, a cache-control constant fails at `Starting local server` with `Incorrect type for map entry` + * — at deploy time, on a file that bundled and typechecked cleanly. Keeping the handler in its own + * module with one default export makes that class of mistake unreachable, and leaves `preview.ts` + * free to export the constants and functions its tests drive. + * + * `test/edge-bundle.test.mjs` holds the emitted bundle to exactly one export, named `default`. + */ + +import { defaultCache, handlePreview, type Env } from './preview.js' + +interface ExecutionContext { + waitUntil(work: Promise<unknown>): void +} + +export default { + fetch: (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => + handlePreview(request, env, { + cache: defaultCache(), + waitUntil: (work) => ctx.waitUntil(work), + }), +} diff --git a/packages/ui/src/lib/meta.test.ts b/packages/ui/src/lib/meta.test.ts new file mode 100644 index 0000000..123cf4c --- /dev/null +++ b/packages/ui/src/lib/meta.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it } from 'vitest' +// The shell itself, as text. The defaults a crawler reads live in the HTML, and the only way they +// stay the same sentence as the one the worker falls back to is for a test to hold both copies. +import shell from '../app.html?raw' +import { + DESCRIPTION_MAX, + EXCERPT_MAX, + SITE_DESCRIPTION, + SITE_NAME, + TITLE_MAX, + clamp, + excerpt, + goalPreview, + pageTitle, + projectPreview, + unitPreview, +} from './meta.js' +import type { ArtifactRecord, ArtifactRequestRecord, GoalRecord } from '@radial/core' + +const ref = (uri: string) => ({ uri, cid: 'bafyone' }) + +const goal: GoalRecord = { + $type: 'com.disnetdev.radial.goal', + space: ref('at://did:plc:alice/com.disnetdev.radial.space/self'), + project: ref('at://did:plc:alice/com.disnetdev.radial.project/radial'), + title: 'make links to radial have nice og images', + body: '# Why\n\nincluding deep links to goals and artifacts, which today unfurl as nothing at all.', + createdAt: '2026-07-31T00:00:00.000Z', +} + +const artifact = (over: Partial<ArtifactRecord> = {}): ArtifactRecord => ({ + $type: 'com.disnetdev.radial.artifact', + request: ref('at://did:plc:alice/com.disnetdev.radial.artifactRequest/r1'), + goal: ref('at://did:plc:alice/com.disnetdev.radial.goal/g1'), + type: 'plan', + body: 'The links that matter are goal deep links.', + links: {}, + createdAt: '2026-07-31T01:00:00.000Z', + ...over, +}) + +const request = (over: Partial<ArtifactRequestRecord> = {}): ArtifactRequestRecord => ({ + $type: 'com.disnetdev.radial.artifactRequest', + goal: ref('at://did:plc:alice/com.disnetdev.radial.goal/g1'), + type: 'implementation', + basedOn: [], + createdAt: '2026-07-31T02:00:00.000Z', + ...over, +}) + +describe('the shell and this module agree on the defaults', () => { + it('writes the same sentence in both places', () => { + // Twice in the shell: `description` for a search engine, `og:description` for an unfurler. + const occurrences = shell.split(SITE_DESCRIPTION).length - 1 + expect(occurrences).toBe(2) + expect(shell).toContain(`<title>${SITE_NAME}`) + expect(shell).toContain(`content="${SITE_NAME}"`) + }) + + it('points og:image at an absolute URL the build is told', () => { + // Relative image URLs are ignored by every major unfurler, and a static bundle cannot discover + // its own origin — so this has to be the build-time knob, not a path. + expect(shell).toContain('content="%sveltekit.env.PUBLIC_RADIAL_ORIGIN%/og.png"') + expect(shell).toContain('content="summary_large_image"') + expect(shell).toContain('property="og:image:width" content="1200"') + expect(shell).toContain('property="og:image:height" content="630"') + }) + + it('leaves og:url out of the defaults', () => { + // One shell answers every route, so any og:url baked into it would be wrong on all but one of + // them. The worker adds it where the URL is actually known (`edge/head.ts`). + expect(shell).not.toContain('property="og:url"') + }) +}) + +describe('clamp', () => { + it('leaves anything short enough alone', () => { + expect(clamp('a goal', 40)).toBe('a goal') + }) + + it('cuts on a word boundary and says it cut', () => { + const cut = clamp('the quick brown fox jumps over the lazy dog', 20) + expect(cut).toBe('the quick brown fox…') + expect([...cut].length).toBeLessThanOrEqual(20) + }) + + it('counts code points, so a cut never splits a character', () => { + // Eleven astral code points; cut to five must leave five whole ones and the ellipsis. + const cut = clamp('👩‍💻🚀🌍🛰️🔭📡🧭🗺️🧪🔬🧬', 5) + expect([...cut].length).toBeLessThanOrEqual(5) + expect(cut.endsWith('…')).toBe(true) + expect(cut).not.toContain('�') + }) + + it('does not leave punctuation dangling before the ellipsis', () => { + expect(clamp('one, two, three, four', 12)).toBe('one, two…') + }) +}) + +describe('excerpt', () => { + it('flattens markdown to the sentence underneath it', () => { + expect(excerpt('# Heading\n\nA **bold** claim with a [link](https://example.test).')).toBe( + 'Heading A bold claim with a link.', + ) + }) + + it('keeps blocks apart', () => { + // `plain()` concatenates what it is handed, which would run a heading into the line below it. + expect(excerpt('one\n\ntwo')).toBe('one two') + }) + + it('resolves entities rather than showing their source', () => { + expect(excerpt('a & b')).toBe('a & b') + }) + + it('never runs past the excerpt cap', () => { + expect([...excerpt('word '.repeat(200))].length).toBeLessThanOrEqual(EXCERPT_MAX) + }) +}) + +describe('pageTitle', () => { + it('trails the site name so a narrowed tab still says where it is', () => { + expect(pageTitle('A goal')).toBe('A goal · Radial') + }) + + it('is the site name alone when there is nothing to name', () => { + expect(pageTitle()).toBe(SITE_NAME) + expect(pageTitle(' ')).toBe(SITE_NAME) + }) +}) + +describe('previews', () => { + it('describes a goal by its title and the top of its body', () => { + const preview = goalPreview(goal) + expect(preview.title).toBe(goal.title) + expect(preview.description).toBe( + 'Why including deep links to goals and artifacts, which today unfurl as nothing at all.', + ) + expect(preview.type).toBe('article') + }) + + it('names an artifact by its own title when it has one', () => { + expect(unitPreview(goal, artifact({ title: 'Plan: OG link previews' })).title).toBe( + 'Plan: OG link previews', + ) + }) + + it('falls back to naming the type against the goal', () => { + expect(unitPreview(goal, artifact()).title).toBe(`plan — ${goal.title}`) + }) + + it('says a request is a request, since nothing has landed yet', () => { + const preview = unitPreview(goal, request({ brief: 'ship steps 1-4' })) + expect(preview.title).toBe(`implementation requested — ${goal.title}`) + expect(preview.description).toBe('ship steps 1-4') + }) + + it('borrows the goal when a request carries no brief', () => { + // `brief` is optional in the lexicon, and the goal it was asked against is never empty. + expect(unitPreview(goal, request()).description).toBe(goalPreview(goal).description) + }) + + it('claims only the name for a project, which is all the URL carries', () => { + const preview = projectPreview('radial') + expect(preview.title).toBe('radial') + expect(preview.description).toContain('radial') + expect(preview.type).toBe('website') + }) + + it('caps what a record can make it emit', () => { + const shouting = { ...goal, title: 'x'.repeat(1000), body: 'y'.repeat(5000) } + const preview = goalPreview(shouting) + expect([...preview.title].length).toBeLessThanOrEqual(TITLE_MAX) + expect([...preview.description].length).toBeLessThanOrEqual(DESCRIPTION_MAX) + }) +}) diff --git a/packages/ui/src/lib/meta.ts b/packages/ui/src/lib/meta.ts new file mode 100644 index 0000000..dfa08aa --- /dev/null +++ b/packages/ui/src/lib/meta.ts @@ -0,0 +1,128 @@ +/** + * What a Radial link says about itself — the tab title, and the card an unfurler draws. + * + * Two consumers, one set of rules. The browser calls this to keep `document.title` honest as the + * reader moves around, and the Cloudflare Pages worker (`src/edge/`) calls the same functions to + * write `` and the `og:*` tags into the shell before a crawler ever sees it. A crawler does + * not run the app, so the second consumer is the only reason a pasted link says anything at all; + * having it share this module is what keeps the two from disagreeing about the same URL. + * + * Everything here is pure and text-only. Bodies are member- and agent-authored, so nothing is + * assembled as markup: a title is a string, a description is a string, and the one place either + * becomes HTML (`edge/head.ts`) escapes it there. Same posture as `prose.ts` — no `{@html}` near an + * authored body, ever. + */ + +import type { ArtifactRecord, ArtifactRequestRecord, GoalRecord } from '@radial/core' +import { plain, prose } from './prose.js' + +export const SITE_NAME = 'Radial' + +/** + * The one sentence every Radial URL falls back to. It is repeated verbatim in `src/app.html`, which + * is the copy a crawler reads on any route the worker does not touch and on any host that is not + * Pages; `test/og-meta.test.mjs` holds the two to each other. + */ +export const SITE_DESCRIPTION = + 'Coordinate humans and coding agents on software goals. Every unit of agent work is requested by a human and lands as a signed, reviewable artifact in its author’s own atproto repo — no central server.' + +/** Hard caps. Unfurlers truncate anyway; these bound what a hostile record can make us emit. */ +export const TITLE_MAX = 300 +export const DESCRIPTION_MAX = 300 + +/** Where a body excerpt is cut. Past roughly this, no unfurler shows the rest. */ +export const EXCERPT_MAX = 200 + +/** Collapse a run of authored text to one line: no newlines, no runs of spaces, trimmed. */ +export const oneLine = (text: string): string => text.replace(/\s+/gu, ' ').trim() + +/** + * Cut `text` to `max` characters, on a word boundary when there is one near the end, with an + * ellipsis to say it was cut. Counted in code points rather than UTF-16 units so a truncation never + * lands inside a surrogate pair and leaves half a character behind. + */ +export function clamp(text: string, max: number): string { + const points = [...text] + if (points.length <= max) return text + const head = points.slice(0, max - 1).join('') + // The cut already fell between words when the character it displaced was a space; otherwise back + // up to the last one — unless that throws away most of the line, in which case a word cut in half + // reads better than three characters and an ellipsis. + const boundary = /\s/u.test(points[max - 1] ?? '') ? head.length : head.lastIndexOf(' ') + const kept = boundary > head.length * 0.6 ? head.slice(0, boundary) : head + return `${kept.replace(/[\s,;:.—-]+$/u, '')}…` +} + +/** + * A markdown body, flattened to the plain sentence a preview can show. + * + * Blocks are flattened one at a time and rejoined with a space: `plain()` concatenates what it is + * given, which is right for the runs inside a paragraph and wrong across them — a heading would + * otherwise run straight into the sentence beneath it. + */ +export const excerpt = (body: string, max = EXCERPT_MAX): string => + clamp(oneLine(prose(body).map((block) => plain([block])).join(' ')), max) + +/** The tab title. The site name trails it so a row of tabs stays identifiable when they narrow. */ +export const pageTitle = (label?: string): string => + label && label.trim() ? `${clamp(oneLine(label), TITLE_MAX)} · ${SITE_NAME}` : SITE_NAME + +export interface Preview { + /** `og:title` — and, with the site name appended, `<title>`. */ + title: string + description: string + /** `article` once a preview is about one record rather than about the product. */ + type: 'website' | 'article' +} + +/** The defaults: what the shell already says, for a URL that names no record. */ +export const sitePreview = (): Preview => ({ + title: SITE_NAME, + description: SITE_DESCRIPTION, + type: 'website', +}) + +export const goalPreview = (goal: GoalRecord): Preview => ({ + title: clamp(oneLine(goal.title), TITLE_MAX), + description: excerpt(goal.body), + type: 'article', +}) + +/** + * One unit of work on a goal — the drawer a `?unit=` link opens. + * + * A unit is keyed by the root artifact's URI once something has landed, and by the driving + * request's URI until then (§3.7), so both are reachable and both are named here: what landed, or + * what was asked for. An artifact that carries no title falls back to naming its type against the + * goal, which is what the row it came from reads as too. + */ +export function unitPreview( + goal: GoalRecord, + unit: ArtifactRecord | ArtifactRequestRecord, +): Preview { + const goalTitle = oneLine(goal.title) + if (unit.$type === 'com.disnetdev.radial.artifact') { + const title = unit.title ? oneLine(unit.title) : `${unit.type} — ${goalTitle}` + return { title: clamp(title, TITLE_MAX), description: excerpt(unit.body), type: 'article' } + } + return { + title: clamp(`${unit.type} requested — ${goalTitle}`, TITLE_MAX), + // A request need not carry a brief; the goal it was asked against is the next best thing to + // say, and is never empty. + description: excerpt(unit.brief ?? goal.body), + type: 'article', + } +} + +/** + * A project route carries a name and nothing else — no DID, so there is no record to read without + * already knowing the space. The name is all a preview can honestly claim. + */ +export const projectPreview = (name: string): Preview => ({ + title: clamp(oneLine(name), TITLE_MAX), + description: clamp( + `Goals, requests and signed artifacts for ${oneLine(name)} on ${SITE_NAME}.`, + DESCRIPTION_MAX, + ), + type: 'website', +}) diff --git a/packages/ui/src/routes/+layout.svelte b/packages/ui/src/routes/+layout.svelte index d428276..af95575 100644 --- a/packages/ui/src/routes/+layout.svelte +++ b/packages/ui/src/routes/+layout.svelte @@ -8,6 +8,7 @@ import { keepFocus } from '$lib/focus.js' import { watchJoin } from '$lib/join.svelte.js' import { onKeydown, registerHere } from '$lib/keys.js' + import { pageTitle } from '$lib/meta.js' import { rail } from '$lib/rail.svelte.js' import Diagnostics from '$lib/components/Diagnostics.svelte' import NewGoal from '$lib/components/NewGoal.svelte' @@ -127,6 +128,42 @@ return [{ label: spaceName }] }) + // The tab, the history entry and the bookmark. A crawler is served the answer to this question in + // the HTML itself — the shell's defaults, upgraded per record by the Pages worker for a goal or a + // project deep link (`src/edge/`, `lib/meta.ts`) — and this is the same answer, reached the same + // way, for the person who actually opened the link. They have to agree: a bookmark that says one + // thing and the card the reader shared saying another is the app disagreeing with itself about + // what a URL is. + // + // Presentation only. Nothing here reads the index for anything the crumb above does not already + // ask it for, and nothing is written. + const LISTS: Record<string, string> = { + '/': 'Goals', + '/space': 'Space', + '/me': 'For me', + '/awaiting': 'Awaiting input', + '/inflight': 'With an agent', + '/logbook': 'Logbook', + } + const title = $derived.by(() => { + if (!space) return undefined + if (route.startsWith('/g/')) { + const goal = goalByRoute(space.index, page.params.did ?? '', page.params.rkey ?? '') + return goal?.target.value.title + } + if (route.startsWith('/p/')) { + const project = projectByName(space.index, page.params.project ?? '') + if (!project) return undefined + if (route.endsWith('/system')) return `${project.name} · System` + if (route.endsWith('/settings')) return `${project.name} · Settings` + return project.name + } + return LISTS[route] + }) + $effect(() => { + document.title = pageTitle(title) + }) + // Which project the view being shown belongs to, if it belongs to one. The ⊕ offers a goal // everywhere, so this is what makes that goal land where the reader already is rather than asking // a question the route has already answered. diff --git a/packages/ui/static/_routes.json b/packages/ui/static/_routes.json new file mode 100644 index 0000000..fb6ebbe --- /dev/null +++ b/packages/ui/static/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/g/*", "/p/*"], + "exclude": [] +} diff --git a/packages/ui/static/og.png b/packages/ui/static/og.png new file mode 100644 index 0000000000000000000000000000000000000000..2d13b033826c84d09b354b5a14980506ab19aec6 GIT binary patch literal 63814 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'Gn8CO3^$TI;_$ij}j$~|k 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%0D#gfl~ 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 Ib4q9e0F6<(2><{9 literal 0 HcmV?d00001 diff --git a/packages/ui/test/edge-bundle.test.mjs b/packages/ui/test/edge-bundle.test.mjs new file mode 100644 index 0000000..9aecdc1 --- /dev/null +++ b/packages/ui/test/edge-bundle.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, it } from 'node:test' +import * as esbuild from 'esbuild' + +// The Pages worker's counterpart to `browser-bundle.test.mjs`. That one proves the app's path stays +// free of Node builtins; this one proves the same of the edge path, for a different reason: workerd +// has `fetch`, `URL` and the web streams and nothing else, so a `node:` import that slips in is a +// deploy that publishes and then 500s on every goal link. Failing here costs a CI run instead. + +const here = dirname(fileURLToPath(import.meta.url)) +const root = resolve(here, '..') + +const bundle = () => + esbuild.build({ + entryPoints: [resolve(root, 'src', 'edge', 'worker.ts')], + bundle: true, + format: 'esm', + platform: 'neutral', + target: 'es2022', + mainFields: ['browser', 'module', 'main'], + conditions: ['browser', 'import', 'default'], + write: false, + logLevel: 'silent', + }) + +/** The emitted bundle, imported as the module Pages would run. */ +async function load() { + const result = await bundle() + const source = Buffer.from(result.outputFiles[0].text).toString('base64') + return import(`data:text/javascript;base64,${source}`) +} + +describe('the edge worker bundle', () => { + it('builds for workerd with no node builtins', async () => { + const result = await bundle() + assert.deepEqual(result.errors, []) + const code = result.outputFiles[0].text + assert.equal(/require\(["']node:/.test(code), false) + assert.equal(/from ?["']node:/.test(code), false) + // The fold, the store and the SQLite implementations have no business here either: this reads + // one record and rewrites a head. If `materialize` ever appears in this bundle, something has + // imported the app's world into the edge's. + assert.equal(code.includes('SqliteRecordStore'), false) + }) + + it('is what the build emits, into the directory the deploy uploads', async () => { + const manifest = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')) + assert.match(manifest.scripts.build, /scripts\/build-worker\.mjs/) + }) + + it('is confined to the two path families whose URLs name a record', async () => { + // Everything outside these keeps the pure static path — assets, `/client-metadata.json`, and + // the per-viewer views, whose previews are the shell's defaults by nature. + const routes = JSON.parse(await readFile(resolve(root, 'static', '_routes.json'), 'utf8')) + assert.equal(routes.version, 1) + assert.deepEqual(routes.include, ['/g/*', '/p/*']) + assert.deepEqual(routes.exclude, []) + }) + + it('still serves the SPA fallback everywhere the worker does not run', async () => { + const redirects = await readFile(resolve(root, 'static', '_redirects'), 'utf8') + assert.match(redirects, /^\/\*\s+\/index\.html\s+200$/m) + }) + + it('exports the handler and nothing else', async () => { + // workerd inspects every named export of an entry module and refuses anything that is not a + // function or an `ExportedHandler`. A `export const PREVIEW_CACHE_CONTROL = …` sitting beside + // the handler is a bundle that typechecks, builds, passes every unit test, and then fails at + // `Starting local server` on the deploy. Hence `worker.ts` being one default export, and hence + // this. + const worker = await load() + assert.deepEqual(Object.keys(worker), ['default']) + assert.equal(typeof worker.default.fetch, 'function') + }) + + it('runs, as the file it is bundled into rather than as the sources', async () => { + // `src/edge/worker.test.ts` drives these paths as modules; this drives the *artifact*, because + // the failure this catches — a resolution condition, an entry point, a global that only exists + // on one runtime — only exists after bundling. Nothing is fetched: the shape a worker with no + // record to read falls back to is the shell, and the shell is the whole assertion. + const worker = await load() + const shell = await readFile(resolve(root, 'src', 'app.html'), 'utf8') + const env = { + ASSETS: { fetch: async () => new Response(shell, { headers: { 'content-type': 'text/html' } }) }, + } + const response = await worker.default.fetch( + new Request('https://radl.app/p/radial'), + env, + { waitUntil() {} }, + ) + const html = await response.text() + assert.equal(response.status, 200) + assert.match(html, /<title>radial · Radial<\/title>/) + assert.match(html, /property="og:url" content="https:\/\/radl\.app\/p\/radial"/) + assert.match(html, /%sveltekit\.body%/) + }) +}) diff --git a/packages/ui/test/og-card.test.mjs b/packages/ui/test/og-card.test.mjs new file mode 100644 index 0000000..e0391e4 --- /dev/null +++ b/packages/ui/test/og-card.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, it } from 'node:test' + +// `static/og.png` is a checked-in asset drawn by `scripts/og-card.mjs`, not a build output — the +// brand changes about as often as the mark does, and a build that redraws it is a build that can +// disagree with what shipped. So what is held here is the contract the unfurlers care about: the +// file exists, it is a PNG, and it is exactly the 1200×630 the shell declares it to be. A card +// whose real dimensions differ from its `og:image:width` is cropped or rejected outright. + +const here = dirname(fileURLToPath(import.meta.url)) +const root = resolve(here, '..') + +const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + +describe('the default card', () => { + it('is a 1200×630 PNG, which is what the shell says it is', async () => { + const png = await readFile(resolve(root, 'static', 'og.png')) + assert.deepEqual(png.subarray(0, 8), SIGNATURE) + assert.equal(png.subarray(12, 16).toString('latin1'), 'IHDR') + const width = png.readUInt32BE(16) + const height = png.readUInt32BE(20) + assert.equal(width, 1200) + assert.equal(height, 630) + + const shell = await readFile(resolve(root, 'src', 'app.html'), 'utf8') + assert.match(shell, new RegExp(`property="og:image:width" content="${width}"`)) + assert.match(shell, new RegExp(`property="og:image:height" content="${height}"`)) + assert.match(shell, /property="og:image" content="[^"]*\/og\.png"/) + }) + + it('is redrawable, and says how', async () => { + const manifest = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')) + assert.match(manifest.scripts.og, /scripts\/og-card\.mjs/) + }) +})