fix(seo): declare the share card, which the route group stopped inheriting #146

Merged
tudor merged 1 commits from fix/og-image-route-group into main 2026-09-14 21:03:14 +00:00
Owner

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.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq

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
tudor added 1 commit 2026-09-14 20:54:49 +00:00
fix(seo): declare the share card, which the route group stopped inheriting
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m11s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m17s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 53s
b6c2cd5116
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

🤖 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.

## 🤖 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 main 2026-09-14 21:03:14 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#146