feat(places): list schools alphabetically on place pages #123

Merged
tudor merged 2 commits from feat/place-alphabetical-sort into main 2026-08-21 23:22:49 +00:00
Owner

Place pages now list schools A–Z instead of by score.

Sorted case-insensitively — otherwise every capitalised name sorts ahead of every lowercase one, which looks like a bug rather than an order.

Three things this made untrue, fixed with it

Five pieces of copy claimed a ranking. 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, so those are rewritten.

The structured data implied one too. The ItemList carried position with no declared order, which reads as a ranking. It now declares ItemListOrderAscending, so the markup says what the table does.

/api/rankings is untouched. Alphabetical is a place-page decision, not a site-wide one — the league table still ranks by metric, and there is a test on each side pinning that.

Verification

Backend 112 · frontend 260 · tsc --noEmit clean · next build green · 87 e2e journeys.

New coverage on both sides of the boundary: the place API returns A–Z and ignores case, the component does not re-sort what the API sent, the JSON-LD declares its order, a live place page lists alphabetically, and /api/rankings still orders by score.

One trade-off worth naming

Ranked order was doing SEO work: it put the strongest schools in the first screenful and in the top 20 of the ItemList. Alphabetical serves a reader looking for a named school, which is the more common reason to be on the page, but it does mean the markup now leads with whatever starts with "A".

If discovery turns out to matter more than lookup once these pages have Search Console data, a sort control — A–Z by default, score optional — would serve both without either page lying about its order.

🤖 Generated with Claude Code

https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj

Place pages now list schools A–Z instead of by score. Sorted case-insensitively — otherwise every capitalised name sorts ahead of every lowercase one, which looks like a bug rather than an order. ## Three things this made untrue, fixed with it **Five pieces of copy claimed a ranking.** 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, so those are rewritten. **The structured data implied one too.** The `ItemList` carried `position` with no declared order, which reads as a ranking. It now declares `ItemListOrderAscending`, so the markup says what the table does. **`/api/rankings` is untouched.** Alphabetical is a place-page decision, not a site-wide one — the league table still ranks by metric, and there is a test on each side pinning that. ## Verification Backend 112 · frontend 260 · `tsc --noEmit` clean · `next build` green · 87 e2e journeys. New coverage on both sides of the boundary: the place API returns A–Z and ignores case, the component does not re-sort what the API sent, the JSON-LD declares its order, a live place page lists alphabetically, and `/api/rankings` still orders by score. ## One trade-off worth naming Ranked order was doing SEO work: it put the strongest schools in the first screenful and in the top 20 of the `ItemList`. Alphabetical serves a reader looking for a named school, which is the more common reason to be on the page, but it does mean the markup now leads with whatever starts with "A". If discovery turns out to matter more than lookup once these pages have Search Console data, a sort control — A–Z by default, score optional — would serve both without either page lying about its order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
tudor added 1 commit 2026-08-21 23:09:59 +00:00
feat(places): list schools alphabetically on place pages
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 1m21s
8967966eef
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
Author
Owner

Asking where the unscored schools should sit turned up something worse: some pages were almost entirely made of them.

The bug

The per-phase threshold counted schools that were publishable — a result or an Ofsted grade — while a phase page exists for its results column.

Page Rows carrying a result
/schools/kent/primary 0 of 5
/schools/minehead/primary 1 of 7
/schools/buntingford/primary 1 of 5
/schools/whitley-bay/secondary 2 of 9

44 phase pages were majority-blank. It is the same rule as "no page without a local average", written into the spec as a thin-page control and never extended per phase.

The fix

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 was a table with too little in it to justify a page.

Ordering is unchanged

Pure A–Z, blanks interleaved, as agreed. A school sits where its name says it does. With the majority-blank pages gone the remaining density is around a tenth of rows, which reads fine.

Verification

Backend 116 · frontend 260 · tsc --noEmit clean · next build green · 87 e2e journeys.

Four new tests: a phase with Ofsted grades but no results does not publish, one with results does, a publishing page still lists its unscored schools, and the secondary threshold counts its own metric rather than inheriting the primary one.

Expect the sitemap to shrink

Staging reports 27,052 URLs. After this, expect roughly 26,930 — the 126 variant pages leaving. A drop, but the right direction.

Asking where the unscored schools should sit turned up something worse: some pages were almost entirely made of them. ## The bug The per-phase threshold counted schools that were **publishable** — a result **or** an Ofsted grade — while a phase page exists for its **results column**. | Page | Rows carrying a result | |---|---| | `/schools/kent/primary` | **0 of 5** | | `/schools/minehead/primary` | 1 of 7 | | `/schools/buntingford/primary` | 1 of 5 | | `/schools/whitley-bay/secondary` | 2 of 9 | **44 phase pages were majority-blank.** It is the same rule as *"no page without a local average"*, written into the spec as a thin-page control and never extended per phase. ## The fix 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 was a table with too little in it to justify a page. ## Ordering is unchanged Pure A–Z, blanks interleaved, as agreed. A school sits where its name says it does. With the majority-blank pages gone the remaining density is around a tenth of rows, which reads fine. ## Verification Backend 116 · frontend 260 · `tsc --noEmit` clean · `next build` green · 87 e2e journeys. Four new tests: a phase with Ofsted grades but no results does not publish, one with results does, a publishing page still lists its unscored schools, and the secondary threshold counts its own metric rather than inheriting the primary one. ## Expect the sitemap to shrink Staging reports 27,052 URLs. After this, expect roughly **26,930** — the 126 variant pages leaving. A drop, but the right direction.
tudor added 1 commit 2026-08-21 23:14:32 +00:00
fix(places): a phase page needs results, not merely publishable schools
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 16s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m4s
9cc87c41bb
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

🤖 AI Code Review (Claude Code)

This PR changes place pages (backend/app.py, backend/places.py) to list schools alphabetically instead of ranked by score, and tightens phase-page publishing so a phase (primary/secondary) only publishes when at least MIN_SCHOOLS schools actually carry a result for that phase's metric, not merely any publishable data. Frontend copy/metadata and JSON-LD ItemList ordering were updated to match, and the change is backed by consistent unit, API, component, and e2e tests. The logic is internally consistent (with_result is always a subset of the stored urns, so the existing publishes_phase length check still holds) and no severe issues were found.

✅ No issues found.

## 🤖 AI Code Review (Claude Code) This PR changes place pages (backend/app.py, backend/places.py) to list schools alphabetically instead of ranked by score, and tightens phase-page publishing so a phase (primary/secondary) only publishes when at least MIN_SCHOOLS schools actually carry a result for that phase's metric, not merely any publishable data. Frontend copy/metadata and JSON-LD ItemList ordering were updated to match, and the change is backed by consistent unit, API, component, and e2e tests. The logic is internally consistent (with_result is always a subset of the stored urns, so the existing publishes_phase length check still holds) and no severe issues were found. ✅ No issues found.
tudor merged commit 865a69b54d into main 2026-08-21 23:22:49 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#123