The staging E2E gate's one remaining failure. Every link to the site pasted into a chat has been rendering bare.
Root cause
Staging serves og:title, og:description, og:url, og:site_name, og:type and twitter:card — and no og:image at all. So metadata from the layout reaches the page; only the file convention does not.
app/opengraph-image.tsx is not broken: _not-found, which lives in the app root segment, carries an og:image from it in the build output. It just doesn't reach the site's pages, which live in the (frontend) route group whose own layout.tsx is the root layout.
The icon conventions are unaffected — /icon.png and /apple-icon.png are both linked correctly on that same page, verified against staging. That asymmetry is the whole bug, and it arrived with the route-group split Payload required.
The fix
The file stays at the app root. Moving metadata files into a route group is what drops /robots.txt and hashes /icon.png — CLAUDE.md records this, and this PR must not undo it. The build still emits all four of /robots.txt, /icon.png, /apple-icon.png and /opengraph-image.
The root layout points at the generated route instead, and metadataBase makes it absolute — which the journey needs, since it calls new URL() on the value.
twitter.images is set for the same reason: the card is declared summary_large_image, and claiming a large-image card while supplying no image is worse than claiming a summary card.
This does not just move the failure
The journey aborts at line 911, so its apple-touch-icon and maskable-icon assertions had never run — I couldn't assume they passed. Checked each against staging first:
That run's other 117 journeys passed, including the new a school page links back into the location layer one. This same test was already failing on run 1090, before any of this session's work.
Verification
tsc --noEmit clean.
429 Jest tests across 51 suites pass, including three new ones pinning the card, the summary_large_image pairing, and metadataBase.
next build succeeds with DATABASE_URL unset and still emits all four metadata routes.
The honest limit: this can only be proved by the staging journey after merge, since that gate runs post-merge. What I can show now is that the tag is declared, the route it points at serves a PNG, and nothing else in that journey is broken.
The staging E2E gate's one remaining failure. Every link to the site pasted into a chat has been rendering bare.
## Root cause
Staging serves `og:title`, `og:description`, `og:url`, `og:site_name`, `og:type` and `twitter:card` — and **no `og:image` at all**. So metadata from the layout reaches the page; only the file convention does not.
`app/opengraph-image.tsx` is not broken: `_not-found`, which lives in the app **root segment**, carries an `og:image` from it in the build output. It just doesn't reach the site's pages, which live in the `(frontend)` route group whose own `layout.tsx` is the root layout.
The icon conventions are unaffected — `/icon.png` and `/apple-icon.png` are both linked correctly on that same page, verified against staging. That asymmetry is the whole bug, and it arrived with the route-group split Payload required.
## The fix
The file stays at the app root. Moving metadata files into a route group is what drops `/robots.txt` and hashes `/icon.png` — CLAUDE.md records this, and this PR must not undo it. The build still emits all four of `/robots.txt`, `/icon.png`, `/apple-icon.png` and `/opengraph-image`.
The root layout points at the generated route instead, and `metadataBase` makes it absolute — which the journey needs, since it calls `new URL()` on the value.
`twitter.images` is set for the same reason: the card is declared `summary_large_image`, and claiming a large-image card while supplying no image is worse than claiming a summary card.
## This does not just move the failure
The journey aborts at line 911, so its `apple-touch-icon` and maskable-icon assertions had **never run** — I couldn't assume they passed. Checked each against staging first:
| Asset | Result |
|---|---|
| `/apple-icon.png?…` | 200 `image/png` |
| `/icon-192.png` | 200 `image/png` |
| `/icon-512.png` | 200 `image/png` |
| `/icon-maskable-512.png` | 200 `image/png` |
| `/opengraph-image` | 200 `image/png`, 63KB |
`og:image` was the only broken assertion.
## Not a regression from #145
That run's other 117 journeys passed, including the new `a school page links back into the location layer` one. This same test was already failing on run 1090, before any of this session's work.
## Verification
- `tsc --noEmit` clean.
- 429 Jest tests across 51 suites pass, including three new ones pinning the card, the `summary_large_image` pairing, and `metadataBase`.
- `next build` succeeds with `DATABASE_URL` unset and still emits all four metadata routes.
The honest limit: this can only be *proved* by the staging journey after merge, since that gate runs post-merge. What I can show now is that the tag is declared, the route it points at serves a PNG, and nothing else in that journey is broken.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
The staging E2E gate's one failure. Every link to the site pasted into
a chat has been rendering bare.
Staging serves og:title, og:description, og:url, og:site_name, og:type
and twitter:card, and no og:image at all. So metadata from the layout
reaches the page; only the file convention does not.
app/opengraph-image.tsx does work — _not-found, which lives in the app
root segment, carries an og:image from it in the build output. It does
not reach the site's pages, which live in the (frontend) route group
whose own layout.tsx is the root layout. The icon conventions are not
affected: /icon.png and /apple-icon.png are both linked correctly on
the same page, verified against staging. The asymmetry is the whole
bug, and it arrived with the route-group split that Payload required.
The file stays at the app root. Moving metadata files into a route
group is what drops /robots.txt and hashes /icon.png, which CLAUDE.md
records and which this must not undo — the build still emits all four
of /robots.txt, /icon.png, /apple-icon.png and /opengraph-image. The
root layout points at the route instead, and metadataBase makes it
absolute, which the journey needs since it calls new URL() on the value.
twitter.images is set for the same reason: the card is declared
summary_large_image, and claiming a large-image card while supplying no
image is worse than claiming a summary card.
Checked before fixing that og:image was the only broken assertion in
that journey: the test aborts at line 911, so its apple-touch-icon and
maskable-icon assertions had never run. All four of those assets return
200 image/png from staging, so this does not simply move the failure
further down the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
This PR fixes a real bug where og:image/twitter:image metadata wasn't inherited by pages in the (frontend) route group from the root-level opengraph-image.tsx convention, so it explicitly declares images pointing at '/opengraph-image' in both the openGraph and twitter metadata blocks, and adds tests asserting the image is declared, the twitter card type is correct, and metadataBase resolves to an absolute URL. Verified against the actual repo state (opengraph-image.tsx, icon.png, apple-icon.png all exist, SITE_URL has no trailing slash matching the test's normalized expectation) and the change is syntactically correct and well-justified via comments.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
This PR fixes a real bug where og:image/twitter:image metadata wasn't inherited by pages in the (frontend) route group from the root-level opengraph-image.tsx convention, so it explicitly declares images pointing at '/opengraph-image' in both the openGraph and twitter metadata blocks, and adds tests asserting the image is declared, the twitter card type is correct, and metadataBase resolves to an absolute URL. Verified against the actual repo state (opengraph-image.tsx, icon.png, apple-icon.png all exist, SITE_URL has no trailing slash matching the test's normalized expectation) and the change is syntactically correct and well-justified via comments.
✅ No issues found.
tudor
merged commit be780ebe13 into main2026-09-14 21:03:14 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The staging E2E gate's one remaining failure. Every link to the site pasted into a chat has been rendering bare.
Root cause
Staging serves
og:title,og:description,og:url,og:site_name,og:typeandtwitter:card— and noog:imageat all. So metadata from the layout reaches the page; only the file convention does not.app/opengraph-image.tsxis not broken:_not-found, which lives in the app root segment, carries anog:imagefrom it in the build output. It just doesn't reach the site's pages, which live in the(frontend)route group whose ownlayout.tsxis the root layout.The icon conventions are unaffected —
/icon.pngand/apple-icon.pngare both linked correctly on that same page, verified against staging. That asymmetry is the whole bug, and it arrived with the route-group split Payload required.The fix
The file stays at the app root. Moving metadata files into a route group is what drops
/robots.txtand hashes/icon.png— CLAUDE.md records this, and this PR must not undo it. The build still emits all four of/robots.txt,/icon.png,/apple-icon.pngand/opengraph-image.The root layout points at the generated route instead, and
metadataBasemakes it absolute — which the journey needs, since it callsnew URL()on the value.twitter.imagesis set for the same reason: the card is declaredsummary_large_image, and claiming a large-image card while supplying no image is worse than claiming a summary card.This does not just move the failure
The journey aborts at line 911, so its
apple-touch-iconand maskable-icon assertions had never run — I couldn't assume they passed. Checked each against staging first:/apple-icon.png?…image/png/icon-192.pngimage/png/icon-512.pngimage/png/icon-maskable-512.pngimage/png/opengraph-imageimage/png, 63KBog:imagewas the only broken assertion.Not a regression from #145
That run's other 117 journeys passed, including the new
a school page links back into the location layerone. This same test was already failing on run 1090, before any of this session's work.Verification
tsc --noEmitclean.summary_large_imagepairing, andmetadataBase.next buildsucceeds withDATABASE_URLunset and still emits all four metadata routes.The honest limit: this can only be proved by the staging journey after merge, since that gate runs post-merge. What I can show now is that the tag is declared, the route it points at serves a PNG, and nothing else in that journey is broken.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
🤖 AI Code Review (Claude Code)
This PR fixes a real bug where og:image/twitter:image metadata wasn't inherited by pages in the (frontend) route group from the root-level opengraph-image.tsx convention, so it explicitly declares images pointing at '/opengraph-image' in both the openGraph and twitter metadata blocks, and adds tests asserting the image is declared, the twitter card type is correct, and metadataBase resolves to an absolute URL. Verified against the actual repo state (opengraph-image.tsx, icon.png, apple-icon.png all exist, SITE_URL has no trailing slash matching the test's normalized expectation) and the change is syntactically correct and well-justified via comments.
✅ No issues found.