/** * Individual School Page (SSR) * Dynamic route for school details with full SEO optimization * URL format: /school/138267-school-name-here */ import { fetchSchoolDetails, fetchSchools } from '@/lib/api'; import { notFound, redirect } from 'next/navigation'; import { SchoolDetailView } from '@/components/SchoolDetailView'; import { SecondarySchoolDetailView } from '@/components/SecondarySchoolDetailView'; import { parseSchoolSlug, schoolUrl } from '@/lib/utils'; import type { Metadata } from 'next'; /** * Enumerate every school for static generation at build time. * * Set PRERENDER_SCHOOLS=1 in the build environment to enable. When disabled * (or when the API can't be reached), we return an empty list and the route * falls back to ISR on first request — `dynamicParams = true` covers it. */ export async function generateStaticParams(): Promise> { if (process.env.PRERENDER_SCHOOLS !== '1') return []; const params: Array<{ slug: string }> = []; const PAGE_SIZE = 500; let page = 1; let totalPages = 1; try { do { const res = await fetchSchools({ page, page_size: PAGE_SIZE }); for (const s of res.schools) { const path = schoolUrl(s.urn, s.school_name); const slug = path.replace('/school/', ''); params.push({ slug }); } totalPages = res.total_pages || 1; page += 1; } while (page <= totalPages); } catch (error) { console.warn('generateStaticParams: API unreachable, falling back to on-demand ISR.', error); return []; } console.log(`generateStaticParams: prebuilding ${params.length} school pages.`); return params; } interface SchoolPageProps { params: Promise<{ slug: string }>; } export async function generateMetadata({ params }: SchoolPageProps): Promise { const { slug } = await params; const urn = parseSchoolSlug(slug); if (!urn || urn < 100000 || urn > 999999) { return { title: 'School Not Found', }; } try { const data = await fetchSchoolDetails(urn); const { school_info } = data; const canonicalPath = schoolUrl(urn, school_info.school_name); const phaseStr = (school_info.phase ?? '').toLowerCase(); const isAllThrough = phaseStr === 'all-through'; const isSecondary = !isAllThrough && ( phaseStr.includes('secondary') || (data.yearly_data ?? []).some((d: any) => d.attainment_8_score != null) ); const la = school_info.local_authority ? ` in ${school_info.local_authority}` : ''; const title = `${school_info.school_name} | ${school_info.local_authority || 'England'}`; const description = isAllThrough ? `View KS2 SATs and GCSE results for ${school_info.school_name}${la}. All-through school covering primary and secondary education.` : isSecondary ? `View GCSE results, Attainment 8, Progress 8 and school statistics for ${school_info.school_name}${la}.` : `View KS2 performance data, results, and statistics for ${school_info.school_name}${la}. Compare reading, writing, and maths results.`; return { title, description, keywords: isAllThrough ? `${school_info.school_name}, KS2 results, GCSE results, all-through school, ${school_info.local_authority}, SATs, Attainment 8` : isSecondary ? `${school_info.school_name}, GCSE results, secondary school, ${school_info.local_authority}, Attainment 8, Progress 8` : `${school_info.school_name}, KS2 results, primary school, ${school_info.local_authority}, school performance, SATs results`, openGraph: { title, description, type: 'website', url: `https://schoolcompare.co.uk${canonicalPath}`, siteName: 'SchoolCompare', }, twitter: { card: 'summary', title, description, }, alternates: { canonical: `https://schoolcompare.co.uk${canonicalPath}`, }, }; } catch { return { title: 'School Not Found', }; } } // ISR: regenerate at most once a week per slug. School data updates annually, // so a 7-day cache is plenty and gives sub-100ms TTFB on cache hits. export const revalidate = 604800; export const dynamicParams = true; export default async function SchoolPage({ params }: SchoolPageProps) { const { slug } = await params; const urn = parseSchoolSlug(slug); // Validate URN format if (!urn || urn < 100000 || urn > 999999) { notFound(); } // Fetch school data let data; try { data = await fetchSchoolDetails(urn); } catch (error) { console.error(`Failed to fetch school ${urn}:`, error); notFound(); } const { school_info, yearly_data, absence_data, ofsted, parent_view, census, admissions, sen_detail, phonics, deprivation, finance } = data; // Redirect bare URN to canonical slug URL const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', ''); if (slug !== canonicalSlug) { redirect(`/school/${canonicalSlug}`); } const phaseStr = (school_info.phase ?? '').toLowerCase(); const isAllThrough = phaseStr === 'all-through'; // All-through schools go to SchoolDetailView (renders both KS2 + KS4 sections). // SecondarySchoolDetailView is KS4-only, so all-through schools would lose SATs data. const isSecondary = !isAllThrough && ( phaseStr.includes('secondary') || yearly_data.some((d: any) => d.attainment_8_score != null) ); // Generate JSON-LD structured data for SEO const structuredData = { '@context': 'https://schema.org', '@type': 'EducationalOrganization', name: school_info.school_name, identifier: school_info.urn.toString(), ...(school_info.address && { address: { '@type': 'PostalAddress', streetAddress: school_info.address, addressLocality: school_info.local_authority || undefined, postalCode: school_info.postcode || undefined, addressCountry: 'GB', }, }), ...(school_info.latitude && school_info.longitude && { geo: { '@type': 'GeoCoordinates', latitude: school_info.latitude, longitude: school_info.longitude, }, }), ...(school_info.school_type && { additionalType: school_info.school_type, }), }; return ( <>