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 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 1m21s
Someone on a place page is usually looking for a school they can name, so the order should serve scanning for it rather than ranking. /api/rankings keeps its league-table ordering; this is a place-page decision, not a site-wide one. Sorted case-insensitively, or a capitalised name would sort ahead of every lowercase one. The change made five pieces of copy untrue, so they go with it. The phase variant titled itself "— Ranked", and all four route families described themselves as "ranked by SATs and GCSE results". A page that opens by claiming an order it does not keep is worse than one that claims nothing. The ItemList markup carried `position` with no declared order, which reads as a ranking. It now declares ItemListOrderAscending, so the structured data says what the table does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
63 lines
2.1 KiB
TypeScript
63 lines
2.1 KiB
TypeScript
/**
|
|
* Postcode district pages.
|
|
*
|
|
* No phase variants: nobody searches "primary schools in SW11", so the
|
|
* variants would be pages without demand. These exist to catch
|
|
* "schools near <postcode>" and to give London districts a geographic page
|
|
* where the GIAS town field cannot.
|
|
*/
|
|
import { notFound } from 'next/navigation';
|
|
import type { Metadata } from 'next';
|
|
import { fetchPlace, fetchPlaces } from '@/lib/places';
|
|
import { fetchNationalAverages } from '@/lib/api';
|
|
import { PlaceView } from '@/components/places/PlaceView';
|
|
import { absoluteUrl } from '@/lib/site';
|
|
|
|
interface Props { params: Promise<{ outcode: string }> }
|
|
|
|
export const revalidate = 604800;
|
|
export const dynamicParams = true;
|
|
|
|
export async function generateStaticParams(): Promise<Array<{ outcode: string }>> {
|
|
// 1,760 of these; same CI budget argument as the town routes.
|
|
if (process.env.PRERENDER_PLACES !== '1') return [];
|
|
try {
|
|
return (await fetchPlaces())
|
|
.filter((p) => p.kind === 'outcode')
|
|
.map((p) => ({ outcode: p.slug }));
|
|
} catch (error) {
|
|
console.warn('generateStaticParams: API unreachable, falling back to on-demand ISR.', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const { outcode } = await params;
|
|
const detail = await fetchPlace('outcode', outcode);
|
|
if (!detail) return { title: 'Place Not Found' };
|
|
|
|
const { name, count } = detail.place;
|
|
return {
|
|
title: { absolute: `Schools near ${name} | schoolcompare` },
|
|
description:
|
|
`${count} schools in the ${name} postcode district, with results, Ofsted grades `
|
|
+ `and how close you had to live to get a place.`,
|
|
alternates: { canonical: absoluteUrl(`/schools/near/${outcode}`) },
|
|
};
|
|
}
|
|
|
|
export default async function OutcodePage({ params }: Props) {
|
|
const { outcode } = await params;
|
|
const detail = await fetchPlace('outcode', outcode);
|
|
if (!detail) notFound();
|
|
|
|
const national = await fetchNationalAverages().catch(() => null);
|
|
return (
|
|
<PlaceView
|
|
detail={detail}
|
|
englandAverage={national?.primary?.rwm_expected_pct ?? null}
|
|
neighbours={[]}
|
|
/>
|
|
);
|
|
}
|