Files
school_compare/nextjs-app/app/school/[urn]/page.tsx
Tudor ff7f5487e6
Some checks failed
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m26s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 1m48s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped
Complete Next.js migration with SSR and Docker deployment
- 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>
2026-02-02 20:34:35 +00:00

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}
/>
</>
);
}