feat(seo): add school name to URLs, fix sticky nav, collapse compare widget
Some checks failed
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 32s
Build and Push Docker Images / Build Frontend (Next.js) (push) Failing after 57s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 31s
Build and Push Docker Images / Trigger Portainer Update (push) Has been skipped

- URLs now /school/138267-school-name instead of /school/138267
- Bare URN URLs redirect to canonical slug (backward compat)
- Remove overflow-x:hidden that broke sticky tab nav on secondary pages
- ComparisonToast starts collapsed — user must click to open

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 12:41:28 +01:00
parent e2c700fcfc
commit 784febc162
12 changed files with 63 additions and 30 deletions

View File

@@ -1,23 +1,25 @@
/**
* Individual School Page (SSR)
* Dynamic route for school details with full SEO optimization
* URL format: /school/138267-school-name-here
*/
import { fetchSchoolDetails } from '@/lib/api';
import { notFound } from 'next/navigation';
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';
interface SchoolPageProps {
params: Promise<{ urn: string }>;
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: SchoolPageProps): Promise<Metadata> {
const { urn: urnString } = await params;
const urn = parseInt(urnString);
const { slug } = await params;
const urn = parseSchoolSlug(slug);
if (isNaN(urn) || urn < 100000 || urn > 999999) {
if (!urn || urn < 100000 || urn > 999999) {
return {
title: 'School Not Found',
};
@@ -27,6 +29,7 @@ export async function generateMetadata({ params }: SchoolPageProps): Promise<Met
const data = await fetchSchoolDetails(urn);
const { school_info } = data;
const canonicalPath = schoolUrl(urn, school_info.school_name);
const isSecondary = (school_info.phase ?? '').toLowerCase().includes('secondary')
|| (data.yearly_data ?? []).some((d: any) => d.attainment_8_score != null);
const title = `${school_info.school_name} | ${school_info.local_authority || 'England'}`;
@@ -44,7 +47,7 @@ export async function generateMetadata({ params }: SchoolPageProps): Promise<Met
title,
description,
type: 'website',
url: `https://schoolcompare.co.uk/school/${urn}`,
url: `https://schoolcompare.co.uk${canonicalPath}`,
siteName: 'SchoolCompare',
},
twitter: {
@@ -53,7 +56,7 @@ export async function generateMetadata({ params }: SchoolPageProps): Promise<Met
description,
},
alternates: {
canonical: `https://schoolcompare.co.uk/school/${urn}`,
canonical: `https://schoolcompare.co.uk${canonicalPath}`,
},
};
} catch {
@@ -67,11 +70,11 @@ export async function generateMetadata({ params }: SchoolPageProps): Promise<Met
export const dynamic = 'force-dynamic';
export default async function SchoolPage({ params }: SchoolPageProps) {
const { urn: urnString } = await params;
const urn = parseInt(urnString);
const { slug } = await params;
const urn = parseSchoolSlug(slug);
// Validate URN format
if (isNaN(urn) || urn < 100000 || urn > 999999) {
if (!urn || urn < 100000 || urn > 999999) {
notFound();
}
@@ -86,6 +89,12 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
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 isSecondary = (school_info.phase ?? '').toLowerCase().includes('secondary')
|| yearly_data.some((d: any) => d.attainment_8_score != null);

View File

@@ -5,6 +5,7 @@
import { MetadataRoute } from 'next';
import { fetchSchools } from '@/lib/api';
import { schoolUrl } from '@/lib/utils';
const BASE_URL = 'https://schoolcompare.co.uk';
@@ -39,7 +40,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
});
const schoolPages: MetadataRoute.Sitemap = schoolsData.schools.map((school) => ({
url: `${BASE_URL}/school/${school.urn}`,
url: `${BASE_URL}${schoolUrl(school.urn, school.school_name)}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.6,