PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m14s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 19s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m19s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m20s
Rewrites every visitor-facing string that used an em dash, choosing the punctuation for what the dash was doing: a colon before a list or explanation, a comma for an aside, a full stop between two thoughts, parentheses for an aside mid-sentence. Covers page titles and meta descriptions, the home and admissions guide copy, school page headings and notes, the compare page, metric labels and tooltips. Two rewrites also fix the sentence around them: the closure banner no longer repeats "proposed for closure", and the cut-off caveat's list of priorities now parses. A lone dash marking a missing value in a table cell stays: it is a data convention, not prose. A Jest guard walks the source with the TypeScript parser and fails on any other em dash in a string or JSX text node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
/**
|
|
* Compare Page (SSR)
|
|
* Side-by-side comparison of schools with metrics
|
|
*/
|
|
|
|
import { fetchComparison, fetchMetrics } from '@/lib/api';
|
|
import { ComparisonView } from '@/components/ComparisonView';
|
|
import { absoluteUrl } from '@/lib/site';
|
|
import type { Metadata } from 'next';
|
|
|
|
interface ComparePageProps {
|
|
searchParams: Promise<{
|
|
urns?: string;
|
|
metric?: string;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Indexability depends on the query string, so this cannot be a static export.
|
|
*
|
|
* Bare /compare is the landing page for the "compare schools" head term and
|
|
* stays indexable. /compare?urns=… is an unbounded parameter space — 25,193
|
|
* schools make ~317 million pairs — so it goes noindex. It stays `follow` and
|
|
* keeps a canonical to the bare path, so the links out to each school page
|
|
* still count.
|
|
*/
|
|
export async function generateMetadata(
|
|
{ searchParams }: ComparePageProps,
|
|
): Promise<Metadata> {
|
|
const { urns } = await searchParams;
|
|
|
|
const base: Metadata = {
|
|
// Deliberately not the homepage's phrase. Two pages chasing "compare
|
|
// schools" is how a site competes with itself; this one takes the tool
|
|
// phrasing instead.
|
|
title: 'School Comparison Tool: Up to Five Schools at Once | schoolcompare',
|
|
description:
|
|
'Put up to five English schools in one table: SATs and GCSE results against the England average, Ofsted grades, and the distance places were offered.',
|
|
keywords:
|
|
'school comparison, compare schools, Ofsted comparison, school admissions, KS2 comparison, primary school performance',
|
|
alternates: { canonical: absoluteUrl('/compare') },
|
|
};
|
|
|
|
if (!urns) return base;
|
|
|
|
return { ...base, robots: { index: false, follow: true } };
|
|
}
|
|
|
|
// Dynamic via searchParams; remove force-dynamic so internal data fetches
|
|
// can still use Next.js's per-call revalidate cache.
|
|
|
|
export default async function ComparePage({ searchParams }: ComparePageProps) {
|
|
const { urns: urnsParam, metric: metricParam } = await searchParams;
|
|
|
|
const urns = urnsParam?.split(',').map(Number).filter(Boolean) || [];
|
|
const selectedMetric = metricParam || 'rwm_expected_pct';
|
|
|
|
try {
|
|
// Fetch comparison + metrics in parallel — they are independent.
|
|
const [comparisonResponse, metricsResponse] = await Promise.all([
|
|
urns.length > 0
|
|
? fetchComparison(urnsParam!).catch((error) => {
|
|
console.error('Failed to fetch comparison:', error);
|
|
return null;
|
|
})
|
|
: Promise.resolve(null),
|
|
fetchMetrics(),
|
|
]);
|
|
|
|
const metricsArray = metricsResponse?.metrics || [];
|
|
|
|
return (
|
|
<ComparisonView
|
|
initialData={comparisonResponse?.comparison ?? null}
|
|
initialNationalAverages={comparisonResponse?.national_averages}
|
|
initialBenchmarks={comparisonResponse?.benchmarks}
|
|
initialUrns={urns}
|
|
metrics={metricsArray}
|
|
selectedMetric={selectedMetric}
|
|
/>
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching data for compare page:', error);
|
|
|
|
// Return error state with empty metrics
|
|
return (
|
|
<ComparisonView
|
|
initialData={null}
|
|
initialUrns={urns}
|
|
metrics={[]}
|
|
selectedMetric={selectedMetric}
|
|
/>
|
|
);
|
|
}
|
|
}
|