Both variables default to empty, so an environment without Unleash has
every flag off — the correct dark state rather than a boot failure.
The cache volume is the mitigation for the one real regression risk in
this design: the SDK evaluates everything False until it syncs, so a
backend cold-starting with an empty cache while Unleash is unreachable
would make a *released* feature disappear. On a named volume the disk
cache survives a restart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The existing distance journeys all skip when no school has a published
figure, which is right when the feature is off — and wrong when it is
supposed to be on and is silently broken, because that shows up as a
green run full of skips. The new gate fails in exactly that case.
Feature state is read from the data, not from /api/flags: the public
proxy denies that path on purpose, since it names unreleased features.
Presence of the admission_distance key is the observable effect.
Verified against staging, where the feature is currently on: the on-gate
passes, the off-gate skips, the existing eight distance journeys are
unaffected.
One honest caveat — the /api/flags check passes on staging today because
that image predates the endpoint, not because the denylist works. The
denylist itself is covered by the jest unit test; this is defence in
depth and becomes a real assertion once deployed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
One gate, at the source. The frontend needs no change: DistanceSection
already returns null when distance_m is missing, and the admissions
block already conditions on (admissions || admissionDistance). Only 57
local authorities publish cut-offs, so the off-path is the commonest
path on the site and is well covered already.
Absent, not null. /api/schools/ is public and unauthenticated, so a
field left in the payload is a published field — the reasoning already
recorded in c9a1892 when history was withheld. The two are also
different claims: null says this school has no cut-off, absent says
cut-offs are not being published at all. The frontend type now says so.
The feature is on main and live on staging and has never reached
production, which is what makes it the right first consumer: the flag
lets the code promote without the feature appearing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Ships without a consumer, deliberately. The first flag needs none — the
backend withholds the field and the page follows — but 'UI elements on
existing pages' is one of the three surfaces this capability exists for,
and a flag layer that cannot gate one is incomplete.
Never throws: an unreadable flag is a dark one, which matches the
backend's fail-closed default. A page that 500s because the flags
endpoint blinked would be a worse outcome than a hidden feature.
Reading flags pins the calling route to a 300s ISR floor, since Next
takes the lowest revalidate among a route's fetches. That matches what
/school/[slug] already sits at, and it is the same property that makes a
flip propagate without a webhook.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The endpoint and its exposure control ship together on purpose. The
moment /api/flags exists, app/api/[...path] forwards it — and the
response names every unreleased feature the codebase knows about, along
with whether it is on. Publishing that is the opposite of shipping dark.
Denied on an exact first-segment match, not a prefix, so /api/flagship
does not go down with /api/flags. Next reads the endpoint server-side
over the Docker network, which never transits the public proxy.
jest.setup.js now guards its browser globals. It runs for every suite,
including the one that declares @jest-environment node to exercise the
route handler — NextRequest needs Fetch API globals jsdom lacks, and
there is no window there to define matchMedia on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Unleash holds flag state; it does not hold the list of flags. REGISTRY
is that list, because the SDK evaluates an unknown flag to False and
without a registry that is an undeclared False — indistinguishable from
a typo in a flag name.
Fail-closed throughout, and never raises: an unset UNLEASH_URL, an
unreachable server, a client that throws, an undeclared name — all
False. A flag layer that can 500 a request path or stop the API booting
is worse than one that is switched off.
Every flag defaults to False, with no per-flag override, because a flag
that defaults on is a kill switch and this is deliberately not one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Its own Portainer stack, belonging to neither application stack: a
staging redeploy must not be able to disturb production's flag state.
One instance serves both. OSS Unleash ships development and production
environments with environment-scoped client tokens, so the same flag
holds independent state in each — which is what lets a feature be on in
staging, where the E2E journeys exercise it, while production stays dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Task 1 is the Unleash stack and ends with a human step — the Portainer
deploy and the token generation cannot be automated from here. Nothing
else blocks on it: an unset UNLEASH_URL means every flag is False, which
is what local development and CI get, so the whole suite runs without a
flag server existing.
Self-review caught three defects in the plan itself. get_supplementary_data
takes (db, urn), not (urn), and the test DataFrame was minimised to the
point where the endpoint would have failed for reasons unrelated to
flags — both now copy the known-good shape from test_school_details.py.
The proxy test needs the node jest environment, since NextRequest wants
Fetch API globals jsdom does not provide. And the e2e off-state check
hardcoded a URN, so a 404 page would have satisfied it without proving
anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Next uses the LOWEST revalidate among a route's fetches, not the segment
value. School pages fetch school details at 300s and place pages fetch
national averages at 3600s, so the effective ISR period is five minutes
and one hour respectively — not the seven days the segment declares.
A flag flip therefore propagates on its own, well inside the monthly,
by-hand cadence these flags are for. That deletes two webhook
integrations, a revalidate route, a secret-in-query-string scheme, an
idempotency requirement, and the rule that every fetch carry a cache
tag — which was the part most likely to rot as fetches are added.
Two constraints survive: a flag must never gate content on a
force-static page, because app/admissions never revalidates; and a
route-family flag must rebuild the sitemap, deferred with the route case
since no flag in scope touches it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Unleash self-hosted in its own Portainer stack, with FastAPI holding the
only SDK and Next reading flags through a tagged fetch.
The two hard parts are consequences of putting flag state in a service
rather than the repo: main stops being the whole truth about what is on,
and a flag can now change without the deploy that would have cleared the
caches. A code-declared registry bounds the first; webhook-driven
revalidateTag handles the second.
Cache tagging is deliberately coarse — every server fetch carries the
flags tag, not just the flags fetch itself. The first consumer proves
why: admission_distance changes the shape of /api/schools/{urn}, so a
narrow purge would leave ~25,000 school pages serving the pre-flip
render for a week, invisibly.
First consumer is the last-distance-offered feature, which is on main
and staging and has never reached production. It needs one gate, at the
API, because the frontend already no-ops on a missing field.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Every place page built its phase links as /schools/[slug]/[phase], the
shape that belongs to towns alone.
On an authority page that pointed into the town namespace. For 87 of the
151 authorities the target does not exist and the link 404s; for the
other 64 it resolves to the town of the same name — a different set of
schools, which is precisely the near-duplicate the two namespaces were
introduced to prevent. On an outcode page it 404s outright.
Two causes behind it, both a rule written twice and inherited by only
one of the places that needed it.
The authority phase route was in the spec and dropped by the plan, which
built the three bare routes and no fourth. The sitemap is generated from
the place registry, which was right about them all along, so 302
authority phase URLs have been submitted to Google and every one 404s.
Adding the route makes the sitemap true and serves a real query —
admissions are authority-run, so "primary schools in Kent" is how a
parent searches before they have settled on a town.
The outcode variants were the opposite: the registry computed phases for
outcodes although the spec gives them no route, and the sitemap knew to
skip them while the API did not. The registry now decides alone, and the
sitemap's duplicate of that rule is gone.
Also: an authority under the five-school threshold has no page, so the
API sends a null slug for it and the page names it without linking.
Two English authorities are in that position. It was unreachable in
today's data — verified across the EC and TR outcodes — but the thin
place redirect would have sent a reader to a 404 the year it isn't.
The e2e journey now walks every /schools link a page of each family
emits and requires a 200, which is the check that was missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Asked where schools with no results should sit in an alphabetical list, and
found that some pages were almost entirely made of them.
The per-phase threshold counted schools that were publishable — a result OR
an Ofsted grade — while a phase page exists for its results column.
/schools/kent/primary published with none of its five rows carrying a result;
Minehead had one of seven, Buntingford one of five. Forty-four phase pages
were majority-blank.
It is the same rule as "no page without a local average", which was written
into the spec as a thin-page control and never extended per phase.
The threshold now counts schools with a result for that phase. It gates
whether the page exists; it does not filter rows — a page that publishes still
lists every school of the phase, because someone looking up a school by name
has to find it whether or not it published results.
126 of 1,012 variant pages stop publishing: 62 primary, 64 secondary. Every
one of them was a table with too little in it to be worth a page.
The ordering itself is unchanged: pure A-Z, blanks interleaved. A school sits
where its name says it does, and at roughly a tenth of rows that reads fine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Someone on a place page is usually looking for a school they can name, so the
order should serve scanning for it rather than ranking. /api/rankings keeps
its league-table ordering; this is a place-page decision, not a site-wide one.
Sorted case-insensitively, or a capitalised name would sort ahead of every
lowercase one.
The change made five pieces of copy untrue, so they go with it. The phase
variant titled itself "— Ranked", and all four route families described
themselves as "ranked by SATs and GCSE results". A page that opens by claiming
an order it does not keep is worse than one that claims nothing.
The ItemList markup carried `position` with no declared order, which reads as
a ranking. It now declares ItemListOrderAscending, so the structured data says
what the table does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The staging gate was red on three journeys. All three were faults in the
tests; the site was behaving correctly in each case.
Next normalises canonical URLs against trailingSlash:false, so the homepage
ships "https://www.schoolcompare.co.uk" with no slash while every other route
keeps its path. Both address the same document. The test hardcoded the slash
and so failed only on the root — /rankings and /admissions passed throughout,
which is what made it look like a homepage bug rather than a test bug.
Compared with trailing slashes stripped from both sides.
The robots.txt assertion matched "Disallow: /" anywhere in the file and
tripped over the AI-crawler groups Cloudflare injects — ClaudeBot, GPTBot,
Amazonbot and six others all carry a blanket disallow, deliberately, and none
of them is Googlebot. It now parses the file into user-agent groups and checks
only the "*" group, which is also the thing the test was always trying to say:
Google may crawl the page, so it can see the noindex header.
Both were the same mistake as the doubled brand: asserting a naive string
rather than the semantics, and asserting against what the code assembles
rather than what the page renders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Two findings from review on #121, plus a third the review prompted.
The cap at three authorities silently dropped the fourth in exactly the case
where the information matters most — a genuinely fragmented place — and
contradicted the stated goal of naming every authority a place sits in. It is
gone. The share rule was always the real limit and already bounds the list at
ten. Measured against the live corpus, one town would have been truncated
today: LONDON, split evenly between Hackney, Lambeth, Westminster and
Lewisham.
parent_authority used mode() while authorities used value_counts(), and on an
exact tie pandas does not guarantee the two pick the same name, so the 301
could have pointed somewhere other than the authority named first on the page.
The parent is now derived from authorities[0]: one computation, one answer.
It also inherits the sentinel filter, so a place can no longer redirect to
/schools/authority/does-not-apply.
Chasing the truncation case surfaced a worse bug. Places were grouped by raw
town value, but the registry is keyed by slug, and GIAS spells the same place
several ways. Five town slugs come from more than one spelling: "London"
(1,819 schools) and "LONDON" (12) both slugify to `london`, so the later group
simply overwrote the earlier one — /schools/london could have shown twelve
schools, silently, depending on row order. Weston-super-Mare was split 14/19
across two spellings and Newcastle-under-Lyme across three. Grouping is now by
slug, and the display name is the most common spelling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
SW19 is mostly Merton but partly Wandsworth, and the page said only Merton.
The cause was one field doing two jobs: _parent_authority takes the modal
authority, which is right for a 301 target and wrong as a statement about
where a place is.
This is not a corner case. A quarter of viable outcodes (425 of 1,760) and a
third of viable towns (263 of 783) cross an authority boundary — Bedford the
town spans Bedford and Central Bedfordshire.
Place now carries `authorities`, every authority holding at least a tenth of
the schools and at least two of them, largest first. parent_authority stays
single and unchanged, because a redirect still needs one target.
The share threshold exists because GIAS carries postcode errors: EN6 lists two
Shropshire schools among fourteen in Hertfordshire, and a bare "any authority
present" rule would print those as though they were real. A place too small or
too fragmented to clear the threshold still names its largest, so the page
never goes silent about where it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The heading sat on the right edge of the column and every value on the left.
A specificity collision, not a layout problem: the two were aligned by
different selectors and only one of them won.
.table td (0,1,1) text-align: left <- won for the value
.num (0,1,0) text-align: right <- lost
.table th:last-child (0,2,1) text-align: right <- won for the heading
The heading and the value cell now share one class and one rule, so they
cannot drift apart again whatever else changes around them.
The column also stretched to half the table. It now hugs its content with
width:1% and nowrap, so the school name takes the remaining width — which is
what made the gap read as misalignment on a wide screen, and what crowded the
name column on a narrow one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Three presentation faults on the place pages, all found by looking at a
rendered page rather than at a test.
An unphased place page showed one primary-only measure for a list holding both
phases: 8 of 27 rows on /schools/brentwood were blank, because secondaries
have no reading-writing-maths score. Picking the other measure would only have
inverted which rows were empty, and putting both in one column would have
mixed a percentage with a 0-90 score. Each phase now gets its own table, so a
blank cell means the school genuinely has no published result — which is worth
saying, and now says "Not published" rather than a bare dash.
"RWM expected" was invented here. The site already names the measure in
METRIC_DEFINITIONS, surfaced at /api/metrics: "Reading, Writing & Maths
Combined %". The heading now reads "Reading, writing & maths" with the full
definition in the tooltip.
Links carried no class at all, so they rendered as default blue underlined
browser links beside a site that styles table links as body colour with a
brand hover. They now follow RankingsView's convention, and running-copy links
take the brand colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Every place page shipped as 'Schools in Brentwood - Compare 27 Schools |
schoolcompare | schoolcompare'. The root layout's title template appends
'| schoolcompare' to any plain-string title, and all four place routes already
carried the brand. W8 opted the other routes out with an absolute title; the
place routes were written afterwards and did not inherit the lesson.
~2,600 titles affected, and the repetition pushed them past Google's
truncation point, so the doubled brand displaced real words in the result.
An e2e journey now asserts no title repeats the brand, across the static
routes and a place page, so this cannot come back on a route added later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
/schools/[place]/[phase] shipped as routes but reached nothing. The sitemap
emitted one URL per registry entry and the registry had no phase dimension, so
~950 pages were absent from every sitemap — and PlaceView did not link them
either, leaving them reachable by nothing at all.
That is the query shape the baseline actually showed: 'primary schools in
beccles', 'secondary schools in brentwood'. Publishing the routes without a
path in meant building for the demand and then hiding from it.
Place now carries phase_urns so the per-phase threshold can be applied without
re-querying, the sitemap emits a variant wherever a phase clears the threshold
on its own, and the API exposes the qualifying phases so the place page links
only variants that exist. Outcodes are excluded: nobody searches 'primary
schools in SW11' and those routes do not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Sitemap regeneration failed on staging. 'richmond' in the curated locality
list collides with the GIAS town Richmond in North Yorkshire (37 schools), the
registry raised, and the admin endpoint 500d — taking down sitemap generation
for all 25,000 school pages over one bad row of curated data.
The guard now skips the colliding locality and logs an error. Skipping still
achieves what the guard was for — a locality never silently shadows a town —
without letting curated data break the site. That matters beyond this bug:
GIAS town names change with no code change here, so a raise could fire
spontaneously in production later.
Also removes four localities that were London boroughs rather than districts.
Hackney, Islington, Greenwich and Ealing are local authorities with 104, 72,
108 and 115 schools and already have authority pages; a locality defined by
two or three outcodes would have been a partial near-duplicate of one — the
thin-content failure the two-namespace design exists to avoid. A test now
guards the whole borough list.
Validated against the live corpus: 15 localities, no town collisions, no
authority duplicates, all 15 clear the threshold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
ItemList tells Google the page is a ranked set rather than prose;
BreadcrumbList puts the place in a hierarchy. School URLs in the markup are
absolute on the canonical host, since a relative URL in JSON-LD is ambiguous.
Eight journeys covering all four families, the two-namespace guarantee, the
threshold, the canonical, the sitemap and the local-versus-England line — the
last because that comparison is the reason these pages are not a name dropped
into a template.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Every generateStaticParams is gated behind PRERENDER_PLACES and wrapped in the
same try/catch the school route uses. The plan claimed authority pages were
'few enough to always prebuild' — but few enough still means the API must be
reachable at build time, and in CI it is not: the build failed with
ECONNREFUSED rather than degrading to ISR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Separate children per family so Search Console reports the location layer's
indexation apart from the school pages' — which is the point of the index
built in W1, and the number the stop condition watches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The registry is cached for the process and reset by the same admin endpoint
that rebuilds the sitemaps, so places and sitemap always describe the same
corpus rather than drifting apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The GIAS town field puts 1,819 London schools under the single value
'London', so it cannot answer 'schools in Battersea' — a query that appears in
the baseline. No single field can: parliamentary constituency gives Battersea
but not Canary Wharf, admin_ward gives Canary Wharf but not Battersea, and
neither gives Clapham or Shoreditch. So a locality is curated, defined by the
postcode districts it covers, which needs no new ingestion.
A locality may not shadow a published town: the registry raises rather than
silently costing a page that carries real demand. One below the threshold is
logged rather than raising, because a locality can legitimately be too small.
The pipeline seed mirrors the module, with a test guarding the drift — the
same arrangement gias_codes has, and for the same reason: the backend image
does not contain pipeline/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Two namespaces because 67 town names collide with an authority name and
neither set contains the other — postal towns cross authority boundaries, so
Bedford the town holds 104 schools against the authority's 86.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Seven tasks: the place registry, London localities and outcodes, the places
API, per-family sitemaps, the shared place view, the four route families, and
structured data plus the e2e gate.
Two things the plan corrects against the spec. The backend image does not
contain pipeline/, so the curated locality list cannot live only in a dbt
seed — it follows the gias_codes.py precedent instead, canonical in backend
with the seed as a mirror. And NationalAverages is nested by phase rather than
flat, which the first draft read wrongly and would have rendered every page
without its England comparison.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Supersedes the original spec's W2. The Search Console baseline inverted its
ordering: every measured location query is town or district level, none is an
administrative area, and phase is part of the query rather than a filter.
Two problems the original design did not anticipate. 67 viable towns share a
name with a local authority, and the authority is the larger set in only 43 of
them — postal towns cross authority boundaries, so neither can absorb the
other. Two namespaces resolve it by construction. And the GIAS town field
collapses 1,819 London schools into one value, which a curated
locality-to-outcode seed solves without new ingestion.
Sizing is measured against the live 25,185-school corpus rather than
estimated: 783 viable towns, 1,760 outcodes, 154 authorities.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The baseline says these pages already rank and are not clicked. 'compare
school performance' sits at position 6.1 with 0.43% CTR; 'compare schools' at
7.2 with 0.87%. The brand query 'school compare' draws 9.16% from the same
neighbourhood of the same results page, which rules out a ranking explanation
— when the snippet gives a reason to click, it gets clicked.
These SERPs are owned by the DfE's own 'Compare school performance' service.
The old title put a lowercase brand nobody searches for in the most valuable
pixels, then a near-paraphrase of that service's name. Beside the government's
own result it read as a lookalike.
Intent in the title, differentiator in the description. Titles now match what
people type, and the descriptions carry the one fact gov.uk does not publish:
how close you had to live to get a place.
/compare deliberately takes the tool phrasing rather than the homepage's, so
the two pages stop competing for one phrase. The root layout's default and
Open Graph copy were saying something different again; they now agree.
No hard school counts in any of it. The corpus moves with every data refresh
and this repo has already shipped one copy bug of that kind.
Tests guard the mechanics — SERP length, intent keyword, the differentiator,
no brand-first title — and leave the wording free to iterate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
_school_sitemap_rows tested only the latest year's row, which quietly dropped
every school with results in its history but a null row for the most recent
year — a school that stopped reporting, or whose figures were suppressed for
small-cohort disclosure.
The Mallard Academy (150367) is the case that caught it: real KS2 results for
2015-16 through 2018-19, then null rows from 2022-23 on. Its detail page shows
all four years; the sitemap omitted it. Sampling 40 of the 2,206 excluded
schools found 4 like this, so roughly 220 real pages were being withheld.
Publishable is now a property of the school, computed across every row, while
lastmod still comes from the latest row so the most recent Ofsted date wins.
The field list is a module constant shared with _has_publishable_data so the
two checks cannot drift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
79 remote branches deleted: 77 fully merged into main, plus
feat/seo-crawl-hygiene and feat/england-only-corpus, whose content is
preserved on feat/seo-crawl-hygiene-main (PR #110).
Each line carries the SHA, so any branch can be restored with
git push origin <sha>:refs/heads/<name>
The 14 branches left standing all carry content that differs from main and
none of them is mine to judge abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Staging serves the same image as production off stx., with robots.txt saying
Allow: / and no noindex — a fully crawlable duplicate of the site. Nothing
appears indexed today, most likely because the pages canonicalise across to
production, but that is a side effect rather than a control.
X-Robots-Tag, not a robots.txt Disallow. Disallow blocks crawling, which is
not the same as blocking indexing: a disallowed URL can still be indexed from
external links, and blocking the crawl means Google never fetches the page and
so never sees a noindex at all. Staging stays crawlable and answers noindex.
Matched on the staging host explicitly rather than 'any host that is not
production'. The inverted form would cover future environments automatically,
but its failure mode is deindexing production if the Host header ever arrives
rewritten by a proxy — which cannot be verified from here. This form's failure
mode is a new environment being indexable until someone adds it, which is
recoverable. Any new non-production hostname must be added.
The journeys only ever run against staging (deploy.yml passes
STAGING_BASE_URL; promote.yml smoke-polls production without Playwright), so
asserting the header there is safe. The two assertions live in one test
because the halves only work together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Search Console reports coverage per submitted sitemap, so one file per page
family is what will make W2's location pages measurable when they land. The
index's lastmod is generation time, which is the correct semantic there —
unlike on a <url>, where it would be a claim we cannot support.
Children sit under /sitemaps/ because Next only treats a whole bracketed path
segment as dynamic; a route folder named sitemap-[...parts] would be read as a
literal static segment and never match. Confirmed by the build output, which
lists /sitemaps/[...parts] as a dynamic route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Drops the schools with neither results nor an Ofsted grade, adds /admissions
which was never listed, replaces the invented priority and changefreq with a
lastmod taken from each school's Ofsted date.
lastmod is omitted where no date is known rather than defaulted to now. An
always-now lastmod is a claim Google learns to distrust; absent honestly
means unknown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
25,193 schools make ~317 million pairs. The bare page stays indexable as the
landing page for the head term; the parameter space goes noindex, follow so
its outbound links still count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The homepage read eleven search params and declared no canonical, so every
filter combination was a crawlable near-duplicate of the page we most want to
rank. Rankings and admissions declared none either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The apex 301s to www at Cloudflare, but metadataBase, the school-page
canonical, robots.txt's Sitemap: line and the sitemap's own <loc> entries all
named the apex. Every one of those pointed Google at a redirect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Beechwood College (URN 142458, Sully, CF64 5SE) survived the England-only
filter. GIAS types it a Special post 16 institution (32), not a Welsh
establishment (30), so filtering on establishment type alone left it behind —
the last Welsh school on the site, and the reason Vale of Glamorgan was still
in the authority list.
The earlier verification claimed the type codes mapped onto the Welsh
authorities in both directions. That was checked exhaustively for Cardiff and
by count for three others; Vale of Glamorgan was never checked, and it was the
one that did not hold.
LA code is the reliable discriminator: GIAS gives the 22 Welsh unitary
authorities the contiguous block 660-681, which English authorities never use.
Filtering on postcode would have been wrong — Redbrook, Tutshill, Wyedean and
two other Gloucestershire schools carry NP16/NP25 postcodes because Royal Mail
areas straddle the border, and they are English schools with English data. An
e2e test now pins those five so the fix cannot be simplified into a postcode
filter later.
The 660-681 range is documented GIAS structure this project cannot verify from
its own data, so the range filters and the authority NAME checks: if the range
is ever wrong, a Welsh authority reappears in assert_england_only_schools and
the pipeline fails loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Next only treats a whole bracketed path segment as dynamic, so the planned
app/sitemap-[...parts]/route.ts would have been read as a literal static
folder and never matched. Children move under /sitemaps/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
Five tasks: one canonical host, canonicals on every route, noindex on
parameterised comparisons, a sitemap that drops dataless schools and invented
priorities, and a per-family sitemap index.
Planning turned up a fault the spec had missed: the apex 301s to www, but
metadataBase, the school-page canonical, robots.txt's Sitemap: line and every
sitemap <loc> named the apex. Task 1 fixes it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
GIAS ships the whole UK plus overseas and offshore establishments. None of
them carry comparable DfE performance data — Wales does not publish on the
English measures at all — so every one of these pages rendered with null
results, null Ofsted and null phase. There were 2,036 of them: 1,569 Welsh,
123 offshore (Jersey, Guernsey, Isle of Man, Gibraltar), 316 British schools
overseas and 28 service children's schools. All 2,036 were being submitted to
search engines, alongside 29 local authorities that existed in the filters
purely to list them.
Filter at the mart boundary rather than the view layer. dim_school and
dim_location both exclude TypeOfEstablishment in {25, 26, 30, 37}, listed once
as vars.non_england_school_type_codes. Everything downstream reads those two
marts — search, the school page, /api/filters, rankings, Typesense and
build_sitemap() — so one filter removes them from the site and the sitemap
together, and Typesense drops them on its next rebuild since it recreates the
collection and swaps the alias rather than upserting in place.
coalesce rather than a bare NOT IN: a null type code would make the predicate
null and drop the row silently, and an unknown type is not grounds for
exclusion. No establishment has a null type today, but a future GIAS refresh
could ship one and the loss would be invisible.
assert_england_only_schools guards both directions: no excluded type survives
in dim_school, and dim_location holds no URN dim_school lacks — the API
inner-joins them, so the two filters drifting apart would silently shrink the
corpus.
Corpus goes from 27,229 schools to 25,193, and the authority list from 182 to
153. The 1,569 Welsh URLs now 404.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
The failing journey was wrong; the page was correct.
Locator: getByRole('button', { name: 'Distance' })
Expected: 0 Received: 1
getByRole matches accessible names by case-insensitive SUBSTRING, so
{ name: 'Distance' } matched <button>Show this distance on a map</button> —
the map toggle added by the same feature. The assertion was meant to say "no
Distance tab in the admissions segmented control" and instead said "no button
anywhere whose label contains the word distance".
Now scoped to the control it is about, via its own aria-label, and read
positively: the tab list must contain "This year" and must not contain
"Distance". An absence check against an unscoped locator passes for the wrong
reason the moment the selector stops matching, which is exactly how the
regression this test guards would return unnoticed.
Two sibling locators had the same weakness and are tightened: 'Check' is a
prefix of the button's own busy label "Checking…", and the figure matcher
accepted `(miles|m)` — a leftover from the mixed-unit era that would have kept
passing if the headline regressed to metres, which is the thing #105 just
fixed.
Tightening that matcher surfaced a real defect behind it. The cut-off figure
and its metric support are flex children with the gap drawn by CSS and nothing
between them in the text layer, so the element read "0.88 miles1.4 km" —
what a screen reader announces, and why a `miles\b` boundary could never
match. Both templates now carry an explicit space. Whitespace text nodes are
not rendered as flex items, so the reading changes and the layout does not.
Verified against staging: 54/54.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
The postcode check read "69 m away — inside the September 2026 cut-off of
0.17 miles". Both numbers are right and the sentence is still useless: the
reader has to convert one of them to check a comparison we had already made
for them.
The cause was a readability rule of mine in formatCutoffDistance, which swapped
to metres below 100m on the grounds that "0.04 miles" carries less than "69 m".
Taken one figure at a time that holds. Taken in a sentence containing two
figures it guarantees a mismatch whenever they fall either side of the
threshold — and a 270m cut-off with a nearby home does exactly that.
Miles now lead everywhere. It is the unit UK school admissions runs on:
councils publish cut-offs in miles (90% of the collected source rows), and it
is what a parent has already been quoted in their booklet and offer letter.
The metric figure survives only as support beside the miles figure on the
Admissions tile, where it converts the same value rather than presenting a
second one to compare.
Below 0.01 miles the decimal places run out rather than the unit being wrong,
so a very short distance is described — "under 0.01 miles" — instead of
rounding to a flat "0.00 miles", which would read as no distance at all.
Both figures in the verdict now go through one formatter with no fallback that
could reach for another unit. The old `?? "N m"` fallbacks on that line were a
second route to the same defect and are gone.
Covered by a sweep over sixty home/cut-off combinations spanning the old
switch point, asserting no verdict contains a metric reading and that exactly
two miles figures appear; plus the reported case pinned verbatim, and an e2e
guard on the rendered verdict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
Earlier years are to become a paid feature, so they stop being published.
The load-bearing part is that this is a change to the API, not only to the
page. /api/schools/{urn} is public and unauthenticated: leaving
admission_distance_history in the payload while declining to render it would
have handed the whole record to anyone who opened the network tab. It is
withheld at the source, and the page follows.
Nothing changes upstream. The tap, the plausibility band and
fact_admission_distance are untouched and still load every published year, so
restoring history for entitled callers is a change to one function in
data_loader rather than a re-collection.
What the reader now gets is the latest figure on the Admissions tile, and a
Distance section that answers the question the number alone cannot: whether
their own address falls inside it. Retitled to "How far away are you?", which
is what it now does — the previous title described a record that is no longer
there.
Removed with the history: the trend chart, the year-by-year table, the
per-year verdict strip, the trend summary and the coverage note, along with
their CSS. The section goes from 743px to 417px.
One consequence worth naming. A run of years used to soften a single close
call — a home just outside one year's cut-off was usually inside another. With
one year published, the "too close to call" band is the entire safety margin
between a parent and a place they do not have, so the verdict now names its
year, and the three outcomes are tinted apart rather than distinguished by
wording alone.
The existing stylesheet test earned its keep here: the three verdict classes
were referenced before they were written, and it caught them. Unstyled, a
"beyond the cut-off" result would have been indistinguishable from an "inside"
one — the exact failure the longhand class map was written to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
The admissions card measured 1503px on a live school page — half the height
of every section put together, and nearly three times the next largest — with
its default view rendering as four tiles adrift in about 1080px of blank card.
The cause was a layout trick meeting content it was never sized for. The
admissions views are stacked in one grid cell so switching them never shifts
layout, and the hidden ones keep their box: only visibility is dropped. That
works while the views are comparable. The distance view added in #102 carries
a chart, a table and a map, came to 1402px against the tile grid's 316px, and
pinned every other view to its height — including the one that renders by
default, which nobody had clicked.
Rather than only unpinning it, the detail moves out. Every other topic on the
page is a section with a nav entry, and "how close did we need to live, and
would we have got in?" is a topic, not a variant reading of the intake
figures. The headline number stays on the Admissions tile where the intake
story is; the record behind it now lives in a Distance section directly below.
admissions 1503px -> 554px
distance new -> 743px (median section on the page is ~528px)
Three further changes, each of which also makes the content better rather
than only shorter:
* The map renders on request. Before a postcode is entered it is a circle
drawn round a school, and it costs a Leaflet bundle and 240px to say so;
a successful check opens it automatically, which is the point at which it
starts answering something. Map height 320px -> 240px.
* The chart appears only at the four published years that let the summary
state a direction. Below that we already refuse to call the series a
trend, and a line through three points asserts one regardless of what the
sentence beneath it admits. The table carries those years anyway, with
the reasons a line cannot show.
* Three caveat paragraphs become one. They said walking-route twice and
made the same point about priorities in two voices. It now sits in
CutoffDistanceDetail rather than inside the check, so it still renders
for a school with coordinates missing, where there is a table but no map
and no check.
The new e2e guard asserts no section exceeds 2.5x the median section height.
Measuring the card's internals cannot catch this: the tile grid is flex: 1,
so it absorbs the stretch and every box still looks full. The first version
of this test targeted an arbitrary primary, passed against the live bug, and
proved nothing; pointed at a school that actually holds cut-off history it
fails on staging with "#admissions is 1459px against a 526px median".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
Completes the last-distance-offered feature against the mockup: the
year-by-year record, the same numbers drawn over real streets, and the
reader's own address measured against them.
Serving the history
The first cut deliberately served only the latest year, because a plain
series would draw a trend line straight through gaps that are absences of
publication, not of a cut-off. That reasoning is answered rather than
abandoned: cutoffYearRows classifies every year in the span, and the chart
breaks the line rather than interpolating across it.
A missing year is not one fact but three. It may be unpublished; it may be
a year the school was not oversubscribed; or there may be no record at all.
Collapsing them into "no data" throws away the reassuring case and hides
the important caveat, so each is stated in words in the table.
The claim is held to what the data supports. fact_admissions.oversubscribed
compares FIRST PREFERENCES against places, which does not establish that
every applicant was offered one — so the copy says "places available on
first preferences" and a test asserts the stronger claim never appears.
The trend summary is not a verdict
It names both endpoints and their years and lets the reader conclude. It is
withheld below four published points, and a swing under a tenth of the
earlier figure is reported as "broadly the same" rather than dressed up as
a direction.
The postcode check
This is the only place on the site that answers a question about a family
rather than a school, so most of the care went into what it refuses to say.
postcodes.io returns a centroid covering roughly fifteen addresses, which
against a 500 m cut-off is a fifth of the whole distance — so a margin
inside 100 m returns "too close to call" rather than a place a family does
not have. Unpublished years count as unknown, never as a pass. The limits
are stated before the check is used, not revealed with the answer.
The postcode is geocoded in the browser and never stored.
Both templates
Banded and selective secondaries are exactly where this matters most, so
the detail is shared. The primary page gives it a third tab; the secondary
page is one flat panel by design and renders it inline.
Absence is explained rather than reported. A selective school's missing
figure is explained by how it admits; a consistently undersubscribed school
reads as good news.
Also makes the batch loader's test double honour ORDER BY. It was a no-op,
so "latest row per URN" was really "first row in the fixture" and the test
would have passed with the sort reversed or removed.
Verified: 214 frontend tests, 54 backend, 45/53 e2e green against staging
(the 8 cut-off journeys skip until the DAG runs). Rendered offline against
the real compiled CSS in both themes and at 390px; every new surface clears
WCAG AA, measured on composited pixels.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
Adds the cut-off distance a parent actually asks about — "how close do we
need to live?" — end to end: a Singer tap, dbt staging and mart models, an
Airflow DAG, and a tile on both detail templates. 3,597 schools across 57
local authorities carry a figure; the rest are unchanged.
There is no national source for this. Each LA publishes its own cut-offs in
its own format, and the collected CSV is transcribed from PDFs, spreadsheets
and web pages — so most of the work here is deciding what is safe to show.
Data
* tap-uk-school-distance loads the CSV verbatim into raw. Keyed on
(urn, year, school_name), because school_name carries the admission
route: (urn, year) alone collides on 118 keys and a reload would have
silently dropped every band but one.
* stg_school_distance applies a 25 m – 25 km plausibility band. The source
contains 0.0-mile rows (published where a school filled on a higher
criterion), 1-metre cut-offs, and one reading 533 miles — ~4% of rows,
all of which would put a visibly wrong number on a live page.
* fact_admission_distance collapses routes to one row per school per year
using the furthest, and keeps route_count so the page can say the figure
is the widest of several bands rather than the one for a given child.
Serving
* Kept out of fact_admissions: that mart is EES-derived and near-complete
for England, this one covers 57 LAs, and the two refresh independently.
* Latest year only. Coverage is ragged — a school may have 2021 and 2026
and nothing between — so a history array would invite a trend line drawn
through gaps that are absences of publication, not of a cut-off.
* The Admissions section now renders on either source. 3% of the schools
that render have a cut-off and no EES admissions row, and gating on
admissions alone would have hidden the figure on those pages.
Interface
* The year travels with the figure everywhere it appears; a cut-off
detached from its admissions round is not a fact about anything.
* "Not a fixed catchment — it moves every year" sits under every instance,
because that is the inference a parent will otherwise draw.
* Replaces a hardcoded "Historical distance cut-off data is not available
for this school" that appeared on every secondary page, including the
ones whose council does publish it. The absence is now stated only when
it is real, and names the authority that would hold it.
The tint costs the muted tokens their AA margin: measured on the composited
backdrop (not the computed one, which reports the untinted card), --text-muted
falls to 4.09:1 in dark theme. The tile uses --text-secondary instead — 6.50:1
dark, 6.60:1 light.
The DAG is manual, like the other annual ones: councils publish on allocation
day, each on its own timetable, so there is no date worth scheduling against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
Two unrelated working-tree changes, committed as one at the owner's request.
Removes "Built for parents, by a parent." and its comment from the hero. It
was added two commits ago; taking it out is the owner's call, and the reasons
it was placed under the search rather than in the footer no longer apply.
The .heroByline rules in HomeView.module.css are deliberately left in place.
They are now unreferenced, but the class was purpose-built for this one line
and keeping it makes restoring the byline a one-line change. If the removal is
permanent, that block (and its 640px media query) should go with it.
Also updates two quoted figures in the 2026-07-02 UX audit notes from
"24,000+" to "27,000+".
Worth noting for the record: that file documents what the page said when it was
audited, and at that time it genuinely did say "24,000+" — which was itself the
bug later fixed by reading unique_schools instead of a field the API never
sent. Editing the quoted evidence makes the note read consistently with the
current site, at the cost of no longer being a verbatim record of what was
observed. Left as the owner edited it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The marker was var(--brand) — the identical value to the bar fill it sits on.
Measured by pixel-sampling every bar on both templates, in both themes:
primary SatsChart .natTick 1.00:1 (6 bars)
secondary .att8VizNatLine 1.00:1
Not low contrast. The same colour. It scored 5.47:1 only against the empty
track, which means it was visible precisely when a school was BELOW the
national average and vanished for every school at or above it — failing for
exactly the schools people are looking for.
No single colour fixes this, because the marker's position is data-driven: it
can land on the bar, on the empty track, or across the boundary. It is now a
knockout — a light core carrying a dark edge, both from tokens that flip with
the theme, so one part or the other always separates:
on the bar on the track
light 5.47 / 9.70:1 15.17:1
dark 7.81 / 10.85:1 13.52:1
THE LEGEND DESCRIBED A CHART THAT DID NOT EXIST
Both data swatches were var(--status-above) green while their bars were
var(--brand) teal, and the two were identical to each other — one swatch for
two series. Worse, the only swatch matching the bar colour was the one
labelled "National average", so reading the chart by matching colours told you
the teal bars were the benchmark. Each swatch now carries its bar's exact
value, and the marker swatch mirrors the knockout.
TWO SERIES, ONE COLOUR
Expected and Exceeding were both var(--brand), distinguished only by row.
They are a sequential pair — exceeding is the same cohort at a harder bar — so
they take two steps of one hue, the harder measure being the step further from
the ground in each theme.
They measure 1.77:1 (light) and 1.39:1 (dark) against each other, and that is
accepted rather than overlooked: two fills that must EACH clear 3:1 against
the same white track are geometrically forced close together. The distinction
is carried by the row labels and printed values; colour is redundant here, not
load-bearing.
THE TEST THAT SHOULD HAVE CAUGHT THIS
The WCAG journey composites backgrounds by walking the ancestor chain, but
these markers are absolutely positioned over a sibling — ancestor-walking is
structurally blind to overlap, which is why this shipped in two templates and
passed every gate. The new journey asks the stacking order instead, via
document.elementsFromPoint.
Writing it surfaced a second trap worth recording: elementsFromPoint takes
viewport coordinates and returns an empty stack off-screen, and these markers
sit ~1200px down. The first version defaulted an empty stack to white, which
made teal-on-teal look like teal-on-white and PASS in the light theme. It now
scrolls each marker into view and counts any marker it cannot resolve a
backdrop for as a failure rather than a pass.
Verified both directions: the new journey fails against current staging with
"1.00:1 over rgb(15,118,110)" in both themes, and passes against this build
with 6/6 markers measured at 5.47–15.17:1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same 16:9 frame, but not a drop-in replacement — the composition, palette and
copy area all moved, and each one drives a change here.
COPY AREA
The old artwork was a flat #FDF9F3 down its whole left side. This one runs
peach #FEE8D2 at the top to cream #FDF3E7 around 43% height, and below ~48%
the left edge is foliage rather than cream at all. --hero-ground is resampled
to #FEF2E1, the middle of the pale run where the headline and search actually
sit; the scrim covers the foliage further down.
Measured on rendered pixels with the text hidden, sampling the real glyph runs
(via Range, not the element boxes) plus a 120px growth margin:
title body byline
light 13.61:1 5.74:1 6.66:1
dark 9.49:1 5.45:1 8.35:1
Body drops from 6.52:1 to 5.74:1 as the foliage comes closer, still clear.
BAND CROP
The schoolhouse moved to ~82% across the frame, leaving only ~140px of artwork
to its right, so the band crop is anchored to the right edge and takes 1344px
back — putting the school at 73%, which the build script now derives and
prints rather than leaving it to drift from the CSS.
The crop is also 3.2:1 rather than matching the phone. The band is not one
ratio: it runs 2.4:1 on a phone to about 3.8:1 under the one-column
breakpoint. Cropping at the narrow end means the wide end throws away height,
which cut the flag off the roof and the base off the building. Sitting above
the middle costs a little width on phones — where the crop's left is hillside —
and keeps the building whole where it matters.
BAND HEIGHTS
Raised to 13rem (861–860px) and 10rem (≤640px). The band was widest-per-height
at exactly 640px, where an 8.5rem band measured 4.2:1 — worse than any wider
viewport, because the height steps down at that breakpoint while the width does
not. Ratios across the range are now 2.0 / 2.6 / 3.6 / 3.1 / 3.8 rather than
spiking. The search still lands at 442px on a 667px viewport.
WEIGHT
Source is 3.1MB; a browser fetches one file — 29–59kB on desktop, 10–14kB on a
phone. Widths re-cut for the larger master: 2000/1400/1000 wide, 1344/900/600
band.
Verified: 0 AA failures across both themes at 1440 / 860 / 390, every srcset
entry present on disk with no orphans, tsc clean, 159/159, build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Built for parents, by a parent." — the one line on the page written by a
person rather than by a product.
Placed under the search rather than in the footer, which is where it would
have been true and unread. It does its work at the moment someone is deciding
whether to trust a page full of government statistics, which is the moment
they are looking at the search box.
Set apart without shouting: display face, a step down in size, and a short
brand rule in place of a bullet. No italic — Manrope ships none in the loaded
weights, so font-style would be synthesised into a slant, the same reason
.heroEmph resets it.
Deliberately the only claim of its kind on the page. Every other trust signal
here is about the data's provenance and is checkable against the DfE and
Ofsted; this one is about who built it, and is not. So it is stated once,
plainly, and never repeated — the opposite of the "Official & trusted" line it
now sits near, which claimed something about the site that was not true.
Measured 7.26:1 in the light theme and 8.86:1 in dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things, all reported from the live mobile view.
WE ARE NOT AN OFFICIAL SERVICE
The value prop read "Official & trusted", whose grammatical subject is this
site — it reads as a claim that schoolcompare is an official service. It is
not; it is an independent site that republishes official figures. Now "Built
on official data", which describes the data instead.
Checked every other use of the word: all nine describe the data ("official DfE
figures", "the official figure isn't in") and are correct as they stand. That
one title was the only place the site described itself.
The footer now says it outright — "An independent site. Not affiliated with
the Department for Education or Ofsted." — so it appears on every page rather
than being left to inference. An e2e test asserts both halves: that the
statement is present, and that no text presents the site itself as official.
The disclaimer first measured 4.71:1 against a 4.5 floor. That is a fine
margin for decoration and the wrong one for a line whose job is to be legible
to someone checking whether this is a government site; it is now 5.59:1,
quieter than the copy around it by size rather than by contrast.
THE ARTWORK SAT UNDER THE SEARCH ON PHONES
The DOM keeps .heroContent first so the desktop overlay does not depend on
source order, which left the band stranded at the bottom of the panel, reading
as a strip stuck on the end rather than a hero image. `order` moves it above
the copy on phones; it is decorative and aria-hidden, so no reading order
changes. Measured on a 667px viewport — the shortest phone still in use — the
search button lands at 418px, comfortably inside the fold.
THE SEARCH INPUT WAS UNUSABLE ON PHONES
"Search schools" is a fixed 134px with white-space: nowrap, so it took 48% of
the row. Measured on staging:
390px input text space 102px placeholder needs 194px
360px 72px
320px 32px
At 320px you could not see what you were typing. The button now wraps to its
own full-width row below 480px, which fixes 360px and up — 390px goes from
102px to 226px of text space. The pill stays a single element, so it keeps its
border, shadow and :focus-within ring, and the button gains a full-width tap
target.
320px is still ~30px short. Closing it needs a shorter placeholder, which three
other tests match on by exact string; left alone deliberately rather than
churn them for a width that is effectively gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The staging gate has been failing these two since the supplied logo artwork
replaced the reconstruction. Both tests were still asserting the previous
implementation, and both were right to fail — they were just describing
something the site no longer does.
the header carries the schoolcompare lockup
Looked for an <svg> inside the header link. The mark is raster now:
<picture><source><img src="/brand/mark.png">. Nothing matched, so the
locator timed out.
the brand asset set is complete and served
Requested /apple-icon, which 404s. The route moved when the generated
app/apple-icon.tsx became a static app/apple-icon.png — generated icons
serve at /apple-icon, static ones at /apple-icon.png with a content hash.
The icon was present and correctly linked the whole time.
Both now read from the page instead of hardcoding the shape of the answer: the
touch icon is fetched from its own <link rel="apple-touch-icon"> href, the way
this test already handles og:image, so it follows whatever Next emits.
The lockup assertion also got stronger rather than merely corrected. A
<picture> whose sources all 404 still lays out and still satisfies
toBeVisible(), so that alone would go green on a broken lockup; it now asserts
naturalWidth, which only a decoded image can satisfy.
Verified against staging directly: 41/41 pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At 0.52 the scene was legible but heavily suppressed; 0.75 lets the hills,
path and schoolhouse read while the white H1 stays comfortably the brightest
thing on the panel.
Re-measured rather than assumed, because in the dark theme the text is light
and the artwork is behind it — brightening the image lowers text contrast
rather than raising it. Off rendered pixels, sampling background up to 120px
past each line's right edge:
brightness title body
0.52 11.01:1 6.44:1
0.75 9.48:1 5.21:1
Both still clear the 4.5:1 floor, body being the binding one. The trade is
recorded next to the value so the next person to reach for it knows it has a
floor and not just a taste range.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from review on #93, both verified before fixing.
<img src> cannot vary by viewport, so it was always the wide desktop crop. A
browser taking neither AVIF nor WebP therefore fell through to the desktop
frame on a phone and lost the schoolhouse — the exact failure the two-crop
<picture> exists to prevent, surviving in the one path nobody looks at. The
band JPEG the build script already emitted was never referenced, which was the
tell. It now backs a <source media> placed after the modern formats, so they
still win wherever they are supported.
Verified by stripping the AVIF and WebP <source>s at runtime and letting
<picture> re-resolve, which is what an old browser actually sees:
phone hero-band-500.avif → hero-band-700.jpg (band crop, school kept)
desktop hero-wide-1672.avif → hero-wide-1200.jpg
sharp was not declared: it arrives transitively from next@16.1.6, so the
documented regeneration command works today and breaks on a Next upgrade or a
clean install that resolves differently. Declared in devDependencies for the
same reason next.config.js already declares its traced font files rather than
trusting the tracer to keep finding them.
The third finding — that the hero's licence is marked unconfirmed in
CREDITS.md while the artwork ships — is accurate and deliberate. It is the
owner's to answer; recording it as unknown is the point of the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the hand-drawn SVG landscape with the illustration supplied by the
project owner. Master is assets/hero-source.png; everything under
public/brand/hero-* comes from scripts/build-hero-images.js and should never be
hand-edited.
LAYOUT
The artwork is composed as a full-bleed hero: it reserves an empty cream area
down its left side for the headline. The old hero was a two-column grid with
art in the right 0.88fr, which would have cropped that reserved area off and
shrunk the scene into a thumbnail — the one thing the picture is built not to
be. So the panel is now a single layered block: artwork behind, copy on top,
and below the one-column breakpoint the artwork leaves the background and
becomes a band under the search.
Overlaying does not survive down to phone widths — the panel gets too narrow
for the copy to stay inside the cream, so the scrim would have to cover nearly
the whole image and you would be left with a tinted rectangle. Hence the
switch at 860px rather than a single treatment stretched across every width.
CONTRAST
The copy sits on a gradient of --hero-ground, a new token sampled from the
artwork's own cream (#FDF9F3) rather than from Sand. Sand is eight to thirteen
points darker per channel, which leaves a visible seam straight down the hero.
The scrim exists because the artwork is a fixed image on a fluid panel: past
some width the headline would otherwise land on hillside green.
Measured on rendered pixels, sampling background up to 120px beyond the right
edge of each line, so a longer line still has margin:
light title 14.36:1 body 6.52:1
dark title 11.01:1 body 6.44:1
The worst light case is hillside green showing through the scrim at 6.52:1.
TWO CROPS
The slot is two shapes: roughly 2.1:1–2.7:1 behind the desktop panel, and
2.6:1–4.9:1 as the band. A single file under object-fit: cover centre-crops,
and at the band's extreme that slices a strip through the scene and loses the
schoolhouse — exactly how the drawn hero failed on phones. <picture> switches
crop, not just resolution: the band file is pre-cropped around the school and
is already near 2.6:1, with the subject at ~65% across so squeezing toward
4.9:1 crops the empty sides instead.
WEIGHT
1.6 MB PNG in, AVIF and WebP out; a browser fetches exactly one file — about
23–33 kB on desktop, 9–18 kB on a phone. The JPEG is only the <img> fallback.
`sizes` describes the real panel box (max-width 1400 minus padding) rather than
100vw, which over-requested a candidate at every width on the LCP element.
DARK THEME
A raster cannot be re-graded token by token the way the drawing was, but it
would reproduce the same failure — a bright illustration is the brightest
object on a near-black page. It is dimmed in CSS to read as dusk, and the
scrim fades it into the dark panel rather than into cream.
TESTS
The two illustration journeys are replaced by three, each covering a failure
that still renders a valid-looking page: the artwork actually loading
(naturalWidth, not src), the crop switching at the breakpoint, a modern format
winning negotiation, and the dark theme dimming it.
public/brand/CREDITS.md records provenance for every asset in that folder. The
hero's licence line is marked unconfirmed — that one needs the owner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main's staging E2E has been red since #90. Two of the four failures were the
staging container serving a stale image at the moment the gate ran — apple-icon
and the header lockup both check out against staging now. The other two were
real, and both are the same mistake: a colour pairing verified against one
ground while the element sits on another.
* .heroEyebrow --brand on its own 10% tint over Sand is 4.19:1. The token
clears AA on Sand, but the tint darkens the ground under the label, and
that composite was never the thing measured. --brand-strong is 5.85:1.
* .hiwVisual The preview panel was grounded in Sand while everything
inside it is a translucent status tint — and those tints are specced to
clear AA over --bg-card. Over Sand the report-card chips landed at 4.22:1
and 4.29:1. The ratio depended on a background two levels up. Moving the
panel to the card surface puts them at 5.69:1 and 5.81:1; a border keeps
it reading as an inset frame now that panel and card share a colour.
* Footer .sectionTitle --sage flips with the theme; the footer band does
not (it is teal in both). So the pairing only held in one of them — the
dark sage measured 4.08:1, on every page. Now --on-sunken-muted, which is
what the --on-sunken-* family exists for, as Footer.tsx's own header
comment already says.
* .compareHeadLabel --text-muted on the header row's tint is 4.45:1 in the
dark theme. Under by a hair, same cause. Now --text-secondary.
The harness gained the footer, because it had no footer and so could not have
caught the one failure that appeared on all three pages. Rewriting fragments
now uses each CSS module's own hash — the footer rewritten with HomeView's
prefix renders unstyled, which would have made any contrast measured on it
meaningless while still reporting a number.
Verified: 0 AA failures in both themes across the assembled page, measured with
the same probe the e2e test uses, against the real compiled CSS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The homepage made four statements that were not true, carried elements that
asked nothing of anyone, and had a hero illustration that broke in both the
places it had to work.
Claims, all verified against the code or the API:
* "24,000+ schools" (three places) against a real 27,230. The fact box meant
to show the live figure rendered its own fallback on every request, because
DataInfoResponse declared a `total_schools` field the API has never sent —
it sends `unique_schools`. The fetch succeeded; only that field was
undefined, so nothing threw and nothing failed. The interface, not the
code, was the thing that was wrong.
* "Up to three schools side by side" against MAX_SCHOOLS = 5, contradicting a
card 400px below it that correctly said five.
* "Class sizes" — data the codebase has never held. That copy line was the
only hit in a full-repo grep.
* Invented results and Ofsted grades attributed to two real, named schools
in the compare preview.
Also one feature, three words: Compare (nav), shortlist (footer), pin (cards).
Settled on Compare everywhere. And <title> was the bare string "Home".
Cut: the trust line (repeated the coverage figure one paragraph after the hero
gave it, behind three decorative dots), the "Start exploring" row (three links
to two destinations already in the nav), the six-row coverage table, and three
of the four countdown cards — which gave the page's largest numeral to dates up
to 245 days away, two of them offer days, which cannot be missed. All four
dates remain, at proportionate weight. Value-prop titles drop from <h2> to <p>;
they were outranking the page's real headings in the document outline.
Rhythm: the gaps between the seven landing bands were 24/32/24/16/48/32/16px,
each band setting its own margin, with four different section-header
treatments between them. The page container now owns one gap, and there is one
header pattern. An e2e test asserts the gaps are identical.
Illustration: it kept a fixed light palette in both themes, which left a pale
sky slab as the brightest object on a near-black page, out-shouting the H1 and
the search box. It now reads from --ill-* tokens with a dark re-grade. And the
hero slot ranges from 1.34:1 to 4.9:1 across breakpoints, which no single
composition survives under `slice` — at 860x176 a 540x520 scene shows only its
bottom 110 units, so the schoolhouse was cropped away entirely on phones,
leaving hills and a pin pointing at nothing. There are now two compositions,
each drawn against the crop window its own breakpoint produces, with CSS
showing one. Both are static server-rendered SVG.
The deadline bar renders on the server rather than on hydrate. The effect-based
version needed a reserved height, and one guessed number cannot cover a block
whose supporting line wraps differently at every width — measured, it was short
at all four, shifting the page up to 108px on a phone.
Verified on the built output through an offline render harness (no local
server): real compiled CSS, real rendered markup, four widths, both themes.
tsc clean, 159/159 unit tests, build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LogoMark derived width from a single hardcoded 153/189, taken from mark.png,
while mark-on-dark.png was 98×112. Both call sites size the mark with
height:100%/width:auto, so the rendered size came out right in the end — but
the width/height attributes are the pre-load aspect hint, so the on-dark
artwork reserved the wrong box and reflowed on load. Any future call site
without that CSS would have rendered it stretched outright.
Per-variant attributes would not have fixed it. variant="auto" is a single
<img> whose srcset swaps the file underneath it, so one set of attributes has
to serve both colourways; the only correct fix is for the two files to share
a ratio.
They also had different artwork ratios (0.810 against 0.833) and different
padding, which the review did not reach: the logo changed size slightly
whenever the OS theme flipped. Both are now written onto one 176×208 canvas
with the artwork at the same height, centred, so they are drop-in swappable.
Measured on the built output, header mark, before → after:
light attr 31×38, rendered 30.75×38 → attr 32×38, rendered 32.15×38
dark attr 31×38, rendered 33.25×38 → attr 32×38, rendered 32.15×38
A 2.25px reflow and a cross-theme size jump both become 0.15px of rounding.
The share card reads the same file and had its own hardcoded 45×56, which the
re-crop would have stretched; it is now 47×56, with a comment tying it to the
canvas. The comment on ASPECT records that re-cropping either file means
re-normalising both.
Verified: tsc clean, 159/159 tests, build green, and both files report
naturalWidth 176 × naturalHeight 208 in the browser.
The mark shipped so far was my SVG approximation, and it was wrong: the real
mark is a teardrop pin with a white window and a path flowing out of its base,
carrying three leaves — not a circle with a separate tail.
Both colourways are extracted from the supplied sheet, which has a genuinely
transparent background, so these are the artwork rather than a trace:
public/brand/mark.png teal pin, white window and path, green leaves
public/brand/mark-on-dark.png white pin with the counter knocked through
Two colourways are needed, not one. Rendered against every real ground, the
teal pin holds up on Warm White, white cards, Sand and both dark-theme grounds
— but it vanishes on the teal footer band, where only the white window
survives. The header therefore serves the teal artwork and swaps to the
on-dark artwork for dark-theme viewers through a <picture> source, needing no
JavaScript; the footer forces on-dark, because its band is teal in both
themes.
Everything downstream now derives from those two files: the favicon
(app/icon.png, replacing icon.svg), the touch icon (app/apple-icon.png,
replacing the generated apple-icon.tsx), the three PWA rasters, and the share
card, which reads public/brand/mark.png off disk so it can never drift from
the header. The logo sizing CSS keyed off a square box, which would have
squashed a 153:189 artwork — height now drives and width follows.
The wordmark stays live text in Manrope. The sheet's wordmark is a raster with
visible edge fringing, and the written style guide specifies Manrope; live
text also stays selectable, scales cleanly and recolours with the theme.
KNOWN LIMITATION: the largest instance on the sheet is 153×189. That is ample
for the header at 38px, the favicon and the share card, but short of the 512px
PWA icon, which is upscaled and slightly soft. A vector would fix it and is a
single swap — every consumer goes through components/Logo.tsx or public/brand.
Verified: tsc clean, 159/159 tests, build green, /icon.png and /apple-icon.png
emit as static routes, and the header, footer, share card and all five icons
were rendered and inspected in both themes.
Three review findings.
--status-below-bg carried the raw --coral tuple (249,115,96) instead of the
hue it belongs to (#A9481F → 169,72,31), so "below average" chips rendered a
coral-pink wash behind terracotta text — the one solid/tint pair in the file
whose hues disagreed. Alpha drops 0.15 → 0.12 to match --status-above-bg, so
the two chips read as equal weight; left at 0.15 the darker terracotta tint
takes the label to 4.45:1 on Warm White, under the 4.5 floor. At 0.12 it is
4.86:1 on cards and 4.67:1 on Warm White.
Auditing the whole file for the same class of error turned up one more:
--action-bg was also a --coral tint while --action is the darkened #BE3C27.
It is unused today and the dark-theme value was already correct, but it is
aligned so a future coral ghost button gets a matching label and ground. The
five phase pairs also "mismatch" by the same test and are deliberately left
alone: those pair a pastel ground (sage, sky, mustard, lavender) with a deeper
text hue, which is exactly what the guideline's Chips/Tags show.
The lowercase wordmark had only reached the visual chrome. Every user-visible
occurrence now matches: page title and template, appleWebApp title, authors,
OpenGraph and Twitter titles and siteName, the school page's siteName, the PWA
manifest name and short_name, the footer copyright line, the nav's aria-label,
the Web Share API strings, and the editorial paragraph. Comments in lib/*.ts
and AdmissionsView still say "SchoolCompare"; they describe the codebase rather
than the mark, so they are left to keep this diff to the reported problem.
The comment explaining why the font classes belong on <html> still described
--font-schibsted, which no longer exists. It now names --font-manrope and
--font-inter, and records that the e2e typeface assertion is the only guard —
this failure mode throws nothing and fails no unit test.
Verified: tsc clean, 159/159 tests, build green, and the rendered title is now
"School Admissions Guide | schoolcompare". Status and phase labels re-checked
on their own composited tints over both light grounds — all clear AA.
Implements the supplied brand guideline: the pin-and-leaf mark, the lowercase
schoolcompare wordmark, Manrope for headings and Inter for everything a person
reads a number from, the deep-teal/coral/mustard/sage palette, and the rounder
geometry the guideline asks for.
The landing hero is rebuilt to the concept: a Sand panel carrying the
proposition, the search and a trust line, with the brand landscape bleeding to
the panel's right edge, followed by the four value props.
Concept vs reality — where the guideline describes something this product does
not have, the real feature takes its place and the decision is recorded in a
code comment next to it:
- The Thriving Score is not implemented. Four of its five sub-scores (pastoral,
enrichment, community, facilities) have no source in DfE or Ofsted data, and
its band names are Ofsted's own, which would sit beside the real Ofsted grade
and be indistinguishable from it.
- Guides, About, Favourites and Sign in are not added to the nav — there are no
such pages and no accounts system. Rankings and Admissions, which the
guideline omits, keep their places because they exist.
- The fourth value prop is "Never miss a deadline" rather than "Save & revisit",
which would need shortlisting.
Two deviations from the supplied swatches, both forced by contrast and both
confined to text and fills. Coral #F97360 under a white label measures 2.75:1
against a 4.5 floor, so --action is a darkened #BE3C27 and the original coral
survives as --coral for tints, borders and illustration. Mustard #F2C94C is
1.5:1 as text, so --mustard-ink carries labels while --mustard stays exact for
fills and rings. Every token clears AA on all three light grounds; Sand is the
binding one, so several values sit a step darker than white alone would need.
The guideline is light-only, but the site already ships a dark theme, so it is
rebuilt from the same tokens rather than dropped.
Verified: tsc clean, 159/159 unit tests, production build green, and the
generated share card and app icon render from the new mark. The hero and the
site chrome were rendered from the compiled CSS and the prerendered markup to
confirm Manrope and Inter actually resolve — a font token that silently falls
back has shipped here before.
Full audit across home, search, rankings, admissions, compare, and primary /
secondary / special / no-data school pages, in both themes, measuring computed
styles rather than reading CSS.
Contrast: the status hues were specced against --bg-primary, but they are used
as chip text on their own tint, which sits on cards and secondary surfaces
rather than the page ground. Measured in the wild they were 3.85–4.44:1 —
under AA — on Ofsted badges, delta chips, report-card chips and metric values,
i.e. most of the product's actual signal. Darkened the light hues
(#0e6e66 → #0b625a, #9a5b00 → #7f4a00) and lifted the dark teal
(#3fb3a4 → #4fc0b0) so each clears AA on its own tint, which is the worst case
rather than the easy one. Chart and series ramps follow.
The footer was painting itself with a text token and lettering itself with a
background token: `background: var(--text-primary); color: var(--bg-secondary)`.
That reads correctly in one theme and inverts in the other — in dark mode it
became a light slab at the bottom of a dark page, with amber section headings
at 1.98:1. Added --surface-sunken and its on-* companions, which stay dark in
BOTH themes (deliberately not --surface-inverse, whose whole job is to flip),
and moved the footer onto them. Section headings were also using a status hue
purely as decoration; they are now a muted on-surface token.
The "Open full map" pill was a fixed white background with a themed text
colour, so in dark mode it rendered light violet on white at 1.84:1. Both
sides are token-driven now.
Tabular numerals are now the default for .main rather than per-component
opt-in, with prose opting back out — a handful of figures (miniNatPill,
compareRowVal, factVal) had been missed by the class-name-based pass.
Added a two-theme AA gate over home, rankings and admissions. It waits for
`transition: color` to settle first: an earlier measurement pass read
mid-transition values and reported seven failures that did not exist at rest.
Worth stating plainly — most of what a naive audit flags here is its own
artifact, and the check has to account for that to be worth having.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two follow-ups from watching the staging E2E run and re-auditing the live site.
The typeface gate failed, but the site was right. Staging now serves
"Schibsted Grotesk", "Schibsted Grotesk Fallback", …, -apple-system, sans-serif
— exactly what it should. My assertion was /Times|serif$/, and `serif$` matches
the tail of `sans-serif`, so a healthy stack could never pass. It now checks
the FIRST family in the stack (the only one actually requested) and matches
Times as a whole word. Also asserts running prose resolves to Literata, which
the earlier version never covered.
The Leaflet overrides from the previous fix never took effect. leaflet.css is
imported from a client component, so its chunk loads after globals.css; at
equal specificity (both bare `.leaflet-container`) the later sheet wins.
Verified on the live page: my rule sat at stylesheet index 0, Leaflet's at
index 3, and the attribution link was still #0078A8. Prefixing the overrides
with `html` takes them to 0,1,1, which beats a bare class regardless of load
order — injecting that on the live page turned the background to
rgb(233,232,226) and the link to rgb(88,74,155).
The palette gate would have caught this, except it skipped everything inside
.leaflet-container to avoid flagging OSM tile imagery — which also skipped
Leaflet's own chrome. Narrowed the exemption to .leaflet-tile-pane so the map
controls are now covered.
Also dropped the duplicated family name from the font tokens: next/font
already expands the variable to the family plus its metric-matched fallback,
so the stack was naming Schibsted Grotesk twice. It looked like a safety net
but wasn't — an unresolvable var() with no fallback invalidates the whole
declaration, so the literal after it never gets a turn.
Verified against live staging: body and headings resolve to Schibsted Grotesk,
prose to Literata, the search input is --text-primary rather than pure black,
and the higher-specificity Leaflet rules win when injected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Staging audit of the Cohort identity found the whole site rendering in Times.
Root cause: next/font's variable classes were on <body>, while globals.css
declares --font-display/--font-ui/--font-prose on :root as
`var(--font-schibsted), ...`. A custom property's var() references resolve on
the element that declares it, so at :root --font-schibsted was undefined,
--font-display computed to the guaranteed-invalid value, and every
font-family referencing it fell back. Nothing threw and the build was green —
the only symptom was visual.
Verified the mechanism in a browser both ways round: class on <body> gives an
empty token and a Times body font; class on <html> resolves to Schibsted
Grotesk. The classes now sit on <html>.
Two colour escapes from the same audit:
* Form controls don't inherit font or colour from their parent, so the omni
search input and the map's "Open full map" button rendered in the system
font at pure black. Nearly invisible against --text-primary in light mode
and completely invisible on the dark ground. Added a base inherit rule.
* Leaflet ships its own palette — a #ddd tile backdrop, #333 attribution text
and a #0078A8 link blue that was the most saturated colour anywhere on the
site. The map chrome now uses tokens; the tiles stay as OSM renders them.
Three e2e gates added, because the existing suite passed while the site was
entirely in Times:
* the font tokens resolve to a non-empty value and the applied family is
Schibsted, not a serif fallback
* no visible element renders in the browser's default black
* every rendered colour comes from the token palette — the manual audit,
turned into a gate
Also made the og:image check environment-relative: metadataBase pins canonical
URLs to the production host, so the absolute URL pointed off-environment when
the suite ran against staging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review follow-up on #86.
The blind coral -> brand rename recreated the exact collision this PR set out
to remove: coral had been both the primary CTA and the "below average" signal,
so every negative indicator followed --primary onto iris. Sixteen rules moved
back onto the status ramp — delta chips, trend-down arrows, progress-negative
values, statusBad, chipBad/badgeBad, and the urgent deadline chips.
The Ofsted scale had also lost its worst step, with grade 4 landing on brand
while 1-2 were teal and 3 was amber. It now escalates by weight rather than by
reaching for another hue: a tinted amber chip for "requires improvement", a
solid amber one for "inadequate" (5.1:1 light, 7.7:1 dark). Report-card grade 5
follows the same rule.
globals.css now describes status as valence — teal above/good, amber
below/needs-attention — which is what it has to mean for an urgent deadline,
rather than the narrower "comparison point only" the first draft claimed.
On the share-card fonts: /opengraph-image is prerendered, so the font read
happens in the builder stage where assets/ exists, and the baked PNG ships
inside .next/standalone/.next/server/app/. File tracing independently places
the fonts at .next/standalone/assets, which the existing standalone COPY
carries to /app/assets. So the reported ENOENT doesn't occur — but it depends
on the tracer resolving a runtime join(), and a miss would be a silent 500
rather than a build failure. Declared outputFileTracingIncludes for the route
and made the Dockerfile COPY explicit so neither is left to inference.
Also repointed the immutable Cache-Control rule from the deleted favicon.svg
to app/icon.svg, where it was caching a 404.
Verified: tsc clean, 159/159 tests, clean rebuild prerenders all three image
routes with the fonts present in standalone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the direction agreed from the identity board: Route C ("Cohort")
with the paper ground from C1, the Schibsted Grotesk / Literata pairing from
C2, and the iris accent from C3. Dark theme is in scope from the start rather
than retrofitted.
The audit found three things wrong beyond taste:
* No brand asset set. og:image was absent entirely, so every link shared into
a class WhatsApp group rendered as a bare grey card. apple-touch-icon pointed
at an SVG, which iOS ignores, and the manifest shipped no PNGs, so Android
installs had no icon. The header mark and the favicon had also drifted into
two different logos.
* No colour discipline. --primary and --trend-down were the same coral, so the
main CTA and "below average" shared a hue. 58 distinct hex values were spread
across component CSS, and the chart palette was still Chart.js's stock demo
colours.
* A dark theme that was declared but never built — themeColor announced a dark
variant with no dark styling behind it.
What changed:
Colour now has exactly three jobs that never borrow each other's hues: brand
(iris) for interactive and identity, status (teal/amber) for above/below a
comparison point, and phase for categories. Teal/amber rather than green/red
keeps the above/below signal readable for every form of colour blindness.
Every chromatic literal in component CSS is now a token, and the JS-painted
surfaces (Chart.js, Leaflet) read the tokens through lib/theme so they follow
the theme instead of ignoring it.
The mark is the five-bar cohort spread — the same object as the distribution
strip inside a school row, built from opacity steps so it inverts cleanly.
components/Logo.tsx is the single source; the favicon, apple-icon and share
card all derive from its geometry.
globals.css drops 123 dead global classes left over from the vanilla-JS app
(only the btn family, .skip-link and .main were still referenced), along with
the noise overlay. It also gains prefers-reduced-motion support, which was
missing entirely, and a type scale so the 54 ad-hoc font sizes have somewhere
to converge.
Verified: tsc clean, 159 unit tests pass, production build succeeds and
prerenders /icon.svg, /apple-icon and /opengraph-image. Three e2e journeys
added for the asset set, the themeColor/background match, and the dark theme
actually repainting — all silent failures that nothing on the page reveals.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server/client split moved the header and sticky nav markup into
SchoolDetailShell, but their responsive rules stayed behind in
schoolSections.module.css. CSS Modules hash class names per file, so those
rules stopped matching entirely: on mobile the header details block never
collapsed ("Show all details" e2e journey failed), the toggle itself never
appeared, and the section-nav mobile swap (menu button in, link row out)
never applied.
Ports the stranded blocks into SchoolDetailShell.module.css verbatim: the
768px header block (stacked header, wrapped meta pills, details toggle +
collapse) and the 640px section-nav blocks (scroll-fade mask, bottom-sheet
panel, control swap). Adds a jest guard that every class a components/school
component references is defined in the stylesheet that component imports —
it fails on the three classes that had no rule at all here
(headerDetailsOpen, atEnd, sectionNavBackLabel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Leftover from the mechanical extraction: the shell still called
computeSchoolFlags() and derived isReportCard / ofstedInspectedDate /
oeifAreas / oeifAllSameGrade / deprivationDesc / primaryAvg / secondaryAvg on
every render, duplicating work page.tsx already does. None of those values
were referenced in its JSX anymore -- that logic moved to the section
composers.
The chrome needs only four locally-derived values (latestResults, phase,
isAllThrough, hasLocation), all one-liners over props it already owns.
Removing them made seven props dead, which TypeScript caught at both call
sites: absenceData, ofsted, admissions, admissionsHistory, deprivation,
finance and nationalAvg now go straight to the section composers and never
reach the client component. The shell's surface is down to schoolInfo,
yearlyData, census, navItems and children.
No behaviour change: 155 tests pass and the characterization tests remain
byte-identical to the commit that introduced them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two journeys for what the split actually changed: the admissions toggle (the
one client island inside an otherwise server-rendered section) and the sticky
nav resolving to server-rendered sections. The nav is client-rendered from a
server-computed list while the sections render on the server, so a mismatch
between the two halves would only show up in a real browser.
Existing journeys already cover all-through and special-school pages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
useSchools, useFilters, useMetrics and useSchoolDetails were imported by
nothing and were the only consumers of swr. All data fetching goes through
lib/api.ts on the server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
page.tsx now composes the sections and passes them through SchoolDetailShell
as children, so ~1,300 lines of static markup stop shipping as client
JavaScript. The shell keeps what is genuinely interactive: back link, header
reveal, hero map, compare CTA, sticky nav and scroll-spy.
The scroll-spy already located sections via document.getElementById, so it
works unchanged against server-rendered children.
Charts needed a client wrapper: next/dynamic with ssr:false is illegal in a
Server Component, so components/school/charts.tsx is the boundary that keeps
Chart.js (64 KB gz) lazy and browser-only.
Measured on this build:
- school route client chunk: 8 KB gz (33 KB raw)
- total static JS across all chunks: 380.6 -> 350.7 KB gz
- section markup is absent from every client chunk ("Got their first choice",
"Ofsted reports", "Most deprived" etc. all return 0 hits); shell strings
still present, as expected
- shared baseline unchanged at 172 KB gz -- out of scope, as designed
The 14 characterization tests pass byte-identical to the commit that
introduced them. Only the render helper changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves ~1,300 lines of section markup out of the two client views into
components/school/, mirroring the components/compare/ layout. Twelve section
components plus shared primitives, all server components. The only client
file is AdmissionsViewToggle, which owns the hidden/aria-pressed state and
receives both views as server-rendered children.
JSX was extracted mechanically rather than retyped, so the markup the CSS
modules depend on is verbatim.
Sharing follows measured similarity, not assumption:
- Finances (91%) shared. The secondary premises-cost card is gated behind a
prop so primary pages are unchanged; enabling it is a one-line follow-up.
- Ofsted (80%) shared, but behind a variant prop. The headline similarity hid
a real fork: on a school with no overall grade the primary page shows a
"Not rated" badge while the secondary shows a four-area OEIF panel, and the
disclaimer copy differs. Both preserved exactly; reconciling them is a
human decision, not a side effect of a move.
- Admissions (14%) and History (40%) kept separate.
Not yet wired up -- the old views still render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Builds components/school/schoolSections.module.css from the primary module,
plus 35 secondary-only rules, so shared section components have one stylesheet.
Three categories of difference, handled separately so nothing changes visually:
- 7 incidental-drift properties unioned in (overflow-wrap, word-break,
min-width, user-select) -- fixes that landed on one page and were never
back-ported. Also fixes latent long-school-name overflow on the page that
lacked them.
- 4 genuinely visual class differences kept as explicit variants:
genderBar, heroStatValue, genderSplitBoys, genderSplitGirls. font-weight on
the genderSplit pair was reclassified from drift to visual -- unioning it
would have bolded the primary page.
- 17 rules that exist only in the secondary stylesheet but target shared class
names (.card, .sectionTitle, .metricCard). Applied flat these would restyle
the primary page, so they are scoped under .secondaryScope, which only
SecondarySchoolSections will carry.
Audited: every rule in both original stylesheets is a subset of the merged one
(0 MISSING, 0 LOST), excluding the documented variants.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pure data-shape logic moves out of the client components so page.tsx can
compute the section list on the server without importing them.
The secondary page is not a variant of the primary one -- different section
ids (gcse, wellbeing), different flags, and History gated on more than one
year rather than at least one -- so it gets its own computeSecondaryFlags and
buildSecondaryNavItems rather than bending a shared function.
Adds 16 unit tests covering the all-through and special-school branches,
previously reachable only through a full component render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both detail views fetched /api/national-averages in a useEffect, so the
England-comparison deltas popped in after hydration and every section that
uses them was pinned to the client. The page now fetches it in parallel with
the school details (backend-cached 1h, degrades to null) and passes it down.
Removes one client round-trip per detail page and unblocks the section
extraction.
Characterization tests pass unmodified; only the render helper changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 characterization tests covering Ofsted (both OEIF and Report Card layouts),
KS2 results with the England delta, KS4 Attainment 8 / Progress 8 / EBacc,
all-through dual rendering, special-school comparison suppression (PR #70),
the admissions year/trend toggle, and conditional section rendering.
All rendering goes through renderSchoolDetail(), the single seam the
server/client split is allowed to change. The assertions must survive the
refactor unmodified.
Excludes __tests__/support from testMatch: it holds fixtures and helpers, not
suites, and the glob was failing them as empty test files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jsdom provides neither IntersectionObserver nor scrollTo; both detail views
use them. Adds four fixtures (primary, secondary, all-through, special)
covering the branches the characterization tests will pin.
SchoolResult carries 57 required nullable fields, so fixtures are built from
an all-null base row and override only what they exercise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine tasks mapping to the spec's commit sequence, each ending in an
independently testable deliverable.
Also corrects the spec's section-sharing table: measured similarity shows
Admissions (14%) and History (40%) are not shareable between the two views,
only Ofsted (80%) and Finances (91%). This costs no bytes -- the win comes
from sections being server components, not from sharing them -- and cuts the
diff and regression risk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Planning surfaced a gap: the two detail views' CSS modules share 79 class
names, 22 with differing rules, 16 of those used in section markup by both
views. Shared sections cannot use one stylesheet without visual change.
Resolution: union the 14 incidental-drift differences (defensive overflow
properties never back-ported between the views), and keep the 2 genuine
visual differences (.genderBar, .heroStatValue) as distinct classes behind a
variant prop. Adds an isolated CSS-merge commit so a visual regression
bisects to the merge rather than a JSX move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design for splitting SchoolDetailView (65KB) and SecondarySchoolDetailView
(46KB) into server section components behind a small client shell, following
the existing components/compare/ decomposition pattern.
Key constraint: a server component imported by a client component becomes
client, so sections are composed in page.tsx and passed through the shell as
children. Moving /api/national-averages server-side is a prerequisite, since
the England-comparison deltas feed most sections.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Religious character now sits with the other labelled header details (near
County / Constituency) as "Religious character: <denomination>" ("None" for
Does not apply / None), instead of as an identity chip.
On mobile/tablet (≤768px) the header details block was hidden outright; it's
now collapsed behind a "Show all details" toggle that reveals the full block
(headteacher, website, pupils, trust, phone, religious character, county,
constituency). Applied to both the primary and secondary detail views for
parity. Extends the e2e journey with the mobile toggle behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The graceful-degradation fallback keys off the column named in a Postgres
UndefinedColumn error, but the matcher only stripped an `s.` alias. The two
new dim_location columns (county, parliamentary_constituency) are selected
via the `l.` alias and Postgres reports them unquoted as
"column l.county does not exist" — which the old regex failed to match at
all, returning None.
If dim_school is rebuilt (telephone/nursery present) but dim_location is not
yet (county/parliamentary_constituency missing) — plausible since they are
independently-rebuilt dbt models — the fallback branch never matched and
load_school_data_as_dataframe() returned an empty DataFrame, showing zero
schools sitewide instead of degrading those columns to NULL.
Generalise the alias prefix to `\w+\.` and cover the l.-qualified case in
tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the chip was hidden for "Does not apply"/"None". Show it in all
cases, rendering "None" for those two sentinel values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add seven school-identity fields to the detail header (both primary and
secondary views): age range, religious character, nursery and sixth-form
indicators as chips; telephone (tel: link), county and parliamentary
constituency as header details. religious_denomination, age_range and
has_sixth_form were already served; telephone, nursery_provision, county
and parliamentary_constituency are newly wired through the marts query
(with a NULL fallback for un-rebuilt marts, mirroring has_sixth_form) and
the school_info API response.
Remove three UI sections the backend never populated (always null): Year 1
Phonics, the SEN "types of additional needs" breakdown, and the average
class-size card — along with their now-dead props, route plumbing, and the
SenDetail/Phonics types + class_size_avg field.
Extend the e2e detail journey to assert the Phonics section is gone and the
new header fields render when the record carries them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adding a school with no performance data to a comparison made every
school's trend line disappear until that school was removed.
Root cause: /api/compare returns such a school with a single phantom
yearly_data row (the dim_school LEFT JOIN) whose year is null. In
buildCompareChart, Math.trunc(null) is 0, so the axis was seeded at
year 0; fillAcademicYears then walked 0, 101, 202, … and hit its
50-step cap long before reaching the real years, leaving every
school's series mapped entirely to null.
Fix: ignore yearly rows without a real numeric year when building the
axis and the per-school year map. Regression test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace X-Frame-Options: SAMEORIGIN with a CSP frame-ancestors directive
so analytics.schoolcompare.co.uk (Umami heatmap/recorder) can embed the
site while all other origins stay blocked. X-Frame-Options cannot allow a
specific sibling subdomain (ALLOW-FROM is deprecated/ignored by modern
browsers), so frame-ancestors is the correct replacement.
Also update the nginx snippet in DEPLOYMENT.md to match, so the reverse
proxy doesn't re-inject a conflicting X-Frame-Options header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The trend chart was capped at 280px tall on desktop, squishing the
lines together and making them hard to read. Raise the base
.chartContainer height to 380px in both the primary and secondary
detail views.
Mobile is unaffected: the base value is already overridden to
height:auto by the existing max-width:768px query, where
PerformanceChart's own .chartWrapper carries the canvas height.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Adds the Umami session recorder (recorder.js) that powers heatmaps,
alongside the existing analytics tracker. Loaded via next/script with
the same data-domains guard as the main tracker so it only fires on the
production hostnames — staging runs the same image under a different
host and must not feed the heatmap.
Cookieless: recorder.js uses Umami's server-side session model (no
cookie or local storage on the device), so it adds no new PECR
consent trigger.
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
The KS2 SATs chart drew a single national-average line spanning the
full height of each subject's chart area, positioned at the national
*expected* value. But the area stacks two bars — Expected and Exceeding
— and the higher-standard/greater-depth national is a very different,
much lower figure (e.g. reading higher standard ~29% vs expected ~75%).
So the line crossed the Exceeding bar at the wrong place, making every
school's exceeding result look far below national when it wasn't.
The per-subject higher-standard nationals were already computed in the
fact_ks2_national_averages mart; they just weren't serialized. Fix:
- backend: add reading_high_pct, writing_gd_pct (writing = greater
depth) and maths_high_pct to the national-averages payload.
- SchoolDetailView: pass a nationalExceedingPct per subject, mapping
writing to the greater-depth figure.
- SatsChart: replace the single full-height line with a national marker
on each bar's own track (coral tick + "nat X%" in the bar header), so
Expected and Exceeding each sit against the correct benchmark.
KS2 only; the secondary Attainment 8 chart already uses one line for
one measure and is untouched.
Verified: tsc --noEmit, next build, and backend pytest (national
averages marts, incl. a new test guarding the per-subject nationals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
The selective-school registration and faith-school SIF warnings were
added as prose in the criteria steps, where they read as ordinary
sentences and were easy to skim past. Pulled them into structured
callout boxes with an amber "note" treatment (using the page's own
gold accent tokens), a warning glyph, and a bold lead-in — so the two
deadlines parents most often miss actually stand out.
- Added a `callouts` field to Step; render as a callout group between
the body and the tool link (both primary and secondary timelines).
- Reverted the warning text back out of the step bodies so it isn't
duplicated.
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Follow-up audit of the rankings and admissions pages.
Admissions (AdmissionsView.tsx):
- National Offer Day no longer claims offers publish "from 12:01 am";
release times are set per-council (often late afternoon, some
overnight), so it now tells parents to check their council's page.
- Secondary preference count no longer states a flat "up to six"
(that's London/Pan-London); most LAs allow three to six. Mirrors the
hedge the primary step already used.
- Added the two deadlines that most often catch parents out: selective
schools' separate entrance-test registration (months earlier), and
faith schools' supplementary information form sent direct to the
school. Covered in both the primary and secondary criteria steps.
- Reworded the equal-preference tip so it's precise: order is the
tie-break among schools you qualify for (you get the highest-ranked
one), not irrelevant.
Rankings (RankingsView.tsx):
- Subtitle "Top-performing schools by X" -> "Schools ranked by X", so
it isn't nonsensical for context/equity/absence metrics.
- KS2 progress isn't published for 2023/24 or 2024/25 (no KS1
baseline). Selecting a primary progress metric on a recent year used
to dead-end on a generic "No rankings found". Added a scoped caveat
to the progress hint and an explanatory empty-state (primary only —
secondary Progress 8 is published).
Added an admissions smoke journey (static content, stable milestones).
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Two internal-consistency fixes on the "What you'll see" mock:
- The Attainment 8 bar had drifted from the numbers beside it (fill 68%
/ line 57% vs a 54.8 score and 45.9 national avg). Restored the mock's
original 1:1 value-to-percent mapping: fill 54.8%, national line 45.9%.
- The report card grouped "Safeguarding: Met" in the same five-point
list as the graded areas, but safeguarding is a separate binary
met/not-met judgement under the real framework. Moved it below the
list behind a divider, with an outlined chip and a "separate check"
label so it no longer reads as a graded area.
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Follow-up to the accuracy pass — the medium-severity wording issues
from the same expert review:
- KS2 "Exceeding" isn't a DfE term. The above-expected level is the
"higher standard" for the reading and maths tests and "greater depth"
for teacher-assessed writing. Labelled per subject, and brought the
illustrative reading value down from an implausible 73% (national
higher standard is ~29%) to a realistic strong-school figure.
- Attributed inspection outcomes to Ofsted, not only the DfE (Ofsted is
a separate non-ministerial department).
- Hero: "2026/2027 admissions results" -> "the 2026/27 admissions
round" (admissions produce offers, not results, and we don't hold
per-school offer outcomes).
Tightened .miniRowHead so the longer "Greater depth" label stays on one
line and the three subject columns keep their bars aligned (the visual
is desktop-only; hidden on mobile).
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Homepage marketing content had drifted from both the current
education regime and the app's own school pages. An expert accuracy
review surfaced five issues (3 critical, 2 high):
- Ofsted card showed a single "OUTSTANDING" word-grade. Overall grades
were abolished (Sept 2024) and inspections now produce a report card
on a five-point scale (from Nov 2025). Rebuilt the card as a report
card with per-area ratings + stand-alone safeguarding, matching what
CompareOfsted actually renders on a school page.
- Attainment 8 "National avg 50.2" was wrong; the real 2024/25 figure
is 45.9. Corrected the benchmark and the illustrative school value.
- Compare mock featured a KS2 "Reading progress" score, but KS2
progress isn't published for 2023/24 or 2024/25 (no post-COVID KS1
baseline). Swapped for Higher standard (RWM), which is published.
- Editorial called it "UK school performance data"; the data is
England-only. Now matches the hero and page metadata.
- Softened the "40-page inspection report" exaggeration.
Landing-page copy isn't covered by e2e journeys, so no test changes.
Verified with tsc --noEmit and next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Drop the note's `max-width: 72ch`, which left it stopping well short of the
section's right edge. It now fills the results-section container like the
subtitle paragraph above it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
The grade boxes rendered inconsistently: Safeguarding "Met" was a small
0.8125rem inline pill while grade values ("Strong") were 1.25rem full-width
chips, and because category labels wrap to one or two lines each value sat at
a different vertical position. Two-line values like "Expected standard" also
produced a taller box.
Add a scoped `.gradeGrid` modifier (Report Card + OEIF grids in both the
primary and secondary detail views) that reserves two label lines so every
chip shares a baseline, and gives all value chips one font size, padding and
min-height — long text wraps inside an equal-height chip via max-width:100%.
Scoped rather than touching the shared `.metricCard`/`.metricValue` used by
the finance, phonics and admissions sections.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Three review points on the special-schools change:
1. Copy accuracy — the context note said "Its pupils have special educational
needs" for every isSpecialSchool() match, but the helper also matches pupil
referral units and alternative provision, whose pupils are educated outside
a mainstream setting (e.g. after exclusion) and are not necessarily SEND.
Extracted a shared <SpecialSchoolNote> with type-aware copy: SEND wording
only for genuine special schools; PRUs/AP get their own accurate wording.
2. Same-school trend was conflated with the England comparison — SchoolRow's
year-over-year trend arrow (and the school's own figure) were gated on the
same flag that drops the vs-England delta, hiding a still-meaningful trend
for special schools with real data. Split the two: the school's OWN RWM
figure + trend show whenever there's a real value (special schools
included; only a placeholder all-zero row is hidden); only the vs-England
delta is additionally dropped for special/PRU/AP. Mirrored in
SecondarySchoolRow (own Attainment 8 shown; only the vs-LA delta dropped).
3. De-duplicated the .specialNote CSS (was copy-pasted between the two detail
view module files) into SpecialSchoolNote.module.css, owned by the shared
component so it can't drift.
New SpecialSchoolNote unit tests assert SEND wording for special schools and
NOT for PRUs/AP. tsc clean; 112/112 unit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Review feedback: the rankings/search rows treated any exactly-0 attainment as
not comparable regardless of school type, so a genuine 0 at a mainstream
school rendered as "—" (looks like missing data) and dropped its delta —
stricter and inconsistent with SchoolDetailView's placeholder check.
- SchoolRow: match SchoolDetailView's ks2Placeholder signature — suppress only
when ALL of RWM + reading + writing + maths are 0 (special/suppressed
signature), not on a bare rwm === 0. A genuine 0% combined (some pupils met
individual subjects but not all three) is not all-zero, so it stays
comparable and shows its real figure + trend.
- SecondarySchoolRow: Attainment 8 is a single 0–80 score with no subject
breakdown to form an all-zero signature, so key off establishment type only
(drop the bare att8 === 0 guard). A genuine (if extreme) 0.0 shows its value
+ LA delta.
- For consistency, apply the same to the detail views: drop the bare
attainment_8_score === 0 guard (KS4 keys off isSpecial only); KS2 keeps the
all-four-subjects-zero placeholder signature.
Special schools / PRUs / AP are still handled via isSpecialSchool everywhere.
tsc clean; 108/108 unit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
Special schools, PRUs and alternative provision teach pupils with SEND who
sit the same KS2/KS4 assessments but very few reach the mainstream "expected
standard". Their headline attainment is therefore ~0% (or a very low
Attainment 8), and the site was comparing that to the England average and
painting it red — e.g. Greenmead School (a community special school) rendered
as "0.0% — −62 pts below England average" with three 0% red SATs bars. That
portrays a special school as catastrophically failing against a benchmark
that doesn't fit it.
Add a shared `isSpecialSchool()` helper (detects every DfE special-school
establishment type — all contain "special" — plus PRUs / alternative
provision) and drop the mainstream England comparison + "below" framing for
these schools across every surface:
- Detail (primary + secondary): a plain-English context note explaining the
school is special and why the comparison isn't shown; England-average delta
chips, "England avg" hints, the SATs national markers, the Attainment-8
"vs national" bar and the trend chart's England overlay are all suppressed.
An all-zero placeholder SATs row hides the (empty) subject bar chart and the
"why is combined lower" bridge.
- Rankings / search rows (primary + secondary): the mainstream RWM / Attainment
8 stat shows "—" with no "vs national" delta, instead of "0% · −62 vs
national".
- Compare: special schools' attainment values are dropped (no misleading dot
at 0% / no "Below England average" chip); progress banding, which IS a fair
measure for special schools, is kept.
Belt-and-braces zero-guard: a whole-row zero attainment (special or a
suppressed cohort) is also treated as not-comparable, while a legitimate
single 0 (e.g. 0% exceeding at a mainstream school) stays comparable.
Tests: new isSpecialSchool unit tests (every DfE special type matched, no
mainstream false positives); an e2e journey asserts Greenmead shows the
special-school note and no England-average comparison. tsc clean; 108/108 unit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
[ 3229ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13147ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 29488ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 183884ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 193559ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 213136ms] [WARNING] Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options @ https://schoolcompare.co.uk/_next/static/chunks/b6104424f97d1001.js:0
[ 216340ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/146678-welland-primary-school:0
[ 3260ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13186ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 155792ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=B91+3:0
[ 165424ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=B91+3:0
[ 3144ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13088ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 23226ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 33141ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 3090ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13086ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 3320ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 13262ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 31275ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 167053ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 3489ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=136916&metric=attainment_8_score:0
[ 13346ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=136916&metric=attainment_8_score:0
[ 67ms] [WARNING] Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options @ https://schoolcompare.co.uk/_next/static/chunks/b6104424f97d1001.js:0
[ 3111ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/146678-welland-primary-school:0
[ 13084ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/146678-welland-primary-school:0
[ 70899ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/146678-welland-primary-school:0
[ 3297ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 13095ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 3677ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 13096ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 480177ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 4073ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=136916&metric=attainment_8_score:0
[ 13162ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=136916&metric=attainment_8_score:0
[ 3410ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct:0
[ 13266ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct:0
[ 41131ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct:0
[ 49979ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 59609ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 66865ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 77770ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161%2C113105:0
[ 87487ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161%2C113105:0
[ 213040ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 222647ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 240300ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161:0
[ 248166ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161%2C136916:0
[ 257801ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?metric=rwm_expected_pct&urns=142161%2C136916:0
[ 270504ms] [WARNING] Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options @ https://schoolcompare.co.uk/_next/static/chunks/b6104424f97d1001.js:0
[ 3428ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C136916&metric=rwm_expected_pct:0
[ 13145ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C136916&metric=rwm_expected_pct:0
[ 61ms] [WARNING] Deprecated API for given entry type. @ https://analytics.schoolcompare.co.uk/script.js:0
[ 3522ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C136916&metric=rwm_expected_pct:0
[ 13114ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C136916&metric=rwm_expected_pct:0
[ 3857ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C113105&metric=rwm_expected_pct:0
[ 13294ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C113105&metric=rwm_expected_pct:0
[ 73049ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C113105&metric=rwm_expected_pct:0
[ 95050ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C113105%2C124613&metric=rwm_expected_pct:0
[ 104770ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/compare?urns=142161%2C113105%2C124613&metric=rwm_expected_pct:0
[ 3222ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 13105ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 47024ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 129393ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull:0
[ 138906ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull:0
[ 173757ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 183459ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 196217ms] [WARNING] Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options @ https://schoolcompare.co.uk/_next/static/chunks/b6104424f97d1001.js:0
[ 200016ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/148084-lady-katherine-leveson-church-of-england-primary-school:0
[ 209230ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/148084-lady-katherine-leveson-church-of-england-primary-school:0
[ 215436ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 225403ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 260204ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/104049-sharmans-cross-junior-school:0
[ 270092ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/104049-sharmans-cross-junior-school:0
[ 290140ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/104049-sharmans-cross-junior-school:0
[ 1476417ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 1486368ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 1506402ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?local_authority=Solihull&metric=rwm_expected_pct:0
[ 3180ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 7050ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 13109ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 23380ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 3354ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13301ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 71ms] [WARNING] Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options @ https://schoolcompare.co.uk/_next/static/chunks/b6104424f97d1001.js:0
[ 3407ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=B91%203DL:0
[ 13281ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=B91%203DL:0
[ 33311ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=B91%203DL:0
[ 3178ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 9456ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?postcode=B91+3DL&radius=1:0
[ 19269ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?postcode=B91+3DL&radius=1:0
[ 3422ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 13307ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 33265ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[15619214ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 3300ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13157ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 3263ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 13184ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 3142ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 13108ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/1944d035eb1dfa50.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings:0
[ 5101581ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/8903db4d490d6a65.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 5101581ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 3285ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 13107ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 33154ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 37100ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/:0
[ 3253ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 9999ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/?search=Welland+Primary:0
[ 3316ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 13172ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/school/136916-the-castle-school:0
[ 3218ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?metric=rwm_high_pct:0
[ 13206ms] [WARNING] The resource https://schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://schoolcompare.co.uk/rankings?metric=rwm_high_pct:0
[ 3633ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=101038%2C101022%2C101036&metric=rwm_expected_pct:0
[ 13405ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=101038%2C101022%2C101036&metric=rwm_expected_pct:0
[ 336ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 338ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 351ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 3412ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 10491ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 13493ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 25526ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 28528ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 55600ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 58602ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 185ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 204ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 231ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/national-averages:0
[ 3299ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 10242ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 13245ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 25282ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 28285ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 215055ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 218057ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 260123ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/compare?urns=101038,101022:0
[ 263125ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 3573ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 13319ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 166ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3388ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 3248ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101038%2C101022&metric=rwm_expected_pct:0
[ 95ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://stx.schoolcompare.co.uk/search?search=primary:0
[ 3287ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/search?search=primary:0
[ 3271ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/:0
[ 8281ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 11313ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 3730ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=137306%2C138690&metric=rwm_expected_pct:0
[ 33217ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=137306%2C138690&metric=rwm_expected_pct:0
[ 137ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3227ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 7230ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 3272ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=137306%2C138690%2C139016&metric=rwm_expected_pct:0
[ 9317ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=137306%2C138690%2C139016&metric=rwm_expected_pct:0
[ 3495ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101037%2C101026%2C101014%2C101038%2C101022&metric=rwm_expected_pct:0
[ 33343ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=101037%2C101026%2C101014%2C101038%2C101022&metric=rwm_expected_pct:0
[ 166ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3265ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 33242ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 210ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3561ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?postcode=B1%201BB:0
[ 33283ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?postcode=B1%201BB:0
[ 320ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3662ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?postcode=B1%201BB:0
[ 10839ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?postcode=B1%201BB:0
[ 16913ms] TypeError: r.current?.requestFullscreen is not a function
at https://stx.schoolcompare.co.uk/_next/static/chunks/a94dcea45b9ebb2c.js:1:25166
at sY (https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:160839)
at https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:166727
at tD (https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:29334)
at s3 (https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:162072)
at fC (https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:198038)
at fN (https://stx.schoolcompare.co.uk/_next/static/chunks/69be39811437728d.js:1:197860)
at eval (eval at <anonymous> (eval at evaluate (:303:30)), <anonymous>:4:16)
at eval (eval at evaluate (:303:30), <anonymous>:4:50)
[ 213ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://stx.schoolcompare.co.uk/api/la-averages:0
[ 3418ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 33266ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[242896019ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[257299531ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/?search=primary:0
[ 3717ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 13359ms] [WARNING] The resource https://www.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://www.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3942ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 131774ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362&metric=rwm_expected_pct:0
[ 419746ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362&metric=rwm_expected_pct:0
[ 3664ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3236ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3479ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3334ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3462ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3520ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=136276%2C137086%2C102052&metric=attainment_8_score:0
[26254907ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=136276%2C137086%2C102052&metric=attainment_8_score:0
[ 3498ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3382ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 3562ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 84848ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ :5
[ 122267ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=reading_expected_pct:0
[ 3424ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=136276%2C137086%2C102052&metric=attainment_8_score:0
[ 3313ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=136276%2C137086%2C102052&metric=attainment_8_score:0
[ 3511ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C136276&metric=rwm_expected_pct:0
[ 3364ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C136276&metric=rwm_expected_pct:0
[ 3588ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
[ 49500ms] [WARNING] The resource https://stx.schoolcompare.co.uk/_next/static/chunks/14a927d6c03d3a7e.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://stx.schoolcompare.co.uk/compare?urns=138690%2C145362%2C100140&metric=rwm_expected_pct:0
- link "Skip to main content" [ref=f1e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f1e3]:
- link "SchoolCompare home" [ref=f1e5] [cursor=pointer]:
- /url:/
- img [ref=f1e7]
- generic [ref=f1e11]:SchoolCompare
- navigation "Main navigation" [ref=f1e12]:
- link "Search" [ref=f1e13] [cursor=pointer]:
- /url:/
- img [ref=f1e15]
- generic [ref=f1e18]:Search
- link "Compare" [ref=f1e19] [cursor=pointer]:
- /url:/compare
- img [ref=f1e21]
- generic [ref=f1e24]:Compare
- link "Rankings" [ref=f1e25] [cursor=pointer]:
- /url:/rankings
- img [ref=f1e27]
- generic [ref=f1e28]:Rankings
- link "Admissions" [ref=f1e29] [cursor=pointer]:
- /url:/admissions
- img [ref=f1e31]
- generic [ref=f1e34]:Admissions
- main [ref=f1e35]:
- generic [ref=f1e36]:
- heading "Every school in England, compared." [level=1] [ref=f1e38]:
- text:Every school in England,
- emphasis [ref=f1e39]:compared.
- generic [ref=f1e42]:
- searchbox "School name or postcode" [ref=f1e43]
- button "Search" [ref=f1e44] [cursor=pointer]
- button "Schools near me" [ref=f1e47] [cursor=pointer]:
- img [ref=f1e48]
- text:Schools near me
- generic [ref=f1e51]:
- generic [ref=f1e52]:
- generic [ref=f1e53]:Key admissions deadlines
- link "Full admissions guide →" [ref=f1e54] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e55]:
- generic [ref=f1e56]:
- generic [ref=f1e57]:Secondary · Deadline
- generic [ref=f1e59]:121days
- generic [ref=f1e60]:Secondary applications close
- generic [ref=f1e61]:Sat, 31 October 2026
- generic [ref=f1e62]:
- generic [ref=f1e63]:Primary · Deadline
- generic [ref=f1e65]:197days
- generic [ref=f1e66]:Primary applications close
- generic [ref=f1e67]:Fri, 15 January 2027
- generic [ref=f1e68]:
- generic [ref=f1e69]:Secondary · Offer Day
- generic [ref=f1e71]:242days
- generic [ref=f1e72]:Secondary National Offer Day
- generic [ref=f1e73]:Mon, 1 March 2027
- generic [ref=f1e74]:
- generic [ref=f1e75]:Primary · Offer Day
- generic [ref=f1e77]:288days
- generic [ref=f1e78]:Primary National Offer Day
- generic [ref=f1e79]:Fri, 16 April 2027
- generic [ref=f1e80]:
- generic [ref=f1e81]:Start exploring
- generic [ref=f1e82]:
- link "Top-rated primary schools" [ref=f1e83] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f1e85] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f1e87] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f1e89]:
- generic [ref=f1e90]:
- heading "What you'll see on every school" [level=2] [ref=f1e91]
- generic [ref=f1e92]:Primary or secondary — the page adapts to the phase
- generic [ref=f1e93]:
- generic [ref=f1e95]:
- generic [ref=f1e96]:Performance
- generic [ref=f1e97]:Results against the national average
- paragraph [ref=f1e98]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f1e100]:
- generic [ref=f1e101]:Judgement
- generic [ref=f1e102]:Ofsted at a glance
- paragraph [ref=f1e103]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f1e105]:
- generic [ref=f1e106]:Compare
- generic [ref=f1e107]:Side-by-side shortlists
- paragraph [ref=f1e108]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f1e110]:
- generic [ref=f1e111]:
- generic [ref=f1e112]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f1e113]
- paragraph [ref=f1e114]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f1e115]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f1e116]:
- heading "Coverage at a glance" [level=3] [ref=f1e117]
- generic [ref=f1e118]:
- generic [ref=f1e119]:Schools covered
- generic [ref=f1e120]:24,000+
- generic [ref=f1e121]:
- generic [ref=f1e122]:Local authorities
- generic [ref=f1e123]:"182"
- generic [ref=f1e124]:
- generic [ref=f1e125]:Phases
- generic [ref=f1e126]:Primary & Secondary
- generic [ref=f1e127]:
- generic [ref=f1e128]:Latest results year
- generic [ref=f1e129]:2024/25
- generic [ref=f1e130]:
- generic [ref=f1e131]:Historical data
- generic [ref=f1e132]:2016–2025
- generic [ref=f1e133]:
- generic [ref=f1e134]:Metrics per school
- generic [ref=f1e135]:40+
- contentinfo [ref=f1e136]:
- generic [ref=f1e137]:
- generic [ref=f1e138]:
- generic [ref=f1e139]:
- heading "SchoolCompare" [level=3] [ref=f1e140]
- paragraph [ref=f1e141]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f1e142] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f1e143]:
- heading "Product" [level=4] [ref=f1e144]
- list [ref=f1e145]:
- listitem [ref=f1e146]:
- link "Search schools" [ref=f1e147] [cursor=pointer]:
- /url:/
- listitem [ref=f1e148]:
- link "Rankings" [ref=f1e149] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f1e150]:
- link "Compare shortlist" [ref=f1e151] [cursor=pointer]:
- /url:/compare
- listitem [ref=f1e152]:
- link "Admissions guide" [ref=f1e153] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e154]:
- heading "Resources" [level=4] [ref=f1e155]
- list [ref=f1e156]:
- listitem [ref=f1e157]:
- link "School Performance Tables" [ref=f1e158] [cursor=pointer]:
- link "Skip to main content" [ref=f1e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f1e3]:
- link "SchoolCompare home" [ref=f1e5] [cursor=pointer]:
- /url:/
- img [ref=f1e7]
- generic [ref=f1e11]:SchoolCompare
- navigation "Main navigation" [ref=f1e12]:
- link "Search" [ref=f1e13] [cursor=pointer]:
- /url:/
- img [ref=f1e15]
- generic [ref=f1e18]:Search
- link "Compare" [ref=f1e19] [cursor=pointer]:
- /url:/compare
- img [ref=f1e21]
- generic [ref=f1e24]:Compare
- link "Rankings" [ref=f1e25] [cursor=pointer]:
- /url:/rankings
- img [ref=f1e27]
- generic [ref=f1e28]:Rankings
- link "Admissions" [ref=f1e29] [cursor=pointer]:
- /url:/admissions
- img [ref=f1e31]
- generic [ref=f1e34]:Admissions
- main [ref=f1e35]:
- generic [ref=f1e36]:
- heading "Every school in England, compared." [level=1] [ref=f1e38]:
- text:Every school in England,
- emphasis [ref=f1e39]:compared.
- generic [ref=f1e42]:
- searchbox "School name or postcode" [active] [ref=f1e43]
- button "Search" [ref=f1e44] [cursor=pointer]
- button "Schools near me" [ref=f1e47] [cursor=pointer]:
- img [ref=f1e48]
- text:Schools near me
- generic [ref=f1e51]:
- generic [ref=f1e52]:
- generic [ref=f1e53]:Key admissions deadlines
- link "Full admissions guide →" [ref=f1e54] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e55]:
- generic [ref=f1e56]:
- generic [ref=f1e57]:Secondary · Deadline
- generic [ref=f1e59]:121days
- generic [ref=f1e60]:Secondary applications close
- generic [ref=f1e61]:Sat, 31 October 2026
- generic [ref=f1e62]:
- generic [ref=f1e63]:Primary · Deadline
- generic [ref=f1e65]:197days
- generic [ref=f1e66]:Primary applications close
- generic [ref=f1e67]:Fri, 15 January 2027
- generic [ref=f1e68]:
- generic [ref=f1e69]:Secondary · Offer Day
- generic [ref=f1e71]:242days
- generic [ref=f1e72]:Secondary National Offer Day
- generic [ref=f1e73]:Mon, 1 March 2027
- generic [ref=f1e74]:
- generic [ref=f1e75]:Primary · Offer Day
- generic [ref=f1e77]:288days
- generic [ref=f1e78]:Primary National Offer Day
- generic [ref=f1e79]:Fri, 16 April 2027
- generic [ref=f1e80]:
- generic [ref=f1e81]:Start exploring
- generic [ref=f1e82]:
- link "Top-rated primary schools" [ref=f1e83] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f1e85] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f1e87] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f1e89]:
- generic [ref=f1e90]:
- heading "What you'll see on every school" [level=2] [ref=f1e91]
- generic [ref=f1e92]:Primary or secondary — the page adapts to the phase
- generic [ref=f1e93]:
- generic [ref=f1e95]:
- generic [ref=f1e96]:Performance
- generic [ref=f1e97]:Results against the national average
- paragraph [ref=f1e98]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f1e100]:
- generic [ref=f1e101]:Judgement
- generic [ref=f1e102]:Ofsted at a glance
- paragraph [ref=f1e103]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f1e105]:
- generic [ref=f1e106]:Compare
- generic [ref=f1e107]:Side-by-side shortlists
- paragraph [ref=f1e108]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f1e110]:
- generic [ref=f1e111]:
- generic [ref=f1e112]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f1e113]
- paragraph [ref=f1e114]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f1e115]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f1e116]:
- heading "Coverage at a glance" [level=3] [ref=f1e117]
- generic [ref=f1e118]:
- generic [ref=f1e119]:Schools covered
- generic [ref=f1e120]:24,000+
- generic [ref=f1e121]:
- generic [ref=f1e122]:Local authorities
- generic [ref=f1e123]:"182"
- generic [ref=f1e124]:
- generic [ref=f1e125]:Phases
- generic [ref=f1e126]:Primary & Secondary
- generic [ref=f1e127]:
- generic [ref=f1e128]:Latest results year
- generic [ref=f1e129]:2024/25
- generic [ref=f1e130]:
- generic [ref=f1e131]:Historical data
- generic [ref=f1e132]:2016–2025
- generic [ref=f1e133]:
- generic [ref=f1e134]:Metrics per school
- generic [ref=f1e135]:40+
- contentinfo [ref=f1e136]:
- generic [ref=f1e137]:
- generic [ref=f1e138]:
- generic [ref=f1e139]:
- heading "SchoolCompare" [level=3] [ref=f1e140]
- paragraph [ref=f1e141]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f1e142] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f1e143]:
- heading "Product" [level=4] [ref=f1e144]
- list [ref=f1e145]:
- listitem [ref=f1e146]:
- link "Search schools" [ref=f1e147] [cursor=pointer]:
- /url:/
- listitem [ref=f1e148]:
- link "Rankings" [ref=f1e149] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f1e150]:
- link "Compare shortlist" [ref=f1e151] [cursor=pointer]:
- /url:/compare
- listitem [ref=f1e152]:
- link "Admissions guide" [ref=f1e153] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e154]:
- heading "Resources" [level=4] [ref=f1e155]
- list [ref=f1e156]:
- listitem [ref=f1e157]:
- link "School Performance Tables" [ref=f1e158] [cursor=pointer]:
- link "Skip to main content" [ref=f1e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f1e3]:
- link "SchoolCompare home" [ref=f1e5] [cursor=pointer]:
- /url:/
- img [ref=f1e7]
- generic [ref=f1e11]:SchoolCompare
- navigation "Main navigation" [ref=f1e12]:
- link "Search" [ref=f1e13] [cursor=pointer]:
- /url:/
- img [ref=f1e15]
- generic [ref=f1e18]:Search
- link "Compare" [ref=f1e19] [cursor=pointer]:
- /url:/compare
- img [ref=f1e21]
- generic [ref=f1e24]:Compare
- link "Rankings" [ref=f1e25] [cursor=pointer]:
- /url:/rankings
- img [ref=f1e27]
- generic [ref=f1e28]:Rankings
- link "Admissions" [ref=f1e29] [cursor=pointer]:
- /url:/admissions
- img [ref=f1e31]
- generic [ref=f1e34]:Admissions
- main [ref=f1e35]:
- generic [ref=f1e36]:
- heading "Every school in England, compared." [level=1] [ref=f1e38]:
- text:Every school in England,
- emphasis [ref=f1e39]:compared.
- generic [ref=f1e42]:
- searchbox "School name or postcode" [active] [ref=f1e43]: Welland Primary
- button "Search" [ref=f1e44] [cursor=pointer]
- button "Schools near me" [ref=f1e47] [cursor=pointer]:
- img [ref=f1e48]
- text:Schools near me
- generic [ref=f1e51]:
- generic [ref=f1e52]:
- generic [ref=f1e53]:Key admissions deadlines
- link "Full admissions guide →" [ref=f1e54] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e55]:
- generic [ref=f1e56]:
- generic [ref=f1e57]:Secondary · Deadline
- generic [ref=f1e59]:121days
- generic [ref=f1e60]:Secondary applications close
- generic [ref=f1e61]:Sat, 31 October 2026
- generic [ref=f1e62]:
- generic [ref=f1e63]:Primary · Deadline
- generic [ref=f1e65]:197days
- generic [ref=f1e66]:Primary applications close
- generic [ref=f1e67]:Fri, 15 January 2027
- generic [ref=f1e68]:
- generic [ref=f1e69]:Secondary · Offer Day
- generic [ref=f1e71]:242days
- generic [ref=f1e72]:Secondary National Offer Day
- generic [ref=f1e73]:Mon, 1 March 2027
- generic [ref=f1e74]:
- generic [ref=f1e75]:Primary · Offer Day
- generic [ref=f1e77]:288days
- generic [ref=f1e78]:Primary National Offer Day
- generic [ref=f1e79]:Fri, 16 April 2027
- generic [ref=f1e80]:
- generic [ref=f1e81]:Start exploring
- generic [ref=f1e82]:
- link "Top-rated primary schools" [ref=f1e83] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f1e85] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f1e87] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f1e89]:
- generic [ref=f1e90]:
- heading "What you'll see on every school" [level=2] [ref=f1e91]
- generic [ref=f1e92]:Primary or secondary — the page adapts to the phase
- generic [ref=f1e93]:
- generic [ref=f1e95]:
- generic [ref=f1e96]:Performance
- generic [ref=f1e97]:Results against the national average
- paragraph [ref=f1e98]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f1e100]:
- generic [ref=f1e101]:Judgement
- generic [ref=f1e102]:Ofsted at a glance
- paragraph [ref=f1e103]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f1e105]:
- generic [ref=f1e106]:Compare
- generic [ref=f1e107]:Side-by-side shortlists
- paragraph [ref=f1e108]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f1e110]:
- generic [ref=f1e111]:
- generic [ref=f1e112]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f1e113]
- paragraph [ref=f1e114]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f1e115]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f1e116]:
- heading "Coverage at a glance" [level=3] [ref=f1e117]
- generic [ref=f1e118]:
- generic [ref=f1e119]:Schools covered
- generic [ref=f1e120]:24,000+
- generic [ref=f1e121]:
- generic [ref=f1e122]:Local authorities
- generic [ref=f1e123]:"182"
- generic [ref=f1e124]:
- generic [ref=f1e125]:Phases
- generic [ref=f1e126]:Primary & Secondary
- generic [ref=f1e127]:
- generic [ref=f1e128]:Latest results year
- generic [ref=f1e129]:2024/25
- generic [ref=f1e130]:
- generic [ref=f1e131]:Historical data
- generic [ref=f1e132]:2016–2025
- generic [ref=f1e133]:
- generic [ref=f1e134]:Metrics per school
- generic [ref=f1e135]:40+
- contentinfo [ref=f1e136]:
- generic [ref=f1e137]:
- generic [ref=f1e138]:
- generic [ref=f1e139]:
- heading "SchoolCompare" [level=3] [ref=f1e140]
- paragraph [ref=f1e141]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f1e142] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f1e143]:
- heading "Product" [level=4] [ref=f1e144]
- list [ref=f1e145]:
- listitem [ref=f1e146]:
- link "Search schools" [ref=f1e147] [cursor=pointer]:
- /url:/
- listitem [ref=f1e148]:
- link "Rankings" [ref=f1e149] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f1e150]:
- link "Compare shortlist" [ref=f1e151] [cursor=pointer]:
- /url:/compare
- listitem [ref=f1e152]:
- link "Admissions guide" [ref=f1e153] [cursor=pointer]:
- /url:/admissions
- generic [ref=f1e154]:
- heading "Resources" [level=4] [ref=f1e155]
- list [ref=f1e156]:
- listitem [ref=f1e157]:
- link "School Performance Tables" [ref=f1e158] [cursor=pointer]:
- link "Skip to main content" [ref=f3e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f3e3]:
- link "SchoolCompare home" [ref=f3e5] [cursor=pointer]:
- /url:/
- img [ref=f3e7]
- generic [ref=f3e11]:SchoolCompare
- navigation "Main navigation" [ref=f3e12]:
- link "Search" [ref=f3e13] [cursor=pointer]:
- /url:/
- img [ref=f3e15]
- generic [ref=f3e18]:Search
- link "Compare" [ref=f3e19] [cursor=pointer]:
- /url:/compare
- img [ref=f3e21]
- generic [ref=f3e24]:Compare
- link "Rankings" [ref=f3e25] [cursor=pointer]:
- /url:/rankings
- img [ref=f3e27]
- generic [ref=f3e28]:Rankings
- link "Admissions" [ref=f3e29] [cursor=pointer]:
- /url:/admissions
- img [ref=f3e31]
- generic [ref=f3e34]:Admissions
- main [ref=f3e35]:
- generic [ref=f3e36]:
- heading "Every school in England, compared." [level=1] [ref=f3e38]:
- text:Every school in England,
- emphasis [ref=f3e39]:compared.
- generic [ref=f3e42]:
- searchbox "School name or postcode" [ref=f3e43]
- button "Search" [ref=f3e44] [cursor=pointer]
- button "Schools near me" [ref=f3e47] [cursor=pointer]:
- img [ref=f3e48]
- text:Schools near me
- generic [ref=f3e51]:
- generic [ref=f3e52]:
- generic [ref=f3e53]:Key admissions deadlines
- link "Full admissions guide →" [ref=f3e54] [cursor=pointer]:
- /url:/admissions
- generic [ref=f3e55]:
- generic [ref=f3e56]:
- generic [ref=f3e57]:Secondary · Deadline
- generic [ref=f3e59]:121days
- generic [ref=f3e60]:Secondary applications close
- generic [ref=f3e61]:Sat, 31 October 2026
- generic [ref=f3e62]:
- generic [ref=f3e63]:Primary · Deadline
- generic [ref=f3e65]:197days
- generic [ref=f3e66]:Primary applications close
- generic [ref=f3e67]:Fri, 15 January 2027
- generic [ref=f3e68]:
- generic [ref=f3e69]:Secondary · Offer Day
- generic [ref=f3e71]:242days
- generic [ref=f3e72]:Secondary National Offer Day
- generic [ref=f3e73]:Mon, 1 March 2027
- generic [ref=f3e74]:
- generic [ref=f3e75]:Primary · Offer Day
- generic [ref=f3e77]:288days
- generic [ref=f3e78]:Primary National Offer Day
- generic [ref=f3e79]:Fri, 16 April 2027
- generic [ref=f3e80]:
- generic [ref=f3e81]:Start exploring
- generic [ref=f3e82]:
- link "Top-rated primary schools" [ref=f3e83] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f3e85] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f3e87] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f3e89]:
- generic [ref=f3e90]:
- heading "What you'll see on every school" [level=2] [ref=f3e91]
- generic [ref=f3e92]:Primary or secondary — the page adapts to the phase
- generic [ref=f3e93]:
- generic [ref=f3e95]:
- generic [ref=f3e96]:Performance
- generic [ref=f3e97]:Results against the national average
- paragraph [ref=f3e98]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f3e100]:
- generic [ref=f3e101]:Judgement
- generic [ref=f3e102]:Ofsted at a glance
- paragraph [ref=f3e103]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f3e105]:
- generic [ref=f3e106]:Compare
- generic [ref=f3e107]:Side-by-side shortlists
- paragraph [ref=f3e108]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f3e110]:
- generic [ref=f3e111]:
- generic [ref=f3e112]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f3e113]
- paragraph [ref=f3e114]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f3e115]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f3e116]:
- heading "Coverage at a glance" [level=3] [ref=f3e117]
- generic [ref=f3e118]:
- generic [ref=f3e119]:Schools covered
- generic [ref=f3e120]:24,000+
- generic [ref=f3e121]:
- generic [ref=f3e122]:Local authorities
- generic [ref=f3e123]:"182"
- generic [ref=f3e124]:
- generic [ref=f3e125]:Phases
- generic [ref=f3e126]:Primary & Secondary
- generic [ref=f3e127]:
- generic [ref=f3e128]:Latest results year
- generic [ref=f3e129]:2024/25
- generic [ref=f3e130]:
- generic [ref=f3e131]:Historical data
- generic [ref=f3e132]:2016–2025
- generic [ref=f3e133]:
- generic [ref=f3e134]:Metrics per school
- generic [ref=f3e135]:40+
- contentinfo [ref=f3e136]:
- generic [ref=f3e137]:
- generic [ref=f3e138]:
- generic [ref=f3e139]:
- heading "SchoolCompare" [level=3] [ref=f3e140]
- paragraph [ref=f3e141]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f3e142] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f3e143]:
- heading "Product" [level=4] [ref=f3e144]
- list [ref=f3e145]:
- listitem [ref=f3e146]:
- link "Search schools" [ref=f3e147] [cursor=pointer]:
- /url:/
- listitem [ref=f3e148]:
- link "Rankings" [ref=f3e149] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f3e150]:
- link "Compare shortlist" [ref=f3e151] [cursor=pointer]:
- /url:/compare
- listitem [ref=f3e152]:
- link "Admissions guide" [ref=f3e153] [cursor=pointer]:
- /url:/admissions
- generic [ref=f3e154]:
- heading "Resources" [level=4] [ref=f3e155]
- list [ref=f3e156]:
- listitem [ref=f3e157]:
- link "School Performance Tables" [ref=f3e158] [cursor=pointer]:
- link "Skip to main content" [ref=f3e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f3e3]:
- link "SchoolCompare home" [ref=f3e5] [cursor=pointer]:
- /url:/
- img [ref=f3e7]
- generic [ref=f3e11]:SchoolCompare
- navigation "Main navigation" [ref=f3e12]:
- link "Search" [ref=f3e13] [cursor=pointer]:
- /url:/
- img [ref=f3e15]
- generic [ref=f3e18]:Search
- link "Compare" [ref=f3e19] [cursor=pointer]:
- /url:/compare
- img [ref=f3e21]
- generic [ref=f3e24]:Compare
- link "Rankings" [ref=f3e25] [cursor=pointer]:
- /url:/rankings
- img [ref=f3e27]
- generic [ref=f3e28]:Rankings
- link "Admissions" [ref=f3e29] [cursor=pointer]:
- /url:/admissions
- img [ref=f3e31]
- generic [ref=f3e34]:Admissions
- main [ref=f3e35]:
- generic [ref=f3e36]:
- heading "Every school in England, compared." [level=1] [ref=f3e38]:
- text:Every school in England,
- emphasis [ref=f3e39]:compared.
- generic [ref=f3e42]:
- searchbox "School name or postcode" [active] [ref=f3e43]: B91 3
- button "Search" [ref=f3e44] [cursor=pointer]
- button "Schools near me" [ref=f3e47] [cursor=pointer]:
- img [ref=f3e48]
- text:Schools near me
- generic [ref=f3e51]:
- generic [ref=f3e52]:
- generic [ref=f3e53]:Key admissions deadlines
- link "Full admissions guide →" [ref=f3e54] [cursor=pointer]:
- /url:/admissions
- generic [ref=f3e55]:
- generic [ref=f3e56]:
- generic [ref=f3e57]:Secondary · Deadline
- generic [ref=f3e59]:121days
- generic [ref=f3e60]:Secondary applications close
- generic [ref=f3e61]:Sat, 31 October 2026
- generic [ref=f3e62]:
- generic [ref=f3e63]:Primary · Deadline
- generic [ref=f3e65]:197days
- generic [ref=f3e66]:Primary applications close
- generic [ref=f3e67]:Fri, 15 January 2027
- generic [ref=f3e68]:
- generic [ref=f3e69]:Secondary · Offer Day
- generic [ref=f3e71]:242days
- generic [ref=f3e72]:Secondary National Offer Day
- generic [ref=f3e73]:Mon, 1 March 2027
- generic [ref=f3e74]:
- generic [ref=f3e75]:Primary · Offer Day
- generic [ref=f3e77]:288days
- generic [ref=f3e78]:Primary National Offer Day
- generic [ref=f3e79]:Fri, 16 April 2027
- generic [ref=f3e80]:
- generic [ref=f3e81]:Start exploring
- generic [ref=f3e82]:
- link "Top-rated primary schools" [ref=f3e83] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f3e85] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f3e87] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f3e89]:
- generic [ref=f3e90]:
- heading "What you'll see on every school" [level=2] [ref=f3e91]
- generic [ref=f3e92]:Primary or secondary — the page adapts to the phase
- generic [ref=f3e93]:
- generic [ref=f3e95]:
- generic [ref=f3e96]:Performance
- generic [ref=f3e97]:Results against the national average
- paragraph [ref=f3e98]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f3e100]:
- generic [ref=f3e101]:Judgement
- generic [ref=f3e102]:Ofsted at a glance
- paragraph [ref=f3e103]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f3e105]:
- generic [ref=f3e106]:Compare
- generic [ref=f3e107]:Side-by-side shortlists
- paragraph [ref=f3e108]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f3e110]:
- generic [ref=f3e111]:
- generic [ref=f3e112]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f3e113]
- paragraph [ref=f3e114]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f3e115]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f3e116]:
- heading "Coverage at a glance" [level=3] [ref=f3e117]
- generic [ref=f3e118]:
- generic [ref=f3e119]:Schools covered
- generic [ref=f3e120]:24,000+
- generic [ref=f3e121]:
- generic [ref=f3e122]:Local authorities
- generic [ref=f3e123]:"182"
- generic [ref=f3e124]:
- generic [ref=f3e125]:Phases
- generic [ref=f3e126]:Primary & Secondary
- generic [ref=f3e127]:
- generic [ref=f3e128]:Latest results year
- generic [ref=f3e129]:2024/25
- generic [ref=f3e130]:
- generic [ref=f3e131]:Historical data
- generic [ref=f3e132]:2016–2025
- generic [ref=f3e133]:
- generic [ref=f3e134]:Metrics per school
- generic [ref=f3e135]:40+
- contentinfo [ref=f3e136]:
- generic [ref=f3e137]:
- generic [ref=f3e138]:
- generic [ref=f3e139]:
- heading "SchoolCompare" [level=3] [ref=f3e140]
- paragraph [ref=f3e141]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f3e142] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f3e143]:
- heading "Product" [level=4] [ref=f3e144]
- list [ref=f3e145]:
- listitem [ref=f3e146]:
- link "Search schools" [ref=f3e147] [cursor=pointer]:
- /url:/
- listitem [ref=f3e148]:
- link "Rankings" [ref=f3e149] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f3e150]:
- link "Compare shortlist" [ref=f3e151] [cursor=pointer]:
- /url:/compare
- listitem [ref=f3e152]:
- link "Admissions guide" [ref=f3e153] [cursor=pointer]:
- /url:/admissions
- generic [ref=f3e154]:
- heading "Resources" [level=4] [ref=f3e155]
- list [ref=f3e156]:
- listitem [ref=f3e157]:
- link "School Performance Tables" [ref=f3e158] [cursor=pointer]:
- link "Skip to main content" [ref=f4e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f4e3]:
- generic [ref=f4e4]:
- link "SchoolCompare home" [ref=f4e5] [cursor=pointer]:
- /url:/
- img [ref=f4e7]
- generic [ref=f4e11]:SchoolCompare
- navigation "Main navigation" [ref=f4e12]:
- link "Search" [ref=f4e13] [cursor=pointer]:
- /url:/
- link "Compare" [ref=f4e14] [cursor=pointer]:
- /url:/compare
- link "Rankings" [ref=f4e15] [cursor=pointer]:
- /url:/rankings
- link "Admissions" [ref=f4e16] [cursor=pointer]:
- /url:/admissions
- main [ref=f4e17]:
- generic [ref=f4e18]:
- generic [ref=f4e19]:
- generic [ref=f4e20]:Updated with 2026/2027 admissions results
- heading "Every school in England, compared." [level=1] [ref=f4e22]:
- text:Every school in England,
- emphasis [ref=f4e23]:compared.
- paragraph [ref=f4e24]:
- strong [ref=f4e25]:24,000+ primary and secondary schools
- text:with Key Stage 2 SATs, GCSE results, Ofsted grades, progress scores and admissions data — side by side, in one place.
- generic [ref=f4e28]:
- searchbox "School name or postcode" [ref=f4e29]
- button "Search" [ref=f4e30] [cursor=pointer]
- button "Schools near me" [ref=f4e33] [cursor=pointer]:
- img [ref=f4e34]
- text:Schools near me
- generic [ref=f4e37]:
- generic [ref=f4e38]:
- generic [ref=f4e39]:Key admissions deadlines
- link "Full admissions guide →" [ref=f4e40] [cursor=pointer]:
- /url:/admissions
- generic [ref=f4e41]:
- generic [ref=f4e42]:
- generic [ref=f4e43]:Secondary · Deadline
- generic [ref=f4e45]:121days
- generic [ref=f4e46]:Secondary applications close
- generic [ref=f4e47]:Sat, 31 October 2026
- generic [ref=f4e48]:
- generic [ref=f4e49]:Primary · Deadline
- generic [ref=f4e51]:197days
- generic [ref=f4e52]:Primary applications close
- generic [ref=f4e53]:Fri, 15 January 2027
- generic [ref=f4e54]:
- generic [ref=f4e55]:Secondary · Offer Day
- generic [ref=f4e57]:242days
- generic [ref=f4e58]:Secondary National Offer Day
- generic [ref=f4e59]:Mon, 1 March 2027
- generic [ref=f4e60]:
- generic [ref=f4e61]:Primary · Offer Day
- generic [ref=f4e63]:288days
- generic [ref=f4e64]:Primary National Offer Day
- generic [ref=f4e65]:Fri, 16 April 2027
- generic [ref=f4e66]:
- generic [ref=f4e67]:Start exploring
- generic [ref=f4e68]:
- link "Top-rated primary schools" [ref=f4e69] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f4e71] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f4e73] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f4e75]:
- generic [ref=f4e76]:
- heading "What you'll see on every school" [level=2] [ref=f4e77]
- generic [ref=f4e78]:Primary or secondary — the page adapts to the phase
- generic [ref=f4e79]:
- generic [ref=f4e80]:
- generic [ref=f4e81]:
- generic [ref=f4e82]:
- generic [ref=f4e83]:
- text:Primary · Year 6 ·
- strong [ref=f4e84]:Key Stage 2 SATs
- generic [ref=f4e85]:
- generic [ref=f4e86]:
- generic [ref=f4e87]:Reading
- generic [ref=f4e88]:
- generic [ref=f4e89]:Expected
- strong [ref=f4e90]:96%
- generic [ref=f4e91]:
- generic:75%
- generic [ref=f4e93]:
- generic [ref=f4e94]:Exceeding
- strong [ref=f4e95]:73%
- generic [ref=f4e98]:
- generic [ref=f4e99]:Writing
- generic [ref=f4e100]:
- generic [ref=f4e101]:Expected
- strong [ref=f4e102]:81%
- generic [ref=f4e103]:
- generic:72%
- generic [ref=f4e105]:
- generic [ref=f4e106]:Exceeding
- strong [ref=f4e107]:15%
- generic [ref=f4e110]:
- generic [ref=f4e111]:Maths
- generic [ref=f4e112]:
- generic [ref=f4e113]:Expected
- strong [ref=f4e114]:85%
- generic [ref=f4e115]:
- generic:74%
- generic [ref=f4e117]:
- generic [ref=f4e118]:Exceeding
- strong [ref=f4e119]:47%
- generic [ref=f4e122]:
- generic [ref=f4e123]:
- text:Secondary · Year 11 ·
- strong [ref=f4e124]:GCSE Attainment 8
- generic [ref=f4e125]:
- generic [ref=f4e127]:
- generic [ref=f4e128]:This school
- generic [ref=f4e129]:National avg 50.2
- generic [ref=f4e133]:
- generic [ref=f4e134]:"62.4"
- generic [ref=f4e135]:+12.2 vs national
- generic [ref=f4e136]:
- generic [ref=f4e137]:Performance
- generic [ref=f4e138]:Results against the national average
- paragraph [ref=f4e139]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f4e140]:
- generic [ref=f4e142]:
- generic [ref=f4e145]:Latest Ofsted inspection
- generic [ref=f4e146]:OUTSTANDING
- generic [ref=f4e147]:
- text:Rated
- emphasis [ref=f4e148]:Outstanding
- text:at last inspection.
- generic [ref=f4e149]:Full inspection · March 2024
- generic [ref=f4e150]:
- generic [ref=f4e151]:Judgement
- generic [ref=f4e152]:Ofsted at a glance
- paragraph [ref=f4e153]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f4e154]:
- generic [ref=f4e156]:
- generic [ref=f4e157]:
- generic [ref=f4e158]:Metric
- generic [ref=f4e159]:
- text:Our Lady
- text:Queen of Heaven
- generic [ref=f4e160]:
- text:St Mary's
- text:Catholic Primary
- generic [ref=f4e161]:
- generic [ref=f4e162]:Reading, Writing & Maths
- generic [ref=f4e163]:70%
- generic [ref=f4e164]:64%
- generic [ref=f4e165]:
- generic [ref=f4e166]:Ofsted
- generic [ref=f4e167]:Outstanding
- generic [ref=f4e168]:Good
- generic [ref=f4e169]:
- generic [ref=f4e170]:Reading progress
- generic [ref=f4e171]:"+2.1"
- generic [ref=f4e172]:"+0.4"
- generic [ref=f4e173]:+ pin up to 5 schools
- generic [ref=f4e174]:
- generic [ref=f4e175]:Compare
- generic [ref=f4e176]:Side-by-side shortlists
- paragraph [ref=f4e177]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f4e179]:
- generic [ref=f4e180]:
- generic [ref=f4e181]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f4e182]
- paragraph [ref=f4e183]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f4e184]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f4e185]:
- heading "Coverage at a glance" [level=3] [ref=f4e186]
- generic [ref=f4e187]:
- generic [ref=f4e188]:Schools covered
- generic [ref=f4e189]:24,000+
- generic [ref=f4e190]:
- generic [ref=f4e191]:Local authorities
- generic [ref=f4e192]:"182"
- generic [ref=f4e193]:
- generic [ref=f4e194]:Phases
- generic [ref=f4e195]:Primary & Secondary
- generic [ref=f4e196]:
- generic [ref=f4e197]:Latest results year
- generic [ref=f4e198]:2024/25
- generic [ref=f4e199]:
- generic [ref=f4e200]:Historical data
- generic [ref=f4e201]:2016–2025
- generic [ref=f4e202]:
- generic [ref=f4e203]:Metrics per school
- generic [ref=f4e204]:40+
- contentinfo [ref=f4e205]:
- generic [ref=f4e206]:
- generic [ref=f4e207]:
- generic [ref=f4e208]:
- heading "SchoolCompare" [level=3] [ref=f4e209]
- paragraph [ref=f4e210]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f4e211] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f4e212]:
- heading "Product" [level=4] [ref=f4e213]
- list [ref=f4e214]:
- listitem [ref=f4e215]:
- link "Search schools" [ref=f4e216] [cursor=pointer]:
- /url:/
- listitem [ref=f4e217]:
- link "Rankings" [ref=f4e218] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f4e219]:
- link "Compare shortlist" [ref=f4e220] [cursor=pointer]:
- /url:/compare
- listitem [ref=f4e221]:
- link "Admissions guide" [ref=f4e222] [cursor=pointer]:
- /url:/admissions
- generic [ref=f4e223]:
- heading "Resources" [level=4] [ref=f4e224]
- list [ref=f4e225]:
- listitem [ref=f4e226]:
- link "School Performance Tables" [ref=f4e227] [cursor=pointer]:
- link "Skip to main content" [ref=f5e2] [cursor=pointer]:
- /url:"#main-content"
- banner [ref=f5e3]:
- generic [ref=f5e4]:
- link "SchoolCompare home" [ref=f5e5] [cursor=pointer]:
- /url:/
- img [ref=f5e7]
- generic [ref=f5e11]:SchoolCompare
- navigation "Main navigation" [ref=f5e12]:
- link "Search" [ref=f5e13] [cursor=pointer]:
- /url:/
- link "Compare" [ref=f5e14] [cursor=pointer]:
- /url:/compare
- link "Rankings" [ref=f5e15] [cursor=pointer]:
- /url:/rankings
- link "Admissions" [ref=f5e16] [cursor=pointer]:
- /url:/admissions
- main [ref=f5e17]:
- generic [ref=f5e18]:
- generic [ref=f5e19]:
- generic [ref=f5e20]:Updated with 2026/2027 admissions results
- heading "Every school in England, compared." [level=1] [ref=f5e22]:
- text:Every school in England,
- emphasis [ref=f5e23]:compared.
- paragraph [ref=f5e24]:
- strong [ref=f5e25]:24,000+ primary and secondary schools
- text:with Key Stage 2 SATs, GCSE results, Ofsted grades, progress scores and admissions data — side by side, in one place.
- generic [ref=f5e28]:
- searchbox "School name or postcode" [ref=f5e29]
- button "Search" [ref=f5e30] [cursor=pointer]
- button "Schools near me" [ref=f5e33] [cursor=pointer]:
- img [ref=f5e34]
- text:Schools near me
- generic [ref=f5e37]:
- generic [ref=f5e38]:
- generic [ref=f5e39]:Key admissions deadlines
- link "Full admissions guide →" [ref=f5e40] [cursor=pointer]:
- /url:/admissions
- generic [ref=f5e41]:
- generic [ref=f5e42]:
- generic [ref=f5e43]:Secondary · Deadline
- generic [ref=f5e45]:121days
- generic [ref=f5e46]:Secondary applications close
- generic [ref=f5e47]:Sat, 31 October 2026
- generic [ref=f5e48]:
- generic [ref=f5e49]:Primary · Deadline
- generic [ref=f5e51]:197days
- generic [ref=f5e52]:Primary applications close
- generic [ref=f5e53]:Fri, 15 January 2027
- generic [ref=f5e54]:
- generic [ref=f5e55]:Secondary · Offer Day
- generic [ref=f5e57]:242days
- generic [ref=f5e58]:Secondary National Offer Day
- generic [ref=f5e59]:Mon, 1 March 2027
- generic [ref=f5e60]:
- generic [ref=f5e61]:Primary · Offer Day
- generic [ref=f5e63]:288days
- generic [ref=f5e64]:Primary National Offer Day
- generic [ref=f5e65]:Fri, 16 April 2027
- generic [ref=f5e66]:
- generic [ref=f5e67]:Start exploring
- generic [ref=f5e68]:
- link "Top-rated primary schools" [ref=f5e69] [cursor=pointer]:
- /url:/rankings
- text:Top-rated primary schools
- link "Top-rated secondary schools" [ref=f5e71] [cursor=pointer]:
- /url:/rankings
- text:Top-rated secondary schools
- link "Start a comparison" [ref=f5e73] [cursor=pointer]:
- /url:/compare
- text:Start a comparison
- generic [ref=f5e75]:
- generic [ref=f5e76]:
- heading "What you'll see on every school" [level=2] [ref=f5e77]
- generic [ref=f5e78]:Primary or secondary — the page adapts to the phase
- generic [ref=f5e79]:
- generic [ref=f5e80]:
- generic [ref=f5e81]:
- generic [ref=f5e82]:
- generic [ref=f5e83]:
- text:Primary · Year 6 ·
- strong [ref=f5e84]:Key Stage 2 SATs
- generic [ref=f5e85]:
- generic [ref=f5e86]:
- generic [ref=f5e87]:Reading
- generic [ref=f5e88]:
- generic [ref=f5e89]:Expected
- strong [ref=f5e90]:96%
- generic [ref=f5e91]:
- generic:75%
- generic [ref=f5e93]:
- generic [ref=f5e94]:Exceeding
- strong [ref=f5e95]:73%
- generic [ref=f5e98]:
- generic [ref=f5e99]:Writing
- generic [ref=f5e100]:
- generic [ref=f5e101]:Expected
- strong [ref=f5e102]:81%
- generic [ref=f5e103]:
- generic:72%
- generic [ref=f5e105]:
- generic [ref=f5e106]:Exceeding
- strong [ref=f5e107]:15%
- generic [ref=f5e110]:
- generic [ref=f5e111]:Maths
- generic [ref=f5e112]:
- generic [ref=f5e113]:Expected
- strong [ref=f5e114]:85%
- generic [ref=f5e115]:
- generic:74%
- generic [ref=f5e117]:
- generic [ref=f5e118]:Exceeding
- strong [ref=f5e119]:47%
- generic [ref=f5e122]:
- generic [ref=f5e123]:
- text:Secondary · Year 11 ·
- strong [ref=f5e124]:GCSE Attainment 8
- generic [ref=f5e125]:
- generic [ref=f5e127]:
- generic [ref=f5e128]:This school
- generic [ref=f5e129]:National avg 50.2
- generic [ref=f5e133]:
- generic [ref=f5e134]:"62.4"
- generic [ref=f5e135]:+12.2 vs national
- generic [ref=f5e136]:
- generic [ref=f5e137]:Performance
- generic [ref=f5e138]:Results against the national average
- paragraph [ref=f5e139]:For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.
- generic [ref=f5e140]:
- generic [ref=f5e142]:
- generic [ref=f5e145]:Latest Ofsted inspection
- generic [ref=f5e146]:OUTSTANDING
- generic [ref=f5e147]:
- text:Rated
- emphasis [ref=f5e148]:Outstanding
- text:at last inspection.
- generic [ref=f5e149]:Full inspection · March 2024
- generic [ref=f5e150]:
- generic [ref=f5e151]:Judgement
- generic [ref=f5e152]:Ofsted at a glance
- paragraph [ref=f5e153]:Current grade, inspection date, and a plain-English headline — without opening a 40-page report.
- generic [ref=f5e154]:
- generic [ref=f5e156]:
- generic [ref=f5e157]:
- generic [ref=f5e158]:Metric
- generic [ref=f5e159]:
- text:Our Lady
- text:Queen of Heaven
- generic [ref=f5e160]:
- text:St Mary's
- text:Catholic Primary
- generic [ref=f5e161]:
- generic [ref=f5e162]:Reading, Writing & Maths
- generic [ref=f5e163]:70%
- generic [ref=f5e164]:64%
- generic [ref=f5e165]:
- generic [ref=f5e166]:Ofsted
- generic [ref=f5e167]:Outstanding
- generic [ref=f5e168]:Good
- generic [ref=f5e169]:
- generic [ref=f5e170]:Reading progress
- generic [ref=f5e171]:"+2.1"
- generic [ref=f5e172]:"+0.4"
- generic [ref=f5e173]:+ pin up to 5 schools
- generic [ref=f5e174]:
- generic [ref=f5e175]:Compare
- generic [ref=f5e176]:Side-by-side shortlists
- paragraph [ref=f5e177]:Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.
- generic [ref=f5e179]:
- generic [ref=f5e180]:
- generic [ref=f5e181]:About school data
- heading "Making UK school performance data actually readable" [level=2] [ref=f5e182]
- paragraph [ref=f5e183]:School performance data in England is rich but fragmented. The Department for Education publishes Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and demographics — each in its own table, each with its own jargon.
- paragraph [ref=f5e184]:SchoolCompare brings it all into one place. Every school page shows performance against the national average, explains what the numbers mean, and lets you shortlist schools side by side. Built for parents, governors, journalists, and anyone who wants to understand a school without reading a 40-page inspection report.
- generic [ref=f5e185]:
- heading "Coverage at a glance" [level=3] [ref=f5e186]
- generic [ref=f5e187]:
- generic [ref=f5e188]:Schools covered
- generic [ref=f5e189]:24,000+
- generic [ref=f5e190]:
- generic [ref=f5e191]:Local authorities
- generic [ref=f5e192]:"182"
- generic [ref=f5e193]:
- generic [ref=f5e194]:Phases
- generic [ref=f5e195]:Primary & Secondary
- generic [ref=f5e196]:
- generic [ref=f5e197]:Latest results year
- generic [ref=f5e198]:2024/25
- generic [ref=f5e199]:
- generic [ref=f5e200]:Historical data
- generic [ref=f5e201]:2016–2025
- generic [ref=f5e202]:
- generic [ref=f5e203]:Metrics per school
- generic [ref=f5e204]:40+
- contentinfo [ref=f5e205]:
- generic [ref=f5e206]:
- generic [ref=f5e207]:
- generic [ref=f5e208]:
- heading "SchoolCompare" [level=3] [ref=f5e209]
- paragraph [ref=f5e210]:Compare primary and secondary schools across England.
- link "contact@schoolcompare.co.uk" [ref=f5e211] [cursor=pointer]:
- /url:mailto:contact@schoolcompare.co.uk
- generic [ref=f5e212]:
- heading "Product" [level=4] [ref=f5e213]
- list [ref=f5e214]:
- listitem [ref=f5e215]:
- link "Search schools" [ref=f5e216] [cursor=pointer]:
- /url:/
- listitem [ref=f5e217]:
- link "Rankings" [ref=f5e218] [cursor=pointer]:
- /url:/rankings
- listitem [ref=f5e219]:
- link "Compare shortlist" [ref=f5e220] [cursor=pointer]:
- /url:/compare
- listitem [ref=f5e221]:
- link "Admissions guide" [ref=f5e222] [cursor=pointer]:
- /url:/admissions
- generic [ref=f5e223]:
- heading "Resources" [level=4] [ref=f5e224]
- list [ref=f5e225]:
- listitem [ref=f5e226]:
- link "School Performance Tables" [ref=f5e227] [cursor=pointer]:
- paragraph [ref=f6e98]:GCSE results for Year 11 pupils. National averages shown for comparison.
- generic [ref=f6e99]:Progress 8 scores for 2024/25 are not used for accountability purposes following the KS2 assessment disruption. Treat with caution.
- generic [ref=f6e100]:
- generic [ref=f6e101]:
- generic [ref=f6e102]:Attainment 8 score
- generic [ref=f6e103]:"53.4"
- generic [ref=f6e104]:
- generic [ref=f6e105]:English & Maths Grade 5+
- generic [ref=f6e106]:57.7%
- generic [ref=f6e107]:
- generic [ref=f6e108]:English & Maths Grade 4+
- generic [ref=f6e109]:79.5%
- generic [ref=f6e110]:
- generic [ref=f6e111]:Attainment 8 — school vs national
- generic [ref=f6e147]:Families who got their first choice
- generic [ref=f6e148]:69.6%
- generic [ref=f6e149]:⚠ Applications exceeded places last year
- paragraph [ref=f6e150]:Historical distance cut-off data is not available for this school. Contact the admissions authority for oversubscription criteria details.
- paragraph [ref=f6e98]:GCSE results for Year 11 pupils. National averages shown for comparison.
- generic [ref=f6e99]:Progress 8 scores for 2024/25 are not used for accountability purposes following the KS2 assessment disruption. Treat with caution.
- generic [ref=f6e100]:
- generic [ref=f6e101]:
- generic [ref=f6e102]:Attainment 8 score
- generic [ref=f6e103]:
- text:"53.4"
- generic [ref=f6e215]:+14 pts
- generic [ref=f6e216]:"National avg: 39.1"
- generic [ref=f6e104]:
- generic [ref=f6e105]:English & Maths Grade 5+
- generic [ref=f6e106]:
- text:57.7%
- generic [ref=f6e217]:+22 pts
- generic [ref=f6e218]:"National avg: 36%"
- generic [ref=f6e107]:
- generic [ref=f6e108]:English & Maths Grade 4+
- generic [ref=f6e109]:
- text:79.5%
- generic [ref=f6e219]:+28 pts
- generic [ref=f6e220]:"National avg: 51%"
- generic [ref=f6e110]:
- generic [ref=f6e111]:Attainment 8 — school vs national
- generic [ref=f6e147]:Families who got their first choice
- generic [ref=f6e148]:69.6%
- generic [ref=f6e149]:⚠ Applications exceeded places last year
- paragraph [ref=f6e150]:Historical distance cut-off data is not available for this school. Contact the admissions authority for oversubscription criteria details.
- paragraph [ref=f9e99]:GCSE results for Year 11 pupils. National averages shown for comparison.
- generic [ref=f9e100]:Progress 8 scores for 2024/25 are not used for accountability purposes following the KS2 assessment disruption. Treat with caution.
- generic [ref=f9e101]:
- generic [ref=f9e102]:
- generic [ref=f9e103]:Attainment 8 score
- generic [ref=f9e104]:
- text:"53.4"
- generic [ref=f9e105]:+14 pts
- generic [ref=f9e106]:"National avg: 39.1"
- generic [ref=f9e107]:
- generic [ref=f9e108]:English & Maths Grade 5+
- generic [ref=f9e109]:
- text:57.7%
- generic [ref=f9e110]:+22 pts
- generic [ref=f9e111]:"National avg: 36%"
- generic [ref=f9e112]:
- generic [ref=f9e113]:English & Maths Grade 4+
- generic [ref=f9e114]:
- text:79.5%
- generic [ref=f9e115]:+28 pts
- generic [ref=f9e116]:"National avg: 51%"
- generic [ref=f9e117]:
- generic [ref=f9e118]:Attainment 8 — school vs national
- generic [ref=f9e156]:Families who got their first choice
- generic [ref=f9e157]:69.6%
- generic [ref=f9e158]:⚠ Applications exceeded places last year
- paragraph [ref=f9e159]:Historical distance cut-off data is not available for this school. Contact the admissions authority for oversubscription criteria details.
- paragraph [ref=f10e81]:GCSE results for Year 11 pupils. National averages shown for comparison.
- generic [ref=f10e82]:Progress 8 scores for 2024/25 are not used for accountability purposes following the KS2 assessment disruption. Treat with caution.
- generic [ref=f10e146]:Families who got their first choice
- generic [ref=f10e147]:69.6%
- generic [ref=f10e148]:⚠ Applications exceeded places last year
- paragraph [ref=f10e149]:Historical distance cut-off data is not available for this school. Contact the admissions authority for oversubscription criteria details.
Loaded 100 of 431 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.