docs(33-05): complete SEO sitemap filter, page registry and site endpoints plan

This commit is contained in:
Jakub Zych
2026-11-04 13:54:09 +01:00
parent 1cc47f6d83
commit eba9567614

View File

@@ -0,0 +1,447 @@
---
phase: 33-seo-output-audit-and-content
plan: 05
subsystem: seo
tags: [sm-seo-plugin, sitemap-filter, page-registry, route-report, site-answer, llms-txt, robots-txt, links, outbound, site-cache-purge, mcp]
requires:
- phase: 33-seo-output-audit-and-content
provides: "33-03/33-04 sm-sitemap-plugin contract (Filter, Listing with HTML settings and KeyFile); 33-01 descriptor Kind/Parents; 33-08 layer contracts for POST /registry/routes and GET /site"
provides:
- "classes.SitemapFilter published as contract.Filter: noindex, canonical-elsewhere and redirected addresses leave the sitemaps; production values, so staging lists the same set (D-95, D-24)"
- "Page registry golem15_seo_pages: SEO › Pages screen, content type seo-page (SEO tab, resolve ok, sitemaps by kind), C21 refusals (D-33)"
- "POST /_seo/api/v1/registry/routes route report: token, 2000-path cap, insert-only, missing state (D-33, P33-20)"
- "GET/POST /_seo/api/v1/pages, PUT/DELETE /pages/{id} and MCP tools seo_list_pages, seo_create_page, seo_update_page, seo_delete_page (D-10)"
- "GET /_seo/api/v1/site (P33-17) and GET /llms.txt (D-25)"
- "robots.txt Sitemap: line from the sitemap plugin's index unless robots_sitemaps is set (D-22)"
- "Links tab (links_new_tab, nofollow_domains, sponsored_domains) and outbound in the resolve answer (P33-16, D-83)"
- "noindex_published only for listed addresses; CMS issue sitemap_blocked; P1 status.excluded_* callouts (D-12, D-24)"
- "Site-cache purge after every SEO write, once per transaction (P33-19)"
- "sm-sitemap-plugin: contract.ListingSettings.TypesOff (additive)"
affects: [33-06, 33-07, 33-08, 33-10, 33-11, 33-12, 33-13, 33-14]
actuals:
tokens: 60200
tasks: 3
commits: 9
plan_head_before: 55f5e5dbb0c1fc50851dd51c9a8f258d265d6024
plan_head_after: df93c860011ee33158657a27f1a82ea1eeef6595
tech-stack:
added: []
patterns:
- "A plugin's own form extension stores a translatable field as a locale map in JSONB (registry url/title), so the record can be matched by any locale's address without the translation writer"
- "Content-type label that is not a phrase key is shown as written (no translator lookup)"
- "Site-cache purge deduplicated per transaction by the transaction's ConnPool (package-level map, reset when it grows past 1024 entries)"
- "Listener transactions that record history run in lagoon.Transaction, so their after-commit work runs"
key-files:
created:
- ../sm-seo-plugin/classes/sitemap_filter.go
- ../sm-seo-plugin/classes/registry.go
- ../sm-seo-plugin/classes/site.go
- ../sm-seo-plugin/classes/llms.go
- ../sm-seo-plugin/models/page.go
- ../sm-seo-plugin/models/page/columns.yaml
- ../sm-seo-plugin/models/page/fields.yaml
- ../sm-seo-plugin/updates/202610280002_create_golem15_seo_pages.go
- ../sm-seo-plugin/updates/202610280003_add_golem15_seo_links_and_llms.go
- ../sm-seo-plugin/menu_item_types.go
- ../sm-seo-plugin/extensions_pages.go
- ../sm-seo-plugin/controllers/pages.go
- ../sm-seo-plugin/controllers/pages/config_list.yaml
- ../sm-seo-plugin/controllers/pages/config_form.yaml
- ../sm-seo-plugin/controllers/pages/config_filter.yaml
- ../sm-seo-plugin/controllers/pages/_page_status.htm
- ../sm-seo-plugin/controllers/api/registry.go
- ../sm-seo-plugin/controllers/api/site.go
- ../sm-seo-plugin/controllers/api/llms.go
- ../sm-seo-plugin/sitemap_filter_test.go
- ../sm-seo-plugin/registry_test.go
modified:
- ../sm-seo-plugin/go.mod
- ../sm-seo-plugin/go.work
- ../sm-seo-plugin/plugin.go
- ../sm-seo-plugin/plugin.yaml
- ../sm-seo-plugin/routes.go
- ../sm-seo-plugin/admin_navigation.go
- ../sm-seo-plugin/extensions.go
- ../sm-seo-plugin/extensions_robots.go
- ../sm-seo-plugin/listeners.go
- ../sm-seo-plugin/mcp.go
- ../sm-seo-plugin/operations.go
- ../sm-seo-plugin/classes/resolver.go
- ../sm-seo-plugin/classes/history.go
- ../sm-seo-plugin/classes/batch.go
- ../sm-seo-plugin/classes/issues.go
- ../sm-seo-plugin/classes/robots.go
- ../sm-seo-plugin/classes/robots_write.go
- ../sm-seo-plugin/classes/settings_write.go
- ../sm-seo-plugin/classes/partials.go
- ../sm-seo-plugin/classes/generate.go
- ../sm-seo-plugin/classes/undo.go
- ../sm-seo-plugin/models/settings.go
- ../sm-seo-plugin/models/settings/fields.yaml
- ../sm-seo-plugin/models/robots.go
- ../sm-seo-plugin/models/revision.go
- ../sm-seo-plugin/extension/_seo_status.htm
- ../sm-seo-plugin/config/config.yaml
- ../sm-seo-plugin/lang/en/lang.yaml
- ../sm-seo-plugin/lang/pl/lang.yaml
- ../sm-seo-plugin/README.md
- ../sm-seo-plugin/fixture_test.go
- ../sm-seo-plugin/settings_test.go
- ../sm-seo-plugin/robots_test.go
- ../sm-seo-plugin/batch_test.go
- ../sm-seo-plugin/operations_test.go
- ../sm-sitemap-plugin/contract/contract.go
- ../sm-sitemap-plugin/classes/listing.go
- ../sm-sitemap-plugin/sitemap_index_test.go
- ../sm-sitemap-plugin/README.md
key-decisions:
- "The sitemap filter also excludes an address the site's address rules would 301 (normalisation) with reason redirect, not only redirect-rule sources"
- "Registry addresses and names per locale are stored in urls/titles JSONB by the plugin's own form extension (fields url and title), not through the translation writer, so Match can find a page by any locale's address in one query"
- "Registry content type label is golem15.seo::lang.pages.type (Site page / Strona witryny): the plain Page label collided with another content type's SEO settings tab"
- "Plan commit groups 2A and 2C are one commit: TestEveryAdminOperationHasTool maps every Pages operation to its tool, so the controller cannot land without the tools"
- "Migration 202610280003 (links and llms columns) landed with the Links commit, ahead of the llms.txt commit that uses its robots columns"
- "llms.txt and the route report: llms.txt answers 404 outside production (non-production lock), and the route report answers 200 with {inserted, updated, skipped, reported_at}"
- "robots.txt names contract.ListingSettings.IndexURL (the sitemap plugin's site address) when robots_sitemaps is unset; the config default became a comment; both consumer apps set robots_sitemaps explicitly and keep their lines"
- "Issue gained an optional severity field, set to error for sitemap_blocked only; the site-audit plans fill the others"
- "sm-sitemap-plugin got the additive contract.ListingSettings.TypesOff so P1 can say status.excluded_type without reading the sitemap's tables (D-95)"
patterns-established:
- "Registry/content-type ownership check: iterate signpost content types except the own one and call Match; Exact means owned"
- "A management write of a content-type record fires cabana.EmitRecordSaved/EmitRecordDeleted itself, so SEO address tracking and the sitemap follow as for an admin save"
requirements-completed: [SC-1, SC-3, D-95, D-24, D-33, D-12, D-22, D-25, D-83, D-94, D-10]
coverage:
- id: D1
description: "A noindex page leaves the XML sitemap with reason noindex, in production and on staging alike"
requirement: "D-95"
verification:
- kind: integration
ref: "../sm-seo-plugin/sitemap_filter_test.go#TestSitemapFilterTracer"
status: pass
human_judgment: false
- id: D2
description: "Canonical elsewhere, redirect and rule-noindex reasons; P1 excluded callouts (plain) for noindex, canonical, redirect and a type switched off"
requirement: "D-24"
verification:
- kind: integration
ref: "../sm-seo-plugin/sitemap_filter_test.go#TestFilterReasons"
status: pass
human_judgment: false
- id: D3
description: "Registry: create, resolve ok, seo-page record with SEO tab, sitemap-pages and sitemap-offers files, hreflang, C21 refusals (owned, taken, invalid, required, parent loop), 301 on address change (admin and API), filter groups and titles, delete removes entries and the sitemap row, management API with 401/403"
requirement: "D-33"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestRegistry"
status: pass
human_judgment: false
- id: D4
description: "Route report: 404 without a token, notfound_token fallback, 401, 422 over 2000 paths, insert-only with derived title, owned and invalid paths skipped, a repeated report changes only reported_at and writes no history, missing row state, P9 states, missing filter"
requirement: "D-33"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestRouteReport"
status: pass
human_judgment: false
- id: D5
description: "Site answer keys, root files (verification and IndexNow key), routes without the registry, noindex header"
requirement: "D-97"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestSiteEndpoint"
status: pass
human_judgment: false
- id: D6
description: "llms.txt off 404, on text/plain with site name, intro, groups and escaped links, excluded and other-locale addresses absent, staging 404"
requirement: "D-25"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestLLMSTxt"
status: pass
human_judgment: false
- id: D7
description: "robots.txt Sitemap line from the index, or the configured robots_sitemaps"
requirement: "D-22"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestRobotsSitemapLine"
status: pass
- kind: integration
ref: "../sm-seo-plugin/robots_test.go#TestRobotsServed"
status: pass
human_judgment: false
- id: D8
description: "Links tab: defaults, line-numbered domain refusal (admin and API), normalised storage, MCP-writable keys, outbound in resolve"
requirement: "D-83"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestSettingsLinks"
status: pass
human_judgment: false
- id: D9
description: "noindex_published only for listed addresses; sitemap_blocked issue with line and rule"
requirement: "D-12"
verification:
- kind: integration
ref: "../sm-seo-plugin/sitemap_filter_test.go#TestNoindexPublishedConflict"
status: pass
- kind: integration
ref: "../sm-seo-plugin/sitemap_filter_test.go#TestSitemapBlockedIssue"
status: pass
human_judgment: false
- id: D10
description: "One purge per committed SEO write (SEO tab, MCP redirect, settings, applied batch); none for a dry run or a refused batch"
requirement: "D-97"
verification:
- kind: integration
ref: "../sm-seo-plugin/registry_test.go#TestPurgeAfterWrite"
status: pass
human_judgment: false
- id: D11
description: "Every Pages operation maps to an MCP tool bound to a route"
requirement: "D-10"
verification:
- kind: unit
ref: "../sm-seo-plugin/operations_test.go#TestEveryAdminOperationHasTool"
status: pass
human_judgment: false
- id: D12
description: "Pages screen, Links tab and the llms.txt fields in the real admin SPA: layout, trigger hiding llms_intro, parent dropdown, missing badge"
verification: []
human_judgment: true
rationale: "Rendering in the embedded admin SPA; the tests cover schema, saves, row states and partial node trees, not the visual layout"
duration: 70min
completed: 2026-11-04
status: complete
---
# Phase 33 Plan 05: SEO plugin sitemap filter, page registry and site-level endpoints Summary
**sm-seo-plugin now decides what the sitemaps list (noindex, canonicalised and redirected addresses leave them), keeps a page registry of the site's own frontend pages as the content type seo-page with an insert-only route report, and gives the layer its site answer, llms.txt, the index Sitemap: line, Links rules in the resolve answer and a site-cache purge after every SEO write.**
## Read first: versions, breaking changes, consumers
- **Versions.** Framework v0.6.0 base, v0.6.1 target (33-01 ruling); the framework got no code here (D-90, `--marker` passed). `sm-seo-plugin` stays at tag v0.1.1 plus these 9 unpushed commits on main; `sm-sitemap-plugin` at v0.1.2 plus 14 unpushed commits (13 from 33-03/33-04, 1 here). Nothing was pushed or tagged.
- **go.mod.** `sm-seo-plugin/go.mod` requires `git.golem15.com/golem15/sm-sitemap-plugin v0.1.2`, and `go.work` replaces it with `../sm-sitemap-plugin`. The v0.1.2 tag has no `contract` package, so a build with `GOWORK=off` fails until 33-14 bumps the require to the new sitemap tag. Consumers resolve the sitemap through their own submodule `use`/`replace` (both apps already do).
- **Changes a consumer will notice when it moves to the next sm-seo-plugin (33-13/33-14):**
- **`Requires()` now names `golem15.sitemap`.** A site with the SEO plugin must activate the sitemap plugin. Both known consumers (sm-demo-app, sm-grzybyfunkcjonalne-app) already list both plugins. Because no existing consumer breaks, I did not stop for a checkpoint.
- The SEO settings get a **Site page** tab (the registry's per-type settings) and a **Links** tab; robots.txt gets the llms.txt fields; SEO › Pages appears in the side menu.
- **robots.txt.** It names the sitemap plugin's index URL when `golem15.seo.robots_sitemaps` is unset. Both consumer apps set that key in `config/golem15.seo.yaml`, so their lines stay as they are.
- **noindex_published.** This batch conflict now fires only for addresses a sitemap file lists right now.
- The resolve answer gains the `outbound` key; no existing key changed.
- **New tables and columns.** Migrations `202610280002` and `202610280003` add the `golem15_seo_pages` table and new columns on settings and robots, all with defaults.
- **Core plugins.** Untouched: user, blog, pages, payment and journal. The vue-* layers were not changed either.
## Performance
- **Duration:** about 70 min (11:40Z to 12:51Z)
- **Tasks:** 3 (tracer, then 2 TDD tasks)
- **Commits:** 9 in sm-seo-plugin, 1 in sm-sitemap-plugin (main, not pushed); planning docs in summercms.go
## Accomplishments
- **Sitemap filter (D-95, D-24).** `classes.SitemapFilter` is published in `Register`. It decides on production values, through a `Resolver` flag that skips the non-production lock. For each address it checks, in order:
- normalisation or a redirect rule → `redirect`;
- effective robots noindex → `noindex`;
- canonical URL not the page itself → `canonical`.
Registry pages go into the `pages`, `offers`, `services` and `landing-pages` groups. `Groups` titles each group per locale.
- **Page registry (D-33, C21).**
- **Model and migration:** the `golem15_seo_pages` model and its migration.
- **SEO › Pages screen:**
- the list has kind, origin and missing filters and the missing row state;
- the form has the P9 partial, and the url/title mltext extension saves each locale and refuses `url_required`, `url_invalid`, `url_taken` and `url_owned` (with type and title), plus `parent_loop`.
- **Content type `seo-page`:** Records, Describe, Match by any locale, Resolve and References. Each page gets the SEO tab, resolves ok, takes part in address tracking with a 301 on change, and is listed in the sitemaps.
- **Route report (P33-20).** `POST /registry/routes` uses a constant-time token and accepts at most 2000 paths. It only inserts or touches `reported_at`, skips owned and invalid paths, and stamps `routes_reported_at`.
- **Management API and MCP (D-10).** Pages CRUD, the four tools and the `AdminOperations` rows.
- **Site answer (P33-17).** Every key the layer's fixture expects except `pagination` and `home_crumb`, which 33-06 adds.
- **llms.txt (D-25).** Built from the sitemaps' listed addresses in the default locale, grouped in the content types' order, with Markdown escaping.
- **robots.txt (D-22).** One `Sitemap:` line naming the index.
- **Links (D-83).** The Links tab, domain validation with line numbers, normalised storage, `outbound` in the resolve answer, and MCP keys.
- **Conflicts and issues (D-12, D-24).** `noindex_published` only for listed addresses. New issue `sitemap_blocked`: severity error, `seo_update_robots`, the line and the rule. P1 shows the "Not in the sitemaps" callouts.
- **Purge (P33-19).** `Service.Record` queues `cooler.PurgeSiteCacheAfterCommit` (kind `seo`) once per transaction.
## Task Commits
sm-seo-plugin (branch main, not pushed):
1. **Task 1 (tracer):** `17b810d` feat: sitemap filter excludes noindex, canonicalised and redirected URLs (tracer)
2. **Task 2:**
- `9e802ec` feat: page registry with its admin screen and signpost type (groups A and C together)
- `26193ae` feat: route report endpoint (group B)
3. **Task 3:**
- `49d3879` feat: links settings and outbound rules in resolve (group B)
- `742a5b9` feat: site endpoint, llms.txt and the sitemap index in robots.txt (group A)
- `59f0457` feat: sitemap-aware batch conflict and the sitemap_blocked issue (group C)
- `2dfc606` feat: purge the site cache after SEO writes (group D)
- `397c126` fix: name a content type's plain label in url_owned without translating it
- `df93c86` docs: registry, site endpoint, llms.txt and links in README (group E)
sm-sitemap-plugin (branch main, not pushed):
- `9e5c393` feat: the listing names the content types switched off
The tracer gate ran in interactive end-of-phase mode: `TestSitemapFilterTracer` was re-run and passed before the expansion tasks.
## Decisions Made
See `key-decisions` in the frontmatter.
## Deviations from Plan
### Adapted to the actual repositories
**1. [Plan shape] Registry translations in JSONB, not the translation writer.**
- **Plan:** P33-18 puts other locales "through the translation writer".
- **Why it cannot work:** a signpost `Match` must find a page by its pl address. The writer offers no reverse lookup, and the test fixture's fake writer is not keyed per record.
- **What I did:**
- `urls` and `titles` are JSONB locale maps (GIN index on `urls`), edited as the mltext fields `url` and `title` of the plugin's own form extension. This is the same approach as 33-03 and 33-04.
- That extension runs before the SEO tab's, so address tracking sees the saved address.
- The url index is unique where `url <> ''`, because the owner row is inserted before the extension writes the address.
- **Commit:** 9e802ec
**2. [Rule 3 - Blocking] Registry type label.**
- **Problem:** the registry type's label `pages.name` ("Page") collided with another content type's "Page" tab in the SEO settings, and cabana refuses two tabs whose labels read the same.
- **Fix:** new copy key `pages.type` (Site page / Strona witryny).
- **Commit:** 9e802ec
**3. [Rule 3 - Blocking] Field types cabana requires.**
- `content_updated_at` is a `*lagoon.Date`: cabana's datepicker in date mode needs that type.
- `parent_id` is `*models.PageRef`, which has `UnmarshalText`, because the dropdown posts text.
- **Commit:** 9e802ec
**4. [Rule 1 - Bug] Row state read from the table.**
- **Bug:** `ListExtendRecords` replaces the origin with its label before `ListRowStates` runs, so the missing state could never match.
- **Fix:** `ListRowStates` reads the state with `models.MissingSQL`.
- **Commit:** 9e802ec
**5. [Plan shape] Commit groups changed.**
- Task 2's groups A and C are one commit, because the parity test needs the Pages tools and the Pages controller together.
- Task 3 order is B (Links, with migration 202610280003, which also holds the llms columns), A, C, D, E.
- Task 2's route report (B) was split from A by staging stripped copies of the files. Both commits passed vet and the Docker suite.
**6. [Rule 2 - Correctness] Additive sitemap contract field.**
- **Why:** P1's `status.excluded_type` needs to know which content types are switched off, and `contract.Listing` had no way to say so. D-95 forbids reading the sitemap's tables.
- **What I did:** added `ListingSettings.TypesOff` in sm-sitemap-plugin, with a test assertion and a README line. The change is additive and outside the plan's file list.
- **Commit:** 9e5c393 in sm-sitemap-plugin
**7. [Rule 2 - Correctness] What the filter treats as a redirect.**
- **Rule:** an address the site's address rules would 301 (normalisation) is excluded as `redirect`, as well as redirect-rule sources.
- **Why:** a listed URL that 301s is a sitemap error.
**8. [Rule 2 - Correctness] Changes to existing code paths.**
- **llms.txt** answers 404 outside production (the non-production lock).
- **Undo** handles the new robots fields `llms_enabled` and `llms_intro`.
- **Transactions:** `RecordDeleted` and `addressesAfterSave` now run in `lagoon.Transaction`. Their history records would otherwise hit "after-commit callback skipped inside unmanaged transaction" and never purge.
- **robots.txt checks:** the file is composed once per batch of address checks, not once per address.
- **Commits:** 742a5b9, 2dfc606, 59f0457
**9. [Plan shape] Copy additions.**
- `pages.type` and `pages.home_title` (the name of a reported `/`: Home / Strona główna).
- `pages.filter_missing_only` and `pages.filter_missing_hide`, read from the C21 shorthand.
- `history.what_page`, `history.kind_page`.
- `Issue.severity` (JSON `severity`) is set only for `sitemap_blocked`.
**10. [Rule 3 - Blocking] Existing tests adjusted to the planned behaviour.**
- `TestSettingsTypeTabs`: the Site page and Links tabs.
- `TestRobotsServed` and `TestRobotsScreen`: the Sitemap line now names the sitemap's index, which is on app.url in the fixture, and the llms fields are on the screen.
- `TestBatch`: rebuilds the sitemap first, so its address is listed.
- `TestPluginManifest`: plugin.yaml regenerated with `requires: golem15.sitemap`.
- The fixture boots `golem15.sitemap` whenever `golem15.seo` is booted.
**11. [Rule 1 - Bug] Plain content type labels.**
- **Bug:** `url_owned` passed a plain content type label to the translator, which logged it as a missing key.
- **Fix:** labels without `::` are used as written.
- **Commit:** 397c126
### TDD note
The plan is `type: execute`. Each task's tests were written before or alongside its code. They were run once the group compiled, and the failures fixed before the group's commit:
- the "/pl/pages/first" substring;
- the file group name;
- the NOT NULL urls column;
- the datepicker type;
- the dropdown fill;
- the row-state ordering.
No separate RED commit was made: the plan requires green tests at every commit, as in 33-04.
### Auth gates
None.
**Total deviations:** 11. Two are storage or contract shapes, four are blocking fixes, three are correctness additions, one is commit grouping and one is copy additions.
**Impact on plan:** every must-have truth is met. The visible additions to UI-SPEC are the Site page settings tab label and the plain-callout variant of P1.
## Issues Encountered
- **Shared file group.** A content type whose sitemap file group is also `pages` (the fixture's `acme-page`) shares the file and the HTML-sitemap group with the registry's `page` kind. The README states it.
- **Route report size.** The framework's default body limit (`http.body_limits.default_bytes`) can refuse a report larger than 1 MB before the 2000-path cap applies. The layer's reports of static routes are far smaller.
- **Registry history.** Undo of a registry page's history row answers `history.undo_unsupported`; registry undo was not planned.
- `go test ./... -run X` prints "no tests to run" for subpackages while the named root tests pass (as in 33-02 to 33-04).
## Known Stubs
None.
## Threat Flags
None outside the plan's threat model:
- **T-33-18** (route report): constant-time token compare, a 2000-path cap, paths of at most 2048 bytes, insert-only, owned paths skipped; covered by `TestRouteReport`.
- **T-33-19** (`/site`): only public facts. The test asserts that no token and no registry type appear.
- **T-33-20** (llms.txt): built from the listed addresses only, off by default, 404 outside production.
- **T-33-21** (Pages admin and MCP): `golem15.seo.manage` on every route and screen. Covered by the parity test and the 401/403 cases.
- **T-33-22** (domain lists): host names only, no scheme or path, validated line by line.
- **T-33-23** (purge): one purge per transaction after commit, coalesced by the framework purger.
## Verification run
- **sm-seo-plugin, after every commit:**
- `go vet ./...` and `go test -short ./...`: green;
- Docker `go test ./... -count=1`: green, 67 top-level tests.
- **Named tests:** all pass.
- TestSitemapFilterTracer, TestFilterReasons, TestNoindexPublishedConflict, TestSitemapBlockedIssue.
- TestRegistry, TestRouteReport, TestSiteEndpoint, TestLLMSTxt, TestRobotsSitemapLine, TestSettingsLinks, TestPurgeAfterWrite.
- TestEveryAdminOperationHasTool, TestMCPReadToolsAreGET.
- TestResolverPrecedence and the Phase 32 suites.
- **sm-sitemap-plugin:** vet, short and the Docker suite green (sitemap without SEO).
- **From summercms.go:**
- `bash scripts/check-phase33.sh --plugins --marker`: PASS (vet and short tests of sitemap, seo and journal; vitest 77 and 244; marker clean).
- `bash scripts/check-phase33.sh --docker`: PASS (29 sitemap, 67 seo, 124 journal top-level tests).
- `--layers` was not run: it is known to be broken and is logged in deferred-items.md. No layer was changed.
- `--go` and `--consumers` were not run: no framework code and no consumer pointer changed in this plan.
- **Acceptance greps:**
- one line or more each for `golem15.sitemap` (plugin.go), `sm-sitemap-plugin/contract` (sitemap_filter.go and plugin.go), `sm-sitemap-plugin =>` (go.work), `"seo-page"` (registry.go), `/registry/routes` (routes.go), `subtle.ConstantTimeCompare` (api/registry.go), and `sitemap_blocked` (issues.go);
- four lines for the four page tools in mcp.go;
- `"/llms.txt"` and `"/site"` in routes.go;
- `links_new_tab` in settings_write.go, fields.yaml and resolver.go (via `OutboundOf`/`LinksNewTab`).
## User Setup Required
None for this plan. For sites (33-13):
- **Route report:** set `golem15.seo.route_report_token`, or rely on `notfound_token`, and give the layer the same token.
- **Purge:** configure `site_cache.purge_url` and `site_cache.purge_token`.
- **Sitemap site address:** set the sitemap's site address (setting or `golem15.sitemap.site_url`) to the public origin, because robots.txt now names its index.
## Next Phase Readiness
- **33-06:** adds `pagination` and `home_crumb` to `classes.SiteAnswer`, and further resolve keys next to `outbound`.
- **33-08's layer** can drop its fixtures for `/registry/routes` and `/site`: the plugin now answers in the shapes the layer reads.
- **33-14:** bump the `sm-sitemap-plugin` require to its new tag and tag the next minor of both plugins.
## Self-Check: PASSED
- FOUND: every file in key-files.created (checked with `[ -f ]`).
- FOUND commits in sm-seo-plugin: 17b810d, 9e802ec, 26193ae, 49d3879, 742a5b9, 59f0457, 2dfc606, 397c126, df93c86. `git rev-list --count 55f5e5d..HEAD` = 9.
- FOUND commit in sm-sitemap-plugin: 9e5c393.
- No uncommitted changes in sm-seo-plugin or sm-sitemap-plugin.
---
*Phase: 33-seo-output-audit-and-content*
*Completed: 2026-11-04*