The root layout sets title.template: %s | schoolcompare, which appends the brand to any plain-string title. All four place routes already carried the brand in their own title, so every one of them shipped doubled.
~2,600 titles affected, and the repetition pushed them past Google truncation point — so the duplicated brand displaced real words in the result snippet, on the pages W2 exists to rank.
W8 opted the other routes out with an absolute title for exactly this reason. The place routes were written afterwards and did not inherit the lesson.
Fix
title: { absolute: ... } on all four place route families, plus an e2e journey asserting no title repeats the brand — checked across the static routes and a place page, so a route added later cannot reintroduce it.
Verification
Backend 100 · frontend 248 · tsc --noEmit clean · next build green · 84 e2e journeys.
Note on how this got missed
Every automated check passed. The title was assembled correctly by generateMetadata and only doubled once Next applied the layout template — which unit tests on the metadata object never see. It took fetching the deployed HTML to find. The new e2e assertion closes that gap, since it reads the rendered <title>.
Found by looking at a real rendered page on staging rather than at a test.
```
Schools in Brentwood — Compare 27 Schools | schoolcompare | schoolcompare
```
The root layout sets `title.template: %s | schoolcompare`, which appends the brand to any plain-string title. All four place routes already carried the brand in their own title, so every one of them shipped doubled.
**~2,600 titles affected**, and the repetition pushed them past Google truncation point — so the duplicated brand displaced real words in the result snippet, on the pages W2 exists to rank.
W8 opted the other routes out with an absolute title for exactly this reason. The place routes were written afterwards and did not inherit the lesson.
## Fix
`title: { absolute: ... }` on all four place route families, plus an e2e journey asserting no title repeats the brand — checked across the static routes and a place page, so a route added later cannot reintroduce it.
## Verification
Backend 100 · frontend 248 · `tsc --noEmit` clean · `next build` green · 84 e2e journeys.
## Note on how this got missed
Every automated check passed. The title was assembled correctly by `generateMetadata` and only doubled once Next applied the layout template — which unit tests on the metadata object never see. It took fetching the deployed HTML to find. The new e2e assertion closes that gap, since it reads the rendered `<title>`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Every place page shipped as 'Schools in Brentwood - Compare 27 Schools |
schoolcompare | schoolcompare'. The root layout's title template appends
'| schoolcompare' to any plain-string title, and all four place routes already
carried the brand. W8 opted the other routes out with an absolute title; the
place routes were written afterwards and did not inherit the lesson.
~2,600 titles affected, and the repetition pushed them past Google's
truncation point, so the doubled brand displaced real words in the result.
An e2e journey now asserts no title repeats the brand, across the static
routes and a place page, so this cannot come back on a route added later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This PR fixes a title-doubling bug where place-page titles already containing '| schoolcompare' were being run through the root layout's title template, producing '... | schoolcompare | schoolcompare'. It correctly switches the four affected page.tsx files to use the Next.js title: { absolute: ... } metadata form to opt out of the template, and adds unit/e2e regression tests. The change is small, well-contained, and low risk.
🟡 Minor
e2e/tests/journeys.spec.ts: The new e2e test only exercises '/', '/rankings', '/admissions', and the place index route '/schools/{slug}'. It does not cover the other three routes touched by this PR ('/schools/{place}/{phase}', '/schools/authority/{la}', '/schools/near/{outcode}'), so a future regression in those specific templates would not be caught by this test.
e2e/tests/journeys.spec.ts: places.find((p) => p.kind === 'town') has no fallback; if the fixture/API response ever contains no 'town' kind, town is undefined and town.slug throws a less-informative TypeError instead of a clear assertion failure.
## 🤖 AI Code Review (Claude Code)
This PR fixes a title-doubling bug where place-page titles already containing '| schoolcompare' were being run through the root layout's title template, producing '... | schoolcompare | schoolcompare'. It correctly switches the four affected page.tsx files to use the Next.js `title: { absolute: ... }` metadata form to opt out of the template, and adds unit/e2e regression tests. The change is small, well-contained, and low risk.
### 🟡 Minor
- **e2e/tests/journeys.spec.ts**: The new e2e test only exercises '/', '/rankings', '/admissions', and the place index route '/schools/{slug}'. It does not cover the other three routes touched by this PR ('/schools/{place}/{phase}', '/schools/authority/{la}', '/schools/near/{outcode}'), so a future regression in those specific templates would not be caught by this test.
- **e2e/tests/journeys.spec.ts**: `places.find((p) => p.kind === 'town')` has no fallback; if the fixture/API response ever contains no 'town' kind, `town` is undefined and `town.slug` throws a less-informative TypeError instead of a clear assertion failure.
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.
Found by looking at a real rendered page on staging rather than at a test.
The root layout sets
title.template: %s | schoolcompare, which appends the brand to any plain-string title. All four place routes already carried the brand in their own title, so every one of them shipped doubled.~2,600 titles affected, and the repetition pushed them past Google truncation point — so the duplicated brand displaced real words in the result snippet, on the pages W2 exists to rank.
W8 opted the other routes out with an absolute title for exactly this reason. The place routes were written afterwards and did not inherit the lesson.
Fix
title: { absolute: ... }on all four place route families, plus an e2e journey asserting no title repeats the brand — checked across the static routes and a place page, so a route added later cannot reintroduce it.Verification
Backend 100 · frontend 248 ·
tsc --noEmitclean ·next buildgreen · 84 e2e journeys.Note on how this got missed
Every automated check passed. The title was assembled correctly by
generateMetadataand only doubled once Next applied the layout template — which unit tests on the metadata object never see. It took fetching the deployed HTML to find. The new e2e assertion closes that gap, since it reads the rendered<title>.🤖 Generated with Claude Code
https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
🤖 AI Code Review (Claude Code)
This PR fixes a title-doubling bug where place-page titles already containing '| schoolcompare' were being run through the root layout's title template, producing '... | schoolcompare | schoolcompare'. It correctly switches the four affected page.tsx files to use the Next.js
title: { absolute: ... }metadata form to opt out of the template, and adds unit/e2e regression tests. The change is small, well-contained, and low risk.🟡 Minor
places.find((p) => p.kind === 'town')has no fallback; if the fixture/API response ever contains no 'town' kind,townis undefined andtown.slugthrows a less-informative TypeError instead of a clear assertion failure.