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 ; 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
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
/**
|
|
* Phase variants of a place page.
|
|
*
|
|
* Phase is part of the query — "primary schools in beccles", "secondary
|
|
* schools in brentwood" — not a filter applied afterwards, so each gets its
|
|
* own indexable path. A place with no schools of the phase has no page: the
|
|
* per-phase threshold, not an error.
|
|
*/
|
|
import { notFound } from 'next/navigation';
|
|
import type { Metadata } from 'next';
|
|
import { fetchPlace } from '@/lib/places';
|
|
import { fetchNationalAverages } from '@/lib/api';
|
|
import { PlaceView } from '@/components/places/PlaceView';
|
|
import { absoluteUrl } from '@/lib/site';
|
|
|
|
interface Props { params: Promise<{ place: string; phase: string }> }
|
|
|
|
export const revalidate = 604800;
|
|
export const dynamicParams = true;
|
|
|
|
const PHASES = ['primary', 'secondary'] as const;
|
|
type Phase = (typeof PHASES)[number];
|
|
|
|
const isPhase = (v: string): v is Phase => (PHASES as readonly string[]).includes(v);
|
|
|
|
async function resolve(slug: string, phase: Phase) {
|
|
return (await fetchPlace('town', slug, phase))
|
|
?? (await fetchPlace('locality', slug, phase));
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { place: slug, phase } = await params;
|
|
if (!isPhase(phase)) return { title: 'Place Not Found' };
|
|
const detail = await resolve(slug, phase);
|
|
if (!detail || detail.schools.length === 0) return { title: 'Place Not Found' };
|
|
|
|
const word = phase === 'secondary' ? 'Secondary' : 'Primary';
|
|
const { name } = detail.place;
|
|
return {
|
|
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.`,
|
|
alternates: { canonical: absoluteUrl(`/schools/${slug}/${phase}`) },
|
|
};
|
|
}
|
|
|
|
export default async function PlacePhasePage({ params }: Props) {
|
|
const { place: slug, phase } = await params;
|
|
if (!isPhase(phase)) notFound();
|
|
const detail = await resolve(slug, phase);
|
|
if (!detail || detail.schools.length === 0) notFound();
|
|
|
|
const national = await fetchNationalAverages().catch(() => null);
|
|
const englandAverage = phase === 'secondary'
|
|
? national?.secondary?.attainment_8_score ?? null
|
|
: national?.primary?.rwm_expected_pct ?? null;
|
|
|
|
return <PlaceView detail={detail} phase={phase}
|
|
englandAverage={englandAverage} neighbours={[]} />;
|
|
}
|