- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router - Add server-side rendering for all pages (Home, Compare, Rankings) - Create individual school pages with dynamic routing (/school/[urn]) - Implement Chart.js and Leaflet map integrations - Add comprehensive SEO with sitemap, robots.txt, and JSON-LD - Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js) - Update CI/CD pipeline to build both backend and frontend images - Fix Dockerfile to include devDependencies for TypeScript compilation - Add Jest testing configuration - Implement performance optimizations (code splitting, caching) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
/**
|
|
* Individual School Page (SSR)
|
|
* Dynamic route for school details with full SEO optimization
|
|
*/
|
|
|
|
import { fetchSchoolDetails } from '@/lib/api';
|
|
import { notFound } from 'next/navigation';
|
|
import { SchoolDetailView } from '@/components/SchoolDetailView';
|
|
import type { Metadata } from 'next';
|
|
|
|
interface SchoolPageProps {
|
|
params: Promise<{ urn: string }>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: SchoolPageProps): Promise<Metadata> {
|
|
const { urn: urnString } = await params;
|
|
const urn = parseInt(urnString);
|
|
|
|
if (isNaN(urn) || urn < 100000 || urn > 999999) {
|
|
return {
|
|
title: 'School Not Found',
|
|
};
|
|
}
|
|
|
|
try {
|
|
const data = await fetchSchoolDetails(urn);
|
|
const { school_info } = data;
|
|
|
|
const title = `${school_info.school_name} | ${school_info.local_authority || 'England'}`;
|
|
const description = `View KS2 performance data, results, and statistics for ${school_info.school_name}${school_info.local_authority ? ` in ${school_info.local_authority}` : ''}. Compare reading, writing, and maths results.`;
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
keywords: `${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/school/${urn}`,
|
|
siteName: 'SchoolCompare',
|
|
},
|
|
twitter: {
|
|
card: 'summary',
|
|
title,
|
|
description,
|
|
},
|
|
alternates: {
|
|
canonical: `https://schoolcompare.co.uk/school/${urn}`,
|
|
},
|
|
};
|
|
} catch {
|
|
return {
|
|
title: 'School Not Found',
|
|
};
|
|
}
|
|
}
|
|
|
|
// Force dynamic rendering
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export default async function SchoolPage({ params }: SchoolPageProps) {
|
|
const { urn: urnString } = await params;
|
|
const urn = parseInt(urnString);
|
|
|
|
// Validate URN format
|
|
if (isNaN(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 } = data;
|
|
|
|
// 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 (
|
|
<>
|
|
<script
|
|
type="application/ld+json"
|
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
|
|
/>
|
|
<SchoolDetailView
|
|
schoolInfo={school_info}
|
|
yearlyData={yearly_data}
|
|
absenceData={absence_data}
|
|
/>
|
|
</>
|
|
);
|
|
}
|