diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 430fb5e..35b6049 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -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(); expect(xml).toMatch(/\/schools\/[a-z0-9-]+\/primary { + // 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); + } +}); diff --git a/nextjs-app/__tests__/app/placeMetadata.test.ts b/nextjs-app/__tests__/app/placeMetadata.test.ts index 67286db..7c9f27e 100644 --- a/nextjs-app/__tests__/app/placeMetadata.test.ts +++ b/nextjs-app/__tests__/app/placeMetadata.test.ts @@ -14,7 +14,7 @@ jest.mock('@/lib/places', () => ({ describe('place page metadata', () => { it('titles the page the way the place is searched', async () => { 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 () => { @@ -23,6 +23,18 @@ describe('place page metadata', () => { .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 () => { const m = await placeMeta({ params: Promise.resolve({ place: 'atlantis' }) }); expect(m.title).toMatch(/not found/i); diff --git a/nextjs-app/app/schools/[place]/[phase]/page.tsx b/nextjs-app/app/schools/[place]/[phase]/page.tsx index be97d0f..b63b80b 100644 --- a/nextjs-app/app/schools/[place]/[phase]/page.tsx +++ b/nextjs-app/app/schools/[place]/[phase]/page.tsx @@ -37,7 +37,7 @@ export async function generateMetadata({ params }: Props): Promise { const word = phase === 'secondary' ? 'Secondary' : 'Primary'; const { name } = detail.place; return { - title: `${word} Schools in ${name} — Ranked | schoolcompare`, + title: { absolute: `${word} Schools in ${name} — Ranked | schoolcompare` }, description: `Every ${phase} school in ${name} ranked by results, with Ofsted grades and ` + `the local average against England.`, diff --git a/nextjs-app/app/schools/[place]/page.tsx b/nextjs-app/app/schools/[place]/page.tsx index 8fe0111..c2007b3 100644 --- a/nextjs-app/app/schools/[place]/page.tsx +++ b/nextjs-app/app/schools/[place]/page.tsx @@ -53,7 +53,10 @@ export async function generateMetadata({ params }: Props): Promise { const { name, count } = detail.place; 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: `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.`, diff --git a/nextjs-app/app/schools/authority/[la]/page.tsx b/nextjs-app/app/schools/authority/[la]/page.tsx index 7f1aac7..f9b4c3e 100644 --- a/nextjs-app/app/schools/authority/[la]/page.tsx +++ b/nextjs-app/app/schools/authority/[la]/page.tsx @@ -43,7 +43,7 @@ export async function generateMetadata({ params }: Props): Promise { const { name, count } = detail.place; return { - title: `Schools in ${name} — Local Authority | schoolcompare`, + title: { absolute: `Schools in ${name} — Local Authority | schoolcompare` }, description: `All ${count} schools in the ${name} local authority, ranked by SATs and GCSE ` + `results, with Ofsted grades and the authority average against England.`, diff --git a/nextjs-app/app/schools/near/[outcode]/page.tsx b/nextjs-app/app/schools/near/[outcode]/page.tsx index 9a43aa2..e08d471 100644 --- a/nextjs-app/app/schools/near/[outcode]/page.tsx +++ b/nextjs-app/app/schools/near/[outcode]/page.tsx @@ -38,7 +38,7 @@ export async function generateMetadata({ params }: Props): Promise { const { name, count } = detail.place; return { - title: `Schools near ${name} | schoolcompare`, + title: { absolute: `Schools near ${name} | schoolcompare` }, description: `${count} schools in the ${name} postcode district, ranked by results, with ` + `Ofsted grades and how close you had to live to get a place.`,