fix(places): stop the place titles doubling the brand #117

Merged
tudor merged 1 commits from fix/place-title-brand-doubling into main 2026-08-21 20:53:14 +00:00
Owner

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

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
tudor added 1 commit 2026-08-21 20:43:56 +00:00
fix(places): stop the place titles doubling the brand
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 27s
24e114dee7
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

🤖 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.
## 🤖 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.
tudor merged commit 6d79bd3331 into main 2026-08-21 20:53: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#117