From 784febc16212d52a86bdad1b659f69ed823c7939 Mon Sep 17 00:00:00 2001 From: Tudor Date: Sun, 29 Mar 2026 12:41:28 +0100 Subject: [PATCH] feat(seo): add school name to URLs, fix sticky nav, collapse compare widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../app/school/{[urn] => [slug]}/page.tsx | 29 ++++++++++++------- nextjs-app/app/sitemap.ts | 3 +- nextjs-app/components/ComparisonToast.tsx | 2 +- nextjs-app/components/ComparisonView.tsx | 4 +-- nextjs-app/components/HomeView.tsx | 5 ++-- nextjs-app/components/LeafletMapInner.tsx | 3 +- nextjs-app/components/RankingsView.tsx | 6 ++-- nextjs-app/components/SchoolCard.tsx | 6 ++-- nextjs-app/components/SchoolRow.tsx | 6 ++-- .../SecondarySchoolDetailView.module.css | 1 - nextjs-app/components/SecondarySchoolRow.tsx | 6 ++-- nextjs-app/lib/utils.ts | 22 ++++++++++++++ 12 files changed, 63 insertions(+), 30 deletions(-) rename nextjs-app/app/school/{[urn] => [slug]}/page.tsx (84%) diff --git a/nextjs-app/app/school/[urn]/page.tsx b/nextjs-app/app/school/[slug]/page.tsx similarity index 84% rename from nextjs-app/app/school/[urn]/page.tsx rename to nextjs-app/app/school/[slug]/page.tsx index 04df878..cefe026 100644 --- a/nextjs-app/app/school/[urn]/page.tsx +++ b/nextjs-app/app/school/[slug]/page.tsx @@ -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 { - 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 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 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); diff --git a/nextjs-app/app/sitemap.ts b/nextjs-app/app/sitemap.ts index 1779fde..eeb4f8b 100644 --- a/nextjs-app/app/sitemap.ts +++ b/nextjs-app/app/sitemap.ts @@ -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 { }); 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, diff --git a/nextjs-app/components/ComparisonToast.tsx b/nextjs-app/components/ComparisonToast.tsx index 05c79ed..176ec64 100644 --- a/nextjs-app/components/ComparisonToast.tsx +++ b/nextjs-app/components/ComparisonToast.tsx @@ -9,7 +9,7 @@ import styles from './ComparisonToast.module.css'; export function ComparisonToast() { const { selectedSchools, clearAll, removeSchool } = useComparison(); const [mounted, setMounted] = useState(false); - const [collapsed, setCollapsed] = useState(false); + const [collapsed, setCollapsed] = useState(true); const pathname = usePathname(); useEffect(() => { diff --git a/nextjs-app/components/ComparisonView.tsx b/nextjs-app/components/ComparisonView.tsx index 499d85f..0787016 100644 --- a/nextjs-app/components/ComparisonView.tsx +++ b/nextjs-app/components/ComparisonView.tsx @@ -13,7 +13,7 @@ import { SchoolSearchModal } from './SchoolSearchModal'; import { EmptyState } from './EmptyState'; import { LoadingSkeleton } from './LoadingSkeleton'; import type { ComparisonData, MetricDefinition, School } from '@/lib/types'; -import { formatPercentage, formatProgress, CHART_COLORS } from '@/lib/utils'; +import { formatPercentage, formatProgress, CHART_COLORS, schoolUrl } from '@/lib/utils'; import { fetchComparison } from '@/lib/api'; import styles from './ComparisonView.module.css'; @@ -305,7 +305,7 @@ export function ComparisonView({ ×

- {school.school_name} + {school.school_name}

{school.local_authority && ( diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index 3efeeb3..8d0793b 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -15,6 +15,7 @@ import { EmptyState } from './EmptyState'; import { useComparisonContext } from '@/context/ComparisonContext'; import { fetchSchools, fetchLAaverages } from '@/lib/api'; import type { SchoolsResponse, Filters, School } from '@/lib/types'; +import { schoolUrl } from '@/lib/utils'; import styles from './HomeView.module.css'; interface HomeViewProps { @@ -316,7 +317,7 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo
- + {school.school_name} {school.distance !== undefined && school.distance !== null && ( @@ -352,7 +353,7 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo > {isInCompare ? '✓ Comparing' : '+ Compare'} - + View
diff --git a/nextjs-app/components/LeafletMapInner.tsx b/nextjs-app/components/LeafletMapInner.tsx index 1d96c58..b71da3f 100644 --- a/nextjs-app/components/LeafletMapInner.tsx +++ b/nextjs-app/components/LeafletMapInner.tsx @@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import type { School } from '@/lib/types'; +import { schoolUrl } from '@/lib/utils'; // Fix for default marker icons in Next.js delete (L.Icon.Default.prototype as any)._getIconUrl; @@ -60,7 +61,7 @@ export default function LeafletMapInner({ schools, center, zoom, onMarkerClick } ${school.school_name} ${school.local_authority ? `
${school.local_authority}
` : ''} ${school.school_type ? `
${school.school_type}
` : ''} - View Details + View Details
`; diff --git a/nextjs-app/components/RankingsView.tsx b/nextjs-app/components/RankingsView.tsx index aed7b9e..14ee612 100644 --- a/nextjs-app/components/RankingsView.tsx +++ b/nextjs-app/components/RankingsView.tsx @@ -8,7 +8,7 @@ import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; import type { RankingEntry, Filters, MetricDefinition } from '@/lib/types'; -import { formatPercentage, formatProgress } from '@/lib/utils'; +import { formatPercentage, formatProgress, schoolUrl } from '@/lib/utils'; import { EmptyState } from './EmptyState'; import styles from './RankingsView.module.css'; @@ -267,7 +267,7 @@ export function RankingsView({ )} - + {ranking.school_name} @@ -277,7 +277,7 @@ export function RankingsView({ {displayValue} - View + View