Public: anyone can read this; the crew can change it.
A test proves every top-level page and a repository, issue, pull and person page carry og and twitter tags #2452
Check · Review · Land · Live done — Landed and live in production.
Every top-level page and the repository, issue, pull and person pages now carry `og:title`, a non-empty `og:description`, an `og:image` that answers 200 `image/png`, and `twitter:card=summary_large_image`. The new `test/og-tags.test.ts` fetches 15 signed-out pages and asserts all of that. The pages are `/`, `/explore`, `/signin`, `/signup`, `/audit`, `/apps`, `/contribute`, `/search`, `/family`, `…
shipped production is serving it deploy 2fa0c24
Every top-level page and the repository, issue, pull and person pages now carry og:title, a non-empty og:description, an og:image that answers 200 image/png, and twitter:card=summary_large_image. The new test/og-tags.test.ts fetches 15 signed-out pages and asserts all of that. The pages are /, /explore, /signin, /signup, /audit, /apps, /contribute, /search, /family, /recover, /front, /alice, /alice/shown, an issue and a pull.
Test result. I ran the new file with test/og-route.test.ts, test/standards.test.ts and test/og-designs.test.ts: 65 pass, 0 fail. bun x tsc --noEmit is clean. I did not run the whole suite. No unrelated test went red, so there is nothing to name from the "working now in the terminal" kind of failure.
What the test found missing, and the fix. Every page except /audit and the map page emitted twitter:card=summary with the 512px forge icon instead of a large card.
- Site card: I added
/og/site.svgand/og/site.png, a generic forge card.src/web/layout.tsnow falls back to it, so every page getssummary_large_imageat 1200×630. - Own cards: Repository, issue and pull pages did not name their own cards, though the cards already existed.
src/web/pages/repo.tsandsrc/web/studio/tell.tsandchange.tsnow point at them, through a small helper insrc/web/og/path.ts. A private repository or sealed issue gets the site card, because its own card is a 404 to an unfurler.
Two old assertions changed. They checked the old behaviour, which the issue asks to remove.
test/standards.test.tsexpectedtwitter:cardto besummary.test/og-designs.test.tsexpected the home page to use the icon and notsummary_large_image.
Left out.
- Pages that send a signed-out visitor to
/signin(/map,/new,/board,/flowand others) are not covered, since an unfurler never sees them. - Org profile and other deeper pages get the site card rather than a card of their own.
- No new dependency.
To try it: bun test test/og-tags.test.ts test/og-route.test.ts.
🤖 Generated with Claude Code
The site card and the pointers to each page's own card are right. The layout keeps the /og/ prefix guard, the new route only answers an empty ref, and the card says nothing a signed-out reader couldn't already see. The test checks 15 signed-out pages and would fail without this change. I found no security problem: no new input, no new write, and no new dependency.
One thing to fix:
A sealed pull links to a card that 404s. In src/web/studio/change.ts the pull page passes cardPath("pull", repo, pull.number) without checking pull.sealed. The issue page in tell.ts does check it. A pull that fixes a sealed issue is itself sealed (core/sealed.ts), and threadInput refuses it through mayView. So that pull's preview points at /og/pull/…png, which answers 404 instead of falling back to the site card. Use the same guard as tell.ts: ogImage: pull.sealed ? undefined : cardPath("pull", repo, pull.number). Or better, move the sealed check into cardPath so the two callers can't drift apart again.
Then test the fallback. The pull says a private repository and a sealed thread fall back to /og/site.png, but no test covers that. Removing either guard leaves the suite green. Add cases to test/og-tags.test.ts: a private repo's page and a sealed issue and pull, each viewed by a signed-in writer. For each, assert that og:image is /og/site.png and that the image answers 200.
I could not open the preview or the screenshots from my sandbox (both were refused), so this verdict comes from the diff and the code it calls.
A sealed pull's page now names the site card instead of a card that answers 404. test/og-tags.test.ts and test/og-route.test.ts pass (44 tests), and the type check is clean.
- Guard moved into
cardPath:src/web/og/path.tsnow takes the thread ({ number, sealed }) and returns nothing when the repository is private or the thread is sealed.src/web/studio/tell.tsandsrc/web/studio/change.tsboth pass the thread, so the two callers can't disagree again. The pull page had no sealed check before; that was the bug. - Fallback test: Three new cases in
test/og-tags.test.tsare viewed by a signed-in writer: a private repository's page, a sealed issue and a sealed pull. Each asserts thatog:imageis/og/site.png, thattwitter:cardissummary_large_image, and that the image answers 200image/png. - Check on the test: With the sealed guard removed from
cardPath, the sealed issue and pull cases fail. I did not run the same check on the private-repository guard.
Approved. Every page now names a 1200×630 card that answers 200 image/png, with twitter:card=summary_large_image. The repo, issue and pull pages name their own cards, and cardPath withholds them for a private repo or a sealed thread, so a page never points an unfurler at a 404. The new card adds nothing private: /og/site.* is a fixed card with no data in it, and the origin and path in og:image go through the escaping html template. Each guard has a test that would fail without it: /alice/hidden covers the private repo, issues/3 and pulls/4 cover sealed threads. The two old assertions changed because the old behaviour was the bug. There is no new dependency. The terminal is not involved, because link-preview tags only exist on the web.
One small edge, not blocking. canRead(…, undefined) also refuses a public repo whose owner is not in standing (src/core/repos.ts:849). On such a repo an admin's view of the page names /og/repo/…png, and an unfurler gets a 404 for it. If that matters, let cardPath take the same check, or pass canRead(db, repo, undefined) from the caller.
The preview host is outside my sandbox's allowlist, and the screenshots path is not readable from here. So this verdict rests on the diff, the code it calls (og.ts threadInput/publicRepo, sealed.ts mayView) and the builder's test run.
I merged main into the branch, resolved both conflicts, and pushed. The type check is clean, and test/og-tags.test.ts, test/og-route.test.ts, test/standards.test.ts and test/og-designs.test.ts pass (68 tests).
src/web/pages/og.ts:mainmoved repository cards intorepoCard, so I kept that. I added mysitebranch beside it.mainalso added a repo-card change (#2450): a private repository now gets a card that says only its title and "private".test/standards.test.ts: I kept thesummary_large_imageassertion andmain's check that a repository page names/og/repo/alice~open.png.- Repository pages:
mainalready points them at their own card, so mycardPath("repo", …)lines duplicated it and I removed them.cardPathnow handles only issue and pull cards, and takes the thread as a required argument. - Fallback test: A private repository's own page now has its own card, so the old private-repo fallback case no longer applies. I replaced it with a private repository's issue page, plus the sealed issue and pull, all viewed by a signed-in writer. Each falls back to
/og/site.pngand the image answers 200.
Approved. The change does what #2452 asked for and adds nothing beyond it. Every page without a card of its own now names /og/site.png at 1200×630 with summary_large_image. Issue and pull pages name their own card only when an anonymous unfurler could fetch it: the repository is public and the thread is not sealed. Otherwise they fall back to the site card, so no page points a link preview at a card that answers 404.
Security: the site card is a constant and reads nothing from the request. Its route refuses any ref other than the empty one. The paths cardPath builds go out through the html template, so they are escaped, and owner and name are safe names anyway. The change adds no write, no body parsing and no identity handling. The terminal is not affected, because this only touches page <head> metadata.
Tests: test/og-tags.test.ts fetches 15 signed-out pages. For each it checks the tags and that the card answers 200 image/png. It also checks that a private repository's issue, a sealed issue and a sealed pull fall back to the site card even when viewed by a writer. Those cases would fail without the change. The og-designs assertion was updated because it tested the old icon behaviour, which the issue removes.
I could not reach the preview or the screenshots from my sandbox, so this verdict comes from reading the diff against the code on main.
One non-blocking nit is in the line comment below.
Every top-level page and the repository, issue, pull and person pages now carry
og:title, a non-emptyog:description, anog:imagethat answers 200image/png, andtwitter:card=summary_large_image. The newtest/og-tags.test.tsfetches 15 signed-out pages and asserts all of that. The pages are/,/explore,/signin,/signup,/audit,/apps,/contribute,/search,/family,/recover,/front,/alice,/alice/shown, an issue and a pull.Test result. I ran the new file with
test/og-route.test.ts,test/standards.test.tsandtest/og-designs.test.ts: 65 pass, 0 fail.bun x tsc --noEmitis clean. I did not run the whole suite. No unrelated test went red, so there is nothing to name from the "working now in the terminal" kind of failure.What the test found missing, and the fix. Every page except
/auditand the map page emittedtwitter:card=summarywith the 512px forge icon instead of a large card./og/site.svgand/og/site.png, a generic forge card.src/web/layout.tsnow falls back to it, so every page getssummary_large_imageat 1200×630.src/web/pages/repo.tsandsrc/web/studio/tell.tsandchange.tsnow point at them, through a small helper insrc/web/og/path.ts. A private repository or sealed issue gets the site card, because its own card is a 404 to an unfurler.Two old assertions changed. They checked the old behaviour, which the issue asks to remove.
test/standards.test.tsexpectedtwitter:cardto besummary.test/og-designs.test.tsexpected the home page to use the icon and notsummary_large_image.Left out.
/signin(/map,/new,/board,/flowand others) are not covered, since an unfurler never sees them.To try it:
bun test test/og-tags.test.ts test/og-route.test.ts.🤖 Generated with Claude Code
Closes #2446