Compare commits

...
Author SHA1 Message Date
TudorandClaude Opus 5 12cbbda3c8 fix(places): stop the place titles doubling the brand
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
2026-08-21 21:43:12 +01:00
6 changed files with 37 additions and 5 deletions

No files matched your search

+17
View File
@@ -1868,3 +1868,20 @@ test('phase variants are submitted in the places sitemap', async ({ page }) => {
const xml = await (await page.request.get('/sitemaps/places-1.xml')).text(); const xml = await (await page.request.get('/sitemaps/places-1.xml')).text();
expect(xml).toMatch(/\/schools\/[a-z0-9-]+\/primary</); expect(xml).toMatch(/\/schools\/[a-z0-9-]+\/primary</);
}); });
test('no page title repeats the brand', async ({ page }) => {
// The root layout appends '| schoolcompare' to a plain-string title. Any
// route whose title already carries the brand must opt out with
// `absolute`, or it ships '... | schoolcompare | schoolcompare' — which is
// how ~2,600 place pages first went out.
const res = await page.request.get('/api/places');
const { places } = await res.json();
const town = places.find((p: { kind: string }) => p.kind === 'town');
for (const path of ['/', '/rankings', '/admissions', `/schools/${town.slug}`]) {
await page.goto(path);
const title = await page.title();
const brands = (title.match(/schoolcompare/gi) ?? []).length;
expect(brands, `${path} repeats the brand: ${title}`).toBeLessThanOrEqual(1);
}
});
+13 -1
View File
@@ -14,7 +14,7 @@ jest.mock('@/lib/places', () => ({
describe('place page metadata', () => { describe('place page metadata', () => {
it('titles the page the way the place is searched', async () => { it('titles the page the way the place is searched', async () => {
const m = await placeMeta({ params: Promise.resolve({ place: 'brentwood' }) }); const m = await placeMeta({ params: Promise.resolve({ place: 'brentwood' }) });
expect(m.title).toMatch(/schools in brentwood/i); expect((m.title as { absolute: string }).absolute).toMatch(/schools in brentwood/i);
}); });
it('canonicalises to its own path on the www host', async () => { it('canonicalises to its own path on the www host', async () => {
@@ -23,6 +23,18 @@ describe('place page metadata', () => {
.toBe('https://www.schoolcompare.co.uk/schools/brentwood'); .toBe('https://www.schoolcompare.co.uk/schools/brentwood');
}); });
it('opts out of the layout template, which would double the brand', () => {
// The root layout appends '| schoolcompare' to a plain string title, and
// these titles already carry it — every place page shipped reading
// '... | schoolcompare | schoolcompare' until this was made absolute.
return placeMeta({ params: Promise.resolve({ place: 'brentwood' }) })
.then((m) => {
expect(typeof m.title).toBe('object');
expect((m.title as { absolute: string }).absolute)
.not.toMatch(/schoolcompare.*schoolcompare/);
});
});
it('an unknown place gets a not-found title rather than inventing one', async () => { it('an unknown place gets a not-found title rather than inventing one', async () => {
const m = await placeMeta({ params: Promise.resolve({ place: 'atlantis' }) }); const m = await placeMeta({ params: Promise.resolve({ place: 'atlantis' }) });
expect(m.title).toMatch(/not found/i); expect(m.title).toMatch(/not found/i);
@@ -37,7 +37,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const word = phase === 'secondary' ? 'Secondary' : 'Primary'; const word = phase === 'secondary' ? 'Secondary' : 'Primary';
const { name } = detail.place; const { name } = detail.place;
return { return {
title: `${word} Schools in ${name} — Ranked | schoolcompare`, title: { absolute: `${word} Schools in ${name} — Ranked | schoolcompare` },
description: description:
`Every ${phase} school in ${name} ranked by results, with Ofsted grades and ` `Every ${phase} school in ${name} ranked by results, with Ofsted grades and `
+ `the local average against England.`, + `the local average against England.`,
+4 -1
View File
@@ -53,7 +53,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { name, count } = detail.place; const { name, count } = detail.place;
return { return {
title: `Schools in ${name} — Compare ${count} Schools | schoolcompare`, // absolute: the root layout's template appends '| schoolcompare' to a
// plain string, and this title already carries it. Without this every
// place title read '... | schoolcompare | schoolcompare'.
title: { absolute: `Schools in ${name} — Compare ${count} Schools | schoolcompare` },
description: description:
`Every school in ${name} ranked by SATs and GCSE results, with Ofsted grades, ` `Every school in ${name} ranked by SATs and GCSE results, with Ofsted grades, `
+ `the local average against England, and how close you had to live to get a place.`, + `the local average against England, and how close you had to live to get a place.`,
@@ -43,7 +43,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { name, count } = detail.place; const { name, count } = detail.place;
return { return {
title: `Schools in ${name} — Local Authority | schoolcompare`, title: { absolute: `Schools in ${name} — Local Authority | schoolcompare` },
description: description:
`All ${count} schools in the ${name} local authority, ranked by SATs and GCSE ` `All ${count} schools in the ${name} local authority, ranked by SATs and GCSE `
+ `results, with Ofsted grades and the authority average against England.`, + `results, with Ofsted grades and the authority average against England.`,
@@ -38,7 +38,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { name, count } = detail.place; const { name, count } = detail.place;
return { return {
title: `Schools near ${name} | schoolcompare`, title: { absolute: `Schools near ${name} | schoolcompare` },
description: description:
`${count} schools in the ${name} postcode district, ranked by results, with ` `${count} schools in the ${name} postcode district, ranked by results, with `
+ `Ofsted grades and how close you had to live to get a place.`, + `Ofsted grades and how close you had to live to get a place.`,