feat(data): publish England only, dropping Welsh and overseas establishments #107

Merged
tudor merged 3 commits from feat/england-only-corpus into main 2026-08-20 21:20:13 +00:00
Owner

GIAS ships the whole UK plus overseas and offshore establishments. None 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, and all of them were being submitted to search engines.

Removed Count
Welsh establishments (type 30) 1,569
British schools overseas (37) 316
Offshore — Jersey, Guernsey, IoM, Gibraltar (25) 123
Service children's education (26) 28
Total 2,036

Corpus 27,229 → 25,193. Authority list 182 → 153.

Approach

Filtered 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. Typesense recreates its collection and swaps the alias rather than upserting, so it drops them on its next sync with no extra work.

coalesce rather than a bare NOT IN: a null type code would make the predicate null and drop the row silently. No establishment has a null type today (verified by summing counts across all 38 types = 27,229 exactly), but a future GIAS refresh could ship one and the loss would be invisible.

Verification

The type codes map cleanly onto the authorities in both directions: all 1,569 type-30 schools sit in exactly the 22 Welsh authorities, and those authorities contain nothing else (Cardiff is 144 of 144). Nothing English is caught.

assert_england_only_schools guards both directions — no excluded type survives in dim_school, and dim_location holds no URN dim_school lacks. The second half matters because the API inner-joins them, so the two filters drifting apart would silently shrink the corpus with nothing failing.

Five e2e journeys added covering the filter lists, a dead Welsh authority, a Welsh URL 404ing (paired with an English one resolving, so it cannot pass by the site being down) and the sitemap.

⚠️ Before the e2e gate runs

Staging's marts must be rebuilt by Airflow after the deploy and before the Playwright journeys. This is a dbt change; until the pipeline runs on staging, the marts still hold Welsh schools and the new journeys will fail and block promotion. Agreed this is a manual trigger.

Not verified locally: dbt is not installed here, so the models were not compiled or run. The Jinja render was confirmed by hand and dbt_project.yml parses.

🤖 Generated with Claude Code

https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj

GIAS ships the whole UK plus overseas and offshore establishments. None 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, and all of them were being submitted to search engines. | Removed | Count | |---|---| | Welsh establishments (type 30) | 1,569 | | British schools overseas (37) | 316 | | Offshore — Jersey, Guernsey, IoM, Gibraltar (25) | 123 | | Service children's education (26) | 28 | | **Total** | **2,036** | Corpus 27,229 → 25,193. Authority list 182 → 153. ## Approach Filtered 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. Typesense recreates its collection and swaps the alias rather than upserting, so it drops them on its next sync with no extra work. `coalesce` rather than a bare `NOT IN`: a null type code would make the predicate null and drop the row silently. No establishment has a null type today (verified by summing counts across all 38 types = 27,229 exactly), but a future GIAS refresh could ship one and the loss would be invisible. ## Verification The type codes map cleanly onto the authorities in both directions: all 1,569 type-30 schools sit in exactly the 22 Welsh authorities, and those authorities contain nothing else (Cardiff is 144 of 144). Nothing English is caught. `assert_england_only_schools` guards both directions — no excluded type survives in `dim_school`, and `dim_location` holds no URN `dim_school` lacks. The second half matters because the API inner-joins them, so the two filters drifting apart would silently shrink the corpus with nothing failing. Five e2e journeys added covering the filter lists, a dead Welsh authority, a Welsh URL 404ing (paired with an English one resolving, so it cannot pass by the site being down) and the sitemap. ## ⚠️ Before the e2e gate runs **Staging's marts must be rebuilt by Airflow after the deploy and before the Playwright journeys.** This is a dbt change; until the pipeline runs on staging, the marts still hold Welsh schools and the new journeys will fail and block promotion. Agreed this is a manual trigger. Not verified locally: dbt is not installed here, so the models were not compiled or run. The Jinja render was confirmed by hand and `dbt_project.yml` parses. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
tudor added 3 commits 2026-08-20 21:17:23 +00:00
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
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
docs(seo): correct W1 plan's sitemap child routes
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 35s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
69f2201244
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
tudor merged commit c47fe38971 into main 2026-08-20 21:20:13 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#107