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.
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
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
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
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.
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.
Corpus 27,229 → 25,193. Authority list 182 → 153.
Approach
Filtered at the mart boundary rather than the view layer.
dim_schoolanddim_locationboth excludeTypeOfEstablishmentin {25, 26, 30, 37}, listed once asvars.non_england_school_type_codes. Everything downstream reads those two marts — search, the school page,/api/filters, rankings, Typesense andbuild_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.coalescerather than a bareNOT 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_schoolsguards both directions — no excluded type survives indim_school, anddim_locationholds no URNdim_schoollacks. 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.ymlparses.🤖 Generated with Claude Code
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