import { metadata as homeMetadata } from '@/app/page'; import { metadata as rankingsMetadata } from '@/app/rankings/page'; import { metadata as admissionsMetadata } from '@/app/admissions/page'; import { generateMetadata as compareMetadata } from '@/app/compare/page'; describe('canonical URLs', () => { it('the homepage canonicalises to the bare root', () => { // page.tsx reads eleven search params. Without this, every filter // combination is a crawlable near-duplicate of the one page we want to // rank for "compare schools". expect(homeMetadata.alternates?.canonical) .toBe('https://www.schoolcompare.co.uk/'); }); it('rankings canonicalises to the bare path', () => { expect(rankingsMetadata.alternates?.canonical) .toBe('https://www.schoolcompare.co.uk/rankings'); }); it('admissions canonicalises to the bare path', () => { expect(admissionsMetadata.alternates?.canonical) .toBe('https://www.schoolcompare.co.uk/admissions'); }); }); describe('/compare indexability', () => { it('the bare compare page is indexable and canonical to itself', async () => { // This is the landing page for the "compare schools" head term. const meta = await compareMetadata({ searchParams: Promise.resolve({}) }); expect(meta.alternates?.canonical) .toBe('https://www.schoolcompare.co.uk/compare'); expect(meta.robots).toBeUndefined(); }); it('a comparison of specific schools is noindex, follow', async () => { // ~317 million pairs before triples. Indexing the parameter space would // swamp everything else in the corpus. const meta = await compareMetadata({ searchParams: Promise.resolve({ urns: '100001,100002' }), }); expect(meta.robots).toEqual({ index: false, follow: true }); }); it('a parameterised comparison still canonicalises to the bare path', async () => { // follow:true plus a canonical means the outbound links to each school // page still pass value even though this URL is not indexed. const meta = await compareMetadata({ searchParams: Promise.resolve({ urns: '100001,100002' }), }); expect(meta.alternates?.canonical) .toBe('https://www.schoolcompare.co.uk/compare'); }); }); /* * W8 — snippet copy for the C1 cluster. * * The baseline (GSC, 16 months to 2026-08-20) showed these pages ranking on * page one and converting at a tenth of the normal rate: "compare school * performance" at position 6.1 with 0.43% CTR, against 9.16% for the brand * query from the same neighbourhood. The SERP is dominated by the DfE's own * "Compare school performance" service, so the job of this copy is to say * what that service does not offer, without losing intent match on the title. * * These tests guard the mechanics that make a snippet work — length, intent * keyword, differentiator, no brand-first — not the exact wording, which * should stay free to iterate. */ // Google truncates titles near 60 characters and descriptions near 155. const TITLE_MAX = 60; const DESC_MIN = 110; const DESC_MAX = 155; type Meta = { title?: unknown; description?: unknown }; const titleOf = (m: Meta): string => { const t = m.title as string | { absolute?: string } | undefined; return typeof t === 'string' ? t : (t?.absolute ?? ''); }; describe('C1 snippet copy', () => { const pages: Array<[string, Meta, RegExp]> = [ ['home', homeMetadata as Meta, /compare schools/i], ['rankings', rankingsMetadata as Meta, /league table/i], ['admissions', admissionsMetadata as Meta, /admission/i], ]; for (const [name, meta, intent] of pages) { it(`${name}: title carries the search intent and fits the SERP`, () => { const t = titleOf(meta); expect(t).toMatch(intent); expect(t.length).toBeLessThanOrEqual(TITLE_MAX); }); it(`${name}: title does not open with the brand`, () => { // The measured 0.43% CTR came from a brand-first title. The most // valuable pixels go to the thing the searcher typed. expect(titleOf(meta).toLowerCase().startsWith('schoolcompare')).toBe(false); }); it(`${name}: description is long enough to be worth reading, short enough to survive`, () => { const d = meta.description as string; expect(d.length).toBeGreaterThanOrEqual(DESC_MIN); expect(d.length).toBeLessThanOrEqual(DESC_MAX); }); } it('the homepage description names what gov.uk does not publish', () => { // Admissions distance is the one fact the DfE service has no equivalent // for. If it ever leaves this description, the snippet is competing with // gov.uk on gov.uk's own ground. expect(homeMetadata.description).toMatch(/close you had to live|distance/i); }); it('/compare targets the tool phrasing rather than repeating the homepage', () => { // Two pages chasing one phrase is how a site competes with itself. return compareMetadata({ searchParams: Promise.resolve({}) }).then((m) => { expect(m.title).toMatch(/comparison tool/i); expect(m.title).not.toBe(titleOf(homeMetadata as Meta)); }); }); it('no C1 page claims a school count that will drift', () => { // The corpus moves with every data refresh; this repo has already shipped // one copy bug of that kind ("three schools" against MAX_SCHOOLS = 5). for (const [, meta] of pages) { expect(meta.description as string).not.toMatch(/\b\d{2},\d{3}\b|\b\d{2},000\b/); } }); });