- {/* Standalone back link, above the header — returns wherever the user
- came from. Scrolls away; the sticky bar keeps a "back to top" control. */}
-
- ← Back
-
-
- {/* ── Header — the location map band blends into the school title ── */}
-
-
- {/* ── Sticky section navigation ─────────────────────── */}
-
-
-
-
- {/* ── Ofsted ─────────────────────────────────────── */}
- {ofsted && (
-
-
- {isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'}
- {ofstedInspectedDate && (
-
- {' '}Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
-
- )}
-
- Ofsted reports ↗
-
-
- {isReportCard ? (
- <>
-
- From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
-
-
- {ofsted.rc_safeguarding_met != null && (
-
-
Safeguarding
-
- {ofsted.rc_safeguarding_met ? 'Met' : 'Not met'}
-
-
- )}
- {RC_CATEGORIES.filter(({ key }) => key !== 'rc_early_years' || ofsted[key] != null).map(({ key, label }) => {
- const value = ofsted[key] as number | null;
- return value != null ? (
-
-
{label}
-
- {RC_LABELS[value]}
-
-
- ) : null;
- })}
-
- >
- ) : ofsted.overall_effectiveness ? (
- <>
-
-
- {OFSTED_LABELS[ofsted.overall_effectiveness]}
-
- {ofsted.previous_overall != null &&
- ofsted.previous_overall !== ofsted.overall_effectiveness && (
-
- Previously: {OFSTED_LABELS[ofsted.previous_overall]}
-
- )}
-
-
- {ofsted.grade_source === 'ungraded_carried_forward'
- ? 'This overall grade is carried forward from an earlier inspection — Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.'
- : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.'}
-
- {oeifAllSameGrade ? (
-
- Rated {OFSTED_LABELS[ofsted.overall_effectiveness]} across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
-
- ) : (
-
- {oeifAreas.map(({ label, value }) => (
-
-
{label}
-
- {OFSTED_LABELS[value]}
-
-
- ))}
-
- )}
- >
- ) : (
- <>
-
- From September 2024, Ofsted no longer gives a single overall grade.
-
-
- {[
- { label: 'Quality of Education', value: ofsted.quality_of_education },
- { label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes },
- { label: 'Personal Development', value: ofsted.personal_development },
- { label: 'Leadership & Management', value: ofsted.leadership_management },
- ].filter(({ value }) => value != null).map(({ label, value }) => (
-
-
{label}
-
- {OFSTED_LABELS[value!]}
-
-
- ))}
-
- >
- )}
-
- )}
-
- {/* ── GCSE Results ───────────────────────────────── */}
- {hasResults && latestResults && (
-
-
- GCSE Results ({formatAcademicYear(latestResults.year)})
-
-
- GCSE results for Year 11 pupils.{!suppressComparison && ' England averages shown for comparison.'}
-
-
-
-
- {p8Suspended && (
-
- Progress 8 isn't published for 2024/25: this GCSE year group sat no KS2 tests
- (COVID), so DfE has no starting point to measure their progress from.
-
- )}
-
- {/* Hero stat cards — top GCSE metrics */}
-
- {latestResults.attainment_8_score != null && (
-
-
- Attainment 8 score
-
-
-
- {latestResults.attainment_8_score.toFixed(1)}
- {!suppressComparison && secondaryAvg.attainment_8_score != null && (
-
- )}
-
- {!suppressComparison && secondaryAvg.attainment_8_score != null && (
-
England avg: {secondaryAvg.attainment_8_score.toFixed(1)}
- )}
-
- )}
- {latestResults.progress_8_score != null && (
-
-
- Progress 8 score
-
-
-
- {formatProgress(latestResults.progress_8_score)}
-
- {(latestResults.progress_8_lower_ci != null && latestResults.progress_8_upper_ci != null) ? (
-
- CI: {latestResults.progress_8_lower_ci.toFixed(2)} to {latestResults.progress_8_upper_ci.toFixed(2)}
-
- ) : (
-
National baseline: 0.0
- )}
-
- )}
- {latestResults.english_maths_strong_pass_pct != null && (
-
-
- English & Maths Grade 5+
-
-
-
- {formatPercentage(latestResults.english_maths_strong_pass_pct)}
- {!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
-
- )}
-
- {!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
-
England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%
- )}
-
- )}
- {latestResults.english_maths_standard_pass_pct != null && (
-
-
- English & Maths Grade 4+
-
-
-
- {formatPercentage(latestResults.english_maths_standard_pass_pct)}
- {!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
-
- )}
-
- {!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
-
England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%
- )}
-
- )}
-
-
- {/* Attainment 8 visual bar (0–80 scale). This viz is explicitly
- "school vs national", so it's dropped for special schools where
- that comparison isn't meaningful. */}
- {!suppressComparison && latestResults.attainment_8_score != null && (
-
-
Attainment 8 — school vs national
-
-
- {secondaryAvg.attainment_8_score != null && (
-
-
- Nat avg {secondaryAvg.attainment_8_score.toFixed(1)}
-
-
- )}
-
-
- 0 20 40 60 80
-
-
- )}
-
- {/* Progress 8 number line with CI */}
- {latestResults.progress_8_score != null && !p8Suspended && (
-
-
Progress 8 — relative to national baseline (0)
- {(() => {
- const p8 = latestResults.progress_8_score!;
- const lo = latestResults.progress_8_lower_ci ?? p8;
- const hi = latestResults.progress_8_upper_ci ?? p8;
- const range = 6; // −3 to +3
- const toX = (v: number) => `${Math.min(Math.max(((v + 3) / range) * 100, 0), 100)}%`;
- return (
-
- {/* CI band */}
-
- {/* Zero line */}
-
- {/* Score dot */}
-
-
- );
- })()}
-
- −3 −2 −1 0 +1 +2 +3
-
-
- )}
-
- {/* Progress 8 component breakdown */}
- {(latestResults.progress_8_english != null || latestResults.progress_8_maths != null ||
- latestResults.progress_8_ebacc != null || latestResults.progress_8_open != null) && (
- <>
- Attainment 8 Components (Progress 8 contribution)
-
- {[
- { label: 'English', val: latestResults.progress_8_english },
- { label: 'Maths', val: latestResults.progress_8_maths },
- { label: 'EBacc subjects', val: latestResults.progress_8_ebacc },
- { label: 'Open (other GCSEs)', val: latestResults.progress_8_open },
- ].filter(r => r.val != null).map(({ label, val }) => (
-
- {label}
-
- {formatProgress(val!)}
-
-
- ))}
-
- >
- )}
-
- {/* EBacc */}
- {(latestResults.ebacc_entry_pct != null || latestResults.ebacc_standard_pass_pct != null) && (
- <>
-
- English Baccalaureate (EBacc)
-
-
-
- {latestResults.ebacc_entry_pct != null && (
-
- Pupils entered for EBacc
- {formatPercentage(latestResults.ebacc_entry_pct)}
-
- )}
- {latestResults.ebacc_standard_pass_pct != null && (
-
- EBacc Grade 4+
- {formatPercentage(latestResults.ebacc_standard_pass_pct)}
-
- )}
- {latestResults.ebacc_strong_pass_pct != null && (
-
- EBacc Grade 5+
- {formatPercentage(latestResults.ebacc_strong_pass_pct)}
-
- )}
- {latestResults.ebacc_avg_score != null && (
-
- EBacc average point score
- {latestResults.ebacc_avg_score.toFixed(2)}
-
- )}
-
- >
- )}
-
-
- )}
-
- {/* ── Admissions ─────────────────────────────────── */}
- {admissions && (
-
- Admissions
-
- {admissionsTag && (
-
- {admissionsTag} {' '}
- {admissionsTag === 'Selective'
- ? '— Entry to this school is by selective examination (e.g. 11+).'
- : `— This school has a faith-based admissions priority (${schoolInfo.religious_denomination}).`}
-
- )}
-
-
- {admissions.places_offered != null && (
-
-
Year 7 places offered
-
{admissions.places_offered}
-
- )}
- {admissions.total_applications != null && (
-
-
Total applications
-
{admissions.total_applications.toLocaleString()}
-
- )}
- {admissions.first_preference_applications != null && (
-
-
1st preference applications
-
{admissions.first_preference_applications.toLocaleString()}
-
- )}
- {admissions.first_preference_offer_pct != null && (
-
-
Families who got their first choice
-
{formatPercentage(admissions.first_preference_offer_pct)}
-
- )}
-
- {admissions.oversubscribed != null && (
-
- {admissions.oversubscribed
- ? '⚠ Applications exceeded places last year'
- : '✓ Places were available last year'}
-
- )}
-
-
- Historical distance cut-off data is not available for this school. Contact the admissions authority for oversubscription criteria details.
-
-
- {hasSixthForm && (
-
- This school has a sixth form (Post-16 provision). Post-16 destination data coming soon.
-
- )}
-
- )}
-
- {/* ── History table ──────────────────────────────── */}
- {yearlyData.length > 1 && (
-
- Historical Results
- {yearlyData.length > 0 && (
- <>
- Results Over Time
-
- >
- )}
-
- View raw year-by-year data
-
-
-
-
- Year
- Attainment 8
- Progress 8
- Eng & Maths 4+
- EBacc entry %
-
-
-
- {yearlyData.map((result) => (
-
- {formatAcademicYear(result.year)}
- {result.attainment_8_score != null ? result.attainment_8_score.toFixed(1) : '-'}
- {result.progress_8_score != null ? formatProgress(result.progress_8_score) : '-'}
- {result.english_maths_standard_pass_pct != null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}
- {result.ebacc_entry_pct != null ? formatPercentage(result.ebacc_entry_pct) : '-'}
-
- ))}
-
-
-
-
-
- )}
- {/* ── Wellbeing ──────────────────────────────────── */}
- {hasWellbeing && (
-
- Wellbeing & Context
-
- {/* SEN */}
- {(latestResults?.sen_support_pct != null || latestResults?.sen_ehcp_pct != null) && (
- <>
- Special Educational Needs (SEN)
-
- {latestResults?.sen_support_pct != null && (
-
-
- SEN support
-
-
-
{formatPercentage(latestResults.sen_support_pct)}
-
Without an EHCP
-
- )}
- {latestResults?.sen_ehcp_pct != null && (
-
-
- Pupils with EHCP
-
-
-
{formatPercentage(latestResults.sen_ehcp_pct)}
-
Education, Health and Care Plan
-
- )}
- {(() => {
- const total = census?.total_pupils ?? schoolInfo.total_pupils ?? latestResults?.total_pupils ?? null;
- if (total == null) return null;
- const female = census?.female_pupils ?? null;
- const male = census?.male_pupils ?? null;
- const isMixed = schoolInfo.gender === 'Mixed' || schoolInfo.gender == null;
- const hasSplit = isMixed && female != null && male != null && female + male > 0;
- const sum = hasSplit ? female! + male! : 0;
- const girlsPct = hasSplit ? Math.round((female! / sum) * 100) : 0;
- const boysPct = hasSplit ? 100 - girlsPct : 0;
- return (
-
-
Total pupils
-
{total.toLocaleString()}
- {hasSplit && (
- <>
-
-
-
-
-
- {girlsPct}% girls
- ·
- {boysPct}% boys
-
- >
- )}
- {schoolInfo.capacity != null && !hasSplit && (
-
Capacity: {schoolInfo.capacity}
- )}
-
- );
- })()}
-
- >
- )}
-
- {/* Deprivation */}
- {hasDeprivation && deprivation && (
- <>
-
- Local Area Context
-
-
-
- {Array.from({ length: 10 }, (_, i) => (
-
- ))}
-
-
- Most deprived
- Least deprived
-
- {deprivationDesc(deprivation.idaci_decile!)}
- >
- )}
-
- )}
-
- {/* ── Finances ───────────────────────────────────── */}
- {hasFinance && finance && (
-
- School Finances ({formatAcademicYear(finance.year)})
-
- Per-pupil spending shows how much the school has to spend on each child's education.
-
-
-
-
Total spend per pupil per year
-
£{Math.round(finance.per_pupil_spend!).toLocaleString()}
-
How much the school has to spend on each pupil annually
-
- {finance.teacher_cost_pct != null && (
-
-
Share of budget spent on teachers
-
{finance.teacher_cost_pct.toFixed(1)}%
-
- )}
- {finance.staff_cost_pct != null && (
-
-
Share of budget spent on all staff
-
{finance.staff_cost_pct.toFixed(1)}%
-
- )}
- {finance.premises_cost_pct != null && (
-
-
Share of budget spent on premises
-
{finance.premises_cost_pct.toFixed(1)}%
-
- )}
-
-
- )}
-
-
- );
-}
diff --git a/nextjs-app/components/school/AdmissionsSection.tsx b/nextjs-app/components/school/AdmissionsSection.tsx
index 95f90dd..def1d3b 100644
--- a/nextjs-app/components/school/AdmissionsSection.tsx
+++ b/nextjs-app/components/school/AdmissionsSection.tsx
@@ -11,15 +11,14 @@
* toggle renders at all, so such pages ship zero admissions JavaScript.
*/
-import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import type { SchoolAdmissions } from '@/lib/types';
import { formatAcademicYear, formatPercentage } from '@/lib/utils';
import { summariseAdmissions } from '@/lib/compareLogic';
import { Section, sectionStyles as styles } from './sectionShared';
import { AdmissionsViewToggle } from './AdmissionsViewToggle';
+import { AdmissionsTrendChart } from './charts';
-const AdmissionsTrendChart = dynamic(() => import('../AdmissionsTrendChart'), { ssr: false });
export function AdmissionsSection({
admissions,
diff --git a/nextjs-app/components/school/HistorySection.tsx b/nextjs-app/components/school/HistorySection.tsx
index 01df546..4f3c041 100644
--- a/nextjs-app/components/school/HistorySection.tsx
+++ b/nextjs-app/components/school/HistorySection.tsx
@@ -4,16 +4,10 @@
* were only 40% similar). Server component.
*/
-import dynamic from 'next/dynamic';
import type { School, SchoolResult, NationalAverages } from '@/lib/types';
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
import { Section, sectionStyles as styles } from './sectionShared';
-
-const PerformanceChart = dynamic(
- () => import('../PerformanceChart').then((m) => m.PerformanceChart),
- { ssr: false },
-);
-const SatsChart = dynamic(() => import('../SatsChart'), { ssr: false });
+import { PerformanceChart, SatsChart } from './charts';
export function HistorySection({
yearlyData, schoolInfo, nationalAvg, primaryAvg, secondaryAvg,
diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx
new file mode 100644
index 0000000..2cafb22
--- /dev/null
+++ b/nextjs-app/components/school/PrimarySchoolSections.tsx
@@ -0,0 +1,139 @@
+/**
+ * PrimarySchoolSections — the section sequence for primary and all-through
+ * detail pages. Server component.
+ *
+ * All-through schools route here rather than to SecondarySchoolSections,
+ * because this list renders BOTH the KS2 and KS4 blocks (ResultsSection and
+ * HistorySection branch on isAllThrough); the secondary list is KS4-only and
+ * would silently drop their SATs data.
+ *
+ * The render conditions here MUST match buildNavItems in lib/schoolSections,
+ * or the sticky nav will link to sections that do not exist.
+ */
+
+import type {
+ School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
+ SchoolAdmissions, SchoolDeprivation, SchoolFinance, NationalAverages,
+} from '@/lib/types';
+import { ofstedLegacyAreas } from '@/lib/utils';
+import type { SchoolFlags } from '@/lib/schoolSections';
+import { OfstedSection } from './OfstedSection';
+import { ResultsSection } from './ResultsSection';
+import { AdmissionsSection } from './AdmissionsSection';
+import { InclusionSection } from './InclusionSection';
+import { HistorySection } from './HistorySection';
+import { SchoolLifeSection } from './SchoolLifeSection';
+import { LocalAreaSection } from './LocalAreaSection';
+import { FinancesSection } from './FinancesSection';
+
+export interface PrimarySchoolSectionsProps {
+ schoolInfo: School;
+ yearlyData: SchoolResult[];
+ absenceData: AbsenceData | null;
+ ofsted: OfstedInspection | null;
+ census: SchoolCensus | null;
+ admissions: SchoolAdmissions | null;
+ admissionsHistory: SchoolAdmissions[];
+ deprivation: SchoolDeprivation | null;
+ finance: SchoolFinance | null;
+ nationalAvg: NationalAverages | null;
+ flags: SchoolFlags;
+}
+
+export function PrimarySchoolSections({
+ schoolInfo, yearlyData, absenceData, ofsted, census,
+ admissions, admissionsHistory, deprivation, finance, nationalAvg, flags,
+}: PrimarySchoolSectionsProps) {
+ const primaryAvg = nationalAvg?.primary ?? {};
+ const secondaryAvg = nationalAvg?.secondary ?? {};
+
+ const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0);
+ // Report cards are dated by their own inspection (rc_inspection_date), never
+ // the legacy inspection_date (report cards exist only from Nov 2025).
+ const ofstedInspectedDate = isReportCard
+ ? ofsted?.rc_inspection_date ?? null
+ : ofsted?.inspection_date ?? null;
+ const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : [];
+ const oeifAllSameGrade =
+ !!ofsted &&
+ !isReportCard &&
+ oeifAreas.length >= 3 &&
+ oeifAreas.every((a) => a.value === ofsted.overall_effectiveness);
+
+ return (
+ <>
+ {ofsted && (
+