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

@@ -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(() => {

View File

@@ -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({
×
</button>
<h2 className={styles.schoolName}>
<a href={`/school/${school.urn}`}>{school.school_name}</a>
<a href={schoolUrl(school.urn, school.school_name)}>{school.school_name}</a>
</h2>
<div className={styles.schoolMeta}>
{school.local_authority && (

View File

@@ -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
<div className={styles.compactItem}>
<div className={styles.compactItemContent}>
<div className={styles.compactItemHeader}>
<a href={`/school/${school.urn}`} className={styles.compactItemName}>
<a href={{schoolUrl(school.urn, school.school_name)}} className={styles.compactItemName}>
{school.school_name}
</a>
{school.distance !== undefined && school.distance !== null && (
@@ -352,7 +353,7 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo
>
{isInCompare ? '✓ Comparing' : '+ Compare'}
</button>
<a href={`/school/${school.urn}`} className="btn btn-tertiary btn-sm">
<a href={{schoolUrl(school.urn, school.school_name)}} className="btn btn-tertiary btn-sm">
View
</a>
</div>

View File

@@ -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 }
<strong style="font-size: 14px; display: block; margin-bottom: 8px;">${school.school_name}</strong>
${school.local_authority ? `<div style="font-size: 12px; color: #666; margin-bottom: 4px;">${school.local_authority}</div>` : ''}
${school.school_type ? `<div style="font-size: 12px; color: #666; margin-bottom: 8px;">${school.school_type}</div>` : ''}
<a href="/school/${school.urn}" style="display: inline-block; margin-top: 8px; padding: 6px 12px; background: #e07256; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;">View Details</a>
<a href="${schoolUrl(school.urn, school.school_name)}" style="display: inline-block; margin-top: 8px; padding: 6px 12px; background: #e07256; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;">View Details</a>
</div>
`;

View File

@@ -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({
)}
</td>
<td className={styles.schoolCell}>
<a href={`/school/${ranking.urn}`} className={styles.schoolLink}>
<a href={{schoolUrl(ranking.urn, ranking.school_name)}} className={styles.schoolLink}>
{ranking.school_name}
</a>
</td>
@@ -277,7 +277,7 @@ export function RankingsView({
<strong>{displayValue}</strong>
</td>
<td className={styles.actionCell}>
<a href={`/school/${ranking.urn}`} className="btn btn-tertiary btn-sm">View</a>
<a href={{schoolUrl(ranking.urn, ranking.school_name)}} className="btn btn-tertiary btn-sm">View</a>
<button
onClick={() => handleAddToCompare(ranking)}
disabled={alreadyInComparison}

View File

@@ -5,7 +5,7 @@
import Link from 'next/link';
import type { School } from '@/lib/types';
import { formatPercentage, formatProgress, calculateTrend, getTrendColor } from '@/lib/utils';
import { formatPercentage, formatProgress, calculateTrend, getTrendColor, schoolUrl } from '@/lib/utils';
import styles from './SchoolCard.module.css';
interface SchoolCardProps {
@@ -25,7 +25,7 @@ export function SchoolCard({ school, onAddToCompare, onRemoveFromCompare, showDi
<div className={`${styles.card} ${isInCompare ? styles.cardInCompare : ''}`}>
<div className={styles.header}>
<h3 className={styles.title}>
<Link href={`/school/${school.urn}`}>
<Link href={{schoolUrl(school.urn, school.school_name)}}>
{school.school_name}
</Link>
</h3>
@@ -146,7 +146,7 @@ export function SchoolCard({ school, onAddToCompare, onRemoveFromCompare, showDi
)}
<div className={styles.actions}>
<Link href={`/school/${school.urn}`} className="btn btn-primary">
<Link href={{schoolUrl(school.urn, school.school_name)}} className="btn btn-primary">
View Details
</Link>
{onAddToCompare && (

View File

@@ -9,7 +9,7 @@
*/
import type { School } from '@/lib/types';
import { formatPercentage, formatProgress, calculateTrend } from '@/lib/utils';
import { formatPercentage, formatProgress, calculateTrend, schoolUrl } from '@/lib/utils';
import { progressBand } from '@/lib/metrics';
import styles from './SchoolRow.module.css';
@@ -61,7 +61,7 @@ export function SchoolRow({
{/* Line 1: School name + Ofsted badge */}
<div className={styles.line1}>
<a href={`/school/${school.urn}`} className={styles.schoolName}>
<a href={{schoolUrl(school.urn, school.school_name)}} className={styles.schoolName}>
{school.school_name}
</a>
{school.ofsted_grade && (
@@ -155,7 +155,7 @@ export function SchoolRow({
{/* Right: actions, vertically centred */}
<div className={styles.rowActions}>
<a href={`/school/${school.urn}`} className="btn btn-tertiary btn-sm">
<a href={{schoolUrl(school.urn, school.school_name)}} className="btn btn-tertiary btn-sm">
View
</a>
{(onAddToCompare || onRemoveFromCompare) && (

View File

@@ -2,7 +2,6 @@
.container {
width: 100%;
overflow-x: hidden;
}
/* ── Header ──────────────────────────────────────────── */

View File

@@ -11,7 +11,7 @@
'use client';
import type { School } from '@/lib/types';
import { schoolUrl } from '@/lib/utils';
import styles from './SecondarySchoolRow.module.css';
const OFSTED_LABELS: Record<number, string> = {
@@ -73,7 +73,7 @@ export function SecondarySchoolRow({
{/* Line 1: School name + Ofsted badge */}
<div className={styles.line1}>
<a href={`/school/${school.urn}`} className={styles.schoolName}>
<a href={{schoolUrl(school.urn, school.school_name)}} className={styles.schoolName}>
{school.school_name}
</a>
{school.ofsted_grade && (
@@ -155,7 +155,7 @@ export function SecondarySchoolRow({
{/* Right: actions */}
<div className={styles.rowActions}>
<a href={`/school/${school.urn}`} className="btn btn-tertiary btn-sm">
<a href={{schoolUrl(school.urn, school.school_name)}} className="btn btn-tertiary btn-sm">
View
</a>
{(onAddToCompare || onRemoveFromCompare) && (