docs(33-06): complete structured data, breadcrumbs, hreflang and pagination plan

- Task 4 answers: chromedp v0.21.0 and gen2brain/webp v0.6.4 approved
- the tracer resolve answer for the layer fixtures
- deferred: a repeater cannot carry a trigger (local_hours)
This commit is contained in:
Jakub Zych
2026-11-04 14:44:16 +01:00
parent bd36b18729
commit 00c8fac080
2 changed files with 480 additions and 0 deletions

View File

@@ -0,0 +1,473 @@
---
phase: 33-seo-output-audit-and-content
plan: 06
subsystem: seo
tags: [sm-seo-plugin, json-ld, schema-org, breadcrumbs, hreflang, pagination, publisher, local-business, custom-elements, mcp, package-legitimacy]
requires:
- phase: 33-seo-output-audit-and-content
provides: "33-01 descriptor facts (Kind, Parents, Authors, PublishedAt, Images, Videos); 33-05 resolve answer with outbound, the /site answer, the page registry kinds"
provides:
- "One JSON-LD @graph per ok resolve answer (classes/schema Build), nodes linked by the P33-22 @ids"
- "Resolve keys html_lang, alternates, jsonld, breadcrumbs, pagination, seo.og.locale, seo.og.locale_alternates (additive)"
- "Publisher, Local business, Pagination tabs and the home crumb setting; P8 publisher preview"
- "Pagination forms (query or path) with one-hop 301s, invalid pages, suffixes with %pages% left for the layer"
- "GET /site pagination {form, param, segments} and home_crumb {name, url, names}"
- "SEO tab: Primary category (seoparent), Structured data (seoschema, E8), preview (E9, widget action seo_graph); P1 primary_gone, P2 schema type and trail rows"
- "GET /_seo/api/v1/graph and MCP seo_get_graph; primary_parent and schema on seo_set_url_seo and apply_batch"
- "Task 4 checkpoint answers: chromedp v0.21.0 and gen2brain/webp v0.6.4 approved"
affects: [33-07, 33-08, 33-09, 33-10, 33-11, 33-12, 33-13, 33-14]
actuals:
tokens: 110000
tasks: 4
commits: 6
plan_head_before: df93c860011ee33158657a27f1a82ea1eeef6595
plan_head_after: d38049a
tech-stack:
added: []
patterns:
- "classes/schema is pure (no plugin imports): the resolver, the admin preview, the publisher preview and the MCP tool share Build and Validate"
- "Per-record structured data values live in the entry's data (primary_parent, schema) on every locale row, like the social image"
- "Pagination is folded into Resolve before the strict check, so normalisation, strict URLs and the page form reach the final address in one 301"
key-files:
created:
- ../sm-seo-plugin/classes/schema/graph.go
- ../sm-seo-plugin/classes/schema/nodes.go
- ../sm-seo-plugin/classes/schema/validate.go
- ../sm-seo-plugin/classes/schema/custom.go
- ../sm-seo-plugin/classes/breadcrumbs.go
- ../sm-seo-plugin/classes/alternates.go
- ../sm-seo-plugin/classes/pagination.go
- ../sm-seo-plugin/classes/structured.go
- ../sm-seo-plugin/updates/202610280004_add_golem15_seo_publisher_pagination.go
- ../sm-seo-plugin/controllers/settings/_publisher_preview.htm
- ../sm-seo-plugin/controllers/api/graph.go
- ../sm-seo-plugin/assets/src/elements/schema.ts
- ../sm-seo-plugin/assets/src/elements/graph.ts
- ../sm-seo-plugin/assets/src/elements/parent.ts
- ../sm-seo-plugin/tests/elements/schema.test.ts
- ../sm-seo-plugin/structured_data_test.go
modified:
- ../sm-seo-plugin/classes/resolver.go
- ../sm-seo-plugin/classes/site.go
- ../sm-seo-plugin/classes/settings_write.go
- ../sm-seo-plugin/classes/sitemap_filter.go
- ../sm-seo-plugin/classes/entries.go
- ../sm-seo-plugin/classes/history.go
- ../sm-seo-plugin/classes/owner.go
- ../sm-seo-plugin/classes/partials.go
- ../sm-seo-plugin/classes/field_types.go
- ../sm-seo-plugin/models/settings.go
- ../sm-seo-plugin/models/settings/fields.yaml
- ../sm-seo-plugin/controllers/settings.go
- ../sm-seo-plugin/controllers/api/settings.go
- ../sm-seo-plugin/controllers/api/seo.go
- ../sm-seo-plugin/extensions.go
- ../sm-seo-plugin/routes.go
- ../sm-seo-plugin/mcp.go
- ../sm-seo-plugin/operations.go
- ../sm-seo-plugin/config/config.yaml
- ../sm-seo-plugin/assets/src/elements/index.ts
- ../sm-seo-plugin/assets/src/elements/labels.ts
- ../sm-seo-plugin/assets/src/elements/styles.css
- ../sm-seo-plugin/assets/dist/seo.js
- ../sm-seo-plugin/assets/dist/seo.css
- ../sm-seo-plugin/lang/en/lang.yaml
- ../sm-seo-plugin/lang/pl/lang.yaml
- ../sm-seo-plugin/README.md
- ../sm-seo-plugin/settings_test.go
- ../sm-seo-plugin/seo_tab_test.go
key-decisions:
- "Task 4 (Jakub 2026-11-04): github.com/chromedp/chromedp approved, pin v0.21.0 (v0.21.1 and v0.22.0 exist but are under two weeks old)"
- "Task 4 (Jakub 2026-11-04): github.com/gen2brain/webp approved, pin v0.6.4 (its fallback is libwebp transpiled to Go with wasm2go, not wazero)"
- "Every graph carries the full WebSite and publisher nodes, not only the home page, so each page's graph is self-contained (the tracer expects both on a post)"
- "home_crumb in the site answer is {name, url, names}: name and url are what the committed 33-08 layer reads; names adds the per-locale names the plan asked for"
- "The graph is skipped in the sitemap filter's production resolver (it decides on robots and canonical only)"
- "Primary category is offered for article kinds only; the value is <type>:<id> of one of the descriptor's Parents"
- "Page 2 and later always uses its own canonical (D-21), and its hreflang alternates point at the same page of each translation"
patterns-established:
- "A structured data refusal is a schema.Error code mapped to the schema.* copy, shared by the field type Decode, seo_set_url_seo and the graph tool"
requirements-completed: [SC-2, SC-3, D-43, D-44, D-45, D-46, D-03, D-86, D-55, D-56, D-57, D-40, D-41, D-42, D-21, D-94, D-10, D-70, D-85]
coverage:
- id: D1
description: "One @graph with WebPage, BlogPosting (headline, author, dates, image, wordCount, articleSection), BreadcrumbList, VideoObject, WebSite and Organization linked by @id; breadcrumbs equal the BreadcrumbList items; alternates en, pl, x-default; none for a single-locale record"
requirement: "D-43"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer"
status: pass
human_judgment: false
- id: D2
description: "Breadcrumbs through the primary parent (stored, else first; recursive; unpublished parents skipped), per-locale home crumb, home page trail"
requirement: "D-86"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer/TestBreadcrumbs"
status: pass
human_judgment: false
- id: D3
description: "html_lang, og:locale and alternates from one source; paged alternates"
requirement: "D-55"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTracer/TestAlternates"
status: pass
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestPagedAlternates"
status: pass
human_judgment: false
- id: D4
description: "Publisher and Local business settings with every C20 refusal (422) in the admin and the API; the local business node; a person publisher; P8 with the logo finding; home crumb name"
requirement: "D-45"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestPublisherSettings"
status: pass
human_judgment: false
- id: D5
description: "Pagination query and path forms, one-hop 301s with normalisation, invalid pages, page 2+ canonical, robots and suffixes, /site pagination and home_crumb, segment and suffix refusals"
requirement: "D-40"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestPagination"
status: pass
human_judgment: false
- id: D6
description: "Custom JSON-LD refusals (invalid JSON with line/column, not an object, 16 KB, depth 8, script tag, type, price, currency), merge with the schema.merged notice, appended nodes, re-serialised storage on every locale row"
requirement: "D-44"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestCustomJSONLD"
status: pass
human_judgment: false
- id: D7
description: "Collection, profile, registry offer (Service with offer), contact page, home SearchAction; no e-commerce node"
requirement: "D-43"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestGraphPerKind"
status: pass
human_judgment: false
- id: D8
description: "GET /graph with findings and rules, unsaved schema preview, 401/403, primary_parent via seo_set_url_seo and apply_batch (dry run), P1 primary_gone, P2 rows"
requirement: "D-46"
verification:
- kind: integration
ref: "../sm-seo-plugin/structured_data_test.go#TestGraphTool"
status: pass
- kind: unit
ref: "../sm-seo-plugin/operations_test.go#TestEveryAdminOperationHasTool"
status: pass
human_judgment: false
- id: D9
description: "E8, E9 and the parent element: type options, Service grid and price backstop, JSON status with line and column, 16 KB, nodes and findings, allGood, 800 ms re-check, locale switch, failure line"
requirement: "D-44"
verification:
- kind: unit
ref: "../sm-seo-plugin/tests/elements/schema.test.ts"
status: pass
human_judgment: false
- id: D10
description: "The new settings tabs, E8, E9 and the primary category select in the real admin SPA: layout, spacing, the local business switch hiding its fields"
verification: []
human_judgment: true
rationale: "Rendering in the embedded admin SPA; the tests cover schemas, saves and element DOM in happy-dom, not the visual layout"
duration: 105min
completed: 2026-11-04
status: complete
---
# Phase 33 Plan 06: Structured data, breadcrumbs, hreflang and pagination Summary
**sm-seo-plugin now answers every page with one JSON-LD @graph (page, article, profile, service, breadcrumbs, videos, WebSite, publisher), its breadcrumb trail through the primary category, hreflang with x-default, and paginated addresses in the site's form, with publisher and local business settings, the Structured data fields and graph preview in the SEO tab, and seo_get_graph through MCP.**
## Task 4 answers: Go module legitimacy (Jakub, 2026-11-04)
33-10 and 33-11 pin exactly these versions, each after its decision note:
| Module | Answer | Pin | Checked by the orchestrator |
|--------|--------|-----|-----------------------------|
| `github.com/chromedp/chromedp` | **approved** | **v0.21.0** | Owned by the chromedp organisation, MIT. Dependencies: `github.com/chromedp/cdproto` and `github.com/go-json-experiment/json`. v0.21.1 (2026-11-01) and v0.22.0 (2026-11-03) exist, but both are less than two weeks old, so v0.21.0 is pinned. |
| `github.com/gen2brain/webp` | **approved** | **v0.6.4** | MIT. The only dependency is `github.com/ebitengine/purego`. Correction to the plan: the fallback without a system libwebp is libwebp transpiled to Go with wasm2go, not a WASM build run through wazero. |
Nothing was installed in this plan; no go.mod changed.
## Read first: versions, breaking changes, consumers
- **Versions.** The framework base is v0.6.0 and the target v0.6.1; this plan changed no framework code (`--marker` passed). `sm-seo-plugin` stays at tag v0.1.1 and now has 15 unpushed commits on main: 9 from 33-05 and 6 from this plan. The require on summercms and the go.work/replace setup are as 33-05 left them. Nothing was pushed or tagged.
- **Resolve answer.** Only additive keys:
- top level: `html_lang`, `alternates`, `jsonld`, `breadcrumbs`, `pagination`;
- inside `seo.og`: `locale`, `locale_alternates`.
No existing key changed. The new top-level keys are left out of the JSON when they are empty, and the layer reads a missing key as empty.
- **What a consumer will notice when moving to the next sm-seo-plugin:**
- **Migration 202610280004** adds settings columns, all with defaults.
- **New settings tabs:** Pagination, Publisher and Local business, plus a home crumb field on the General tab.
- **New SEO tab fields:** Structured data and its preview on every content type, and Primary category on article types.
- **`?page=1` now redirects.** A request for `?page=1`, or for `/strona/N` while the site uses the query form, now answers 301 where it used to answer ok. Page 2 and later now get suffixes on the title and description. This is what D-41 asks for, but sites will see it.
- **Core plugins.** Untouched: user, blog, pages, payment and journal. The vue-* layers were not changed.
## Tracer resolve answer (TestGraphTracer, `/blog/first-post`)
For comparison with the layer fixtures of 33-07 to 33-09:
```json
{
"action": "ok",
"alternates": [
{"hreflang": "en", "url": "https://www.example.test/blog/first-post"},
{"hreflang": "pl", "url": "https://www.example.test/pl/blog/pierwszy-wpis"},
{"hreflang": "x-default", "url": "https://www.example.test/blog/first-post"}
],
"breadcrumbs": [
{"name": "Home", "path": "/", "url": "https://www.example.test/"},
{"name": "Recipes", "path": "/blog/category/recipes", "url": "https://www.example.test/blog/category/recipes"},
{"name": "First post", "path": "/blog/first-post", "url": "https://www.example.test/blog/first-post"}
],
"html_lang": "en",
"jsonld": {
"@context": "https://schema.org",
"@graph": [
{"@id": "https://www.example.test/blog/first-post#webpage", "@type": "WebPage",
"breadcrumb": {"@id": "https://www.example.test/blog/first-post#breadcrumb"},
"dateModified": "2026-10-05T12:30:00Z", "datePublished": "2026-10-01T08:00:00Z",
"description": "About the first post", "inLanguage": "en",
"isPartOf": {"@id": "https://www.example.test/#website"}, "name": "First post",
"primaryImageOfPage": {"@type": "ImageObject", "url": "https://www.example.test/media/first.jpg"},
"url": "https://www.example.test/blog/first-post"},
{"@id": "https://www.example.test/blog/first-post#article", "@type": "BlogPosting",
"articleSection": "Recipes",
"author": [{"@id": "https://www.example.test/blog/author/jan#profile", "@type": "Person", "name": "Jan Kowalski", "url": "https://www.example.test/blog/author/jan"}],
"dateModified": "2026-10-05T12:30:00Z", "datePublished": "2026-10-01T08:00:00Z",
"description": "About the first post", "headline": "First post",
"image": ["https://www.example.test/media/first.jpg"], "inLanguage": "en",
"isPartOf": {"@id": "https://www.example.test/blog/first-post#webpage"},
"mainEntityOfPage": {"@id": "https://www.example.test/blog/first-post#webpage"},
"publisher": {"@id": "https://www.example.test/#organization"}, "wordCount": 420},
{"@id": "https://www.example.test/blog/first-post#breadcrumb", "@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "item": "https://www.example.test/", "name": "Home", "position": 1},
{"@type": "ListItem", "item": "https://www.example.test/blog/category/recipes", "name": "Recipes", "position": 2},
{"@type": "ListItem", "item": "https://www.example.test/blog/first-post", "name": "First post", "position": 3}]},
{"@id": "https://www.example.test/blog/first-post#video-1", "@type": "VideoObject",
"description": "About the first post", "embedUrl": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"isPartOf": {"@id": "https://www.example.test/blog/first-post#webpage"}, "name": "First post",
"thumbnailUrl": ["https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"],
"uploadDate": "2026-10-01T08:00:00Z", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
{"@id": "https://www.example.test/#website", "@type": "WebSite", "inLanguage": "en", "name": "Acme Blog",
"publisher": {"@id": "https://www.example.test/#organization"}, "url": "https://www.example.test/"},
{"@id": "https://www.example.test/#organization", "@type": "Organization", "name": "Acme", "url": "https://www.example.test/"}
]
},
"locale": "en",
"outbound": {"new_tab": true, "nofollow_domains": [], "sponsored_domains": []},
"seo": {
"canonical": "https://www.example.test/blog/first-post",
"description": "About the first post",
"og": {"description": "About the first post", "image": "https://www.example.test/media/first.jpg",
"locale": "en_US", "locale_alternates": ["pl_PL"], "title": "First post | Acme Blog",
"url": "https://www.example.test/blog/first-post"},
"robots": "index, follow", "title": "First post | Acme Blog",
"twitter": {"card": "summary_large_image", "site": ""}, "verification": [], "x_robots_tag": "index, follow"
},
"staging": false,
"status": 200,
"url": "https://www.example.test/blog/first-post"
}
```
A page-3 answer in the query form adds `"pagination": {"form": "query", "invalid": false, "page": 3, "param": "page", "segment": ""}`. Its title is `Recipes – page 3 of %pages% | Acme Blog` and its description ends with `Page 3 of %pages%.` A garbage page value answers `not_found` with `"pagination": {..., "invalid": true}`.
## Performance
- **Duration:** about 105 min (11:56Z to 13:41Z)
- **Tasks:** 4: a tracer, two TDD tasks and the checkpoint, which Jakub answered in parallel.
- **Commits:** 6 in sm-seo-plugin (main, not pushed). The planning docs are committed in summercms.go.
## Accomplishments
- **classes/schema.** A pure package with:
- `Build`: one node builder per type;
- `Validate`: Google's required and recommended properties per type, with `rules` false for types Google has no rich-result rules for;
- `Normalize`, `DecodeCustom` and `Merge`: the C17 server rules for the structured data value and custom JSON-LD.
- **Resolve.**
- The breadcrumb trail goes through the primary parent: the stored choice, else the first parent, recursively up to 6 levels.
- Alternates list each translated locale, include x-default, and come with `html_lang` and the og locales.
- Pagination: the page form is parsed and folded into the one-hop decision, with invalid pages, suffixes, a self canonical and paged alternates.
- **Settings.**
- New Publisher, Local business and Pagination tabs and the home crumb field.
- The four-place pattern (model, writer, API, form) is applied to every new column, with all C20 refusals.
- P8 shows the generated publisher node with its findings.
- **SEO tab.**
- E8 (`seoschema`), E9 (widget `seo_graph`) and Primary category (`seoparent`).
- The `primary_parent` and `schema` values are stored in the entry data on every locale row.
- Saves answer the `schema.merged` notice.
- P1 gains `primary_gone`; P2 gains rows for the schema type and the breadcrumb trail.
- **MCP.**
- `seo_get_graph` (`GET /_seo/api/v1/graph?url=&schema=`).
- `seo_set_url_seo` and `apply_batch` accept `primary_parent` and `schema`.
- `seo_update_settings` documents every new key.
- The parity row `widget_action:seo_graph` maps to `get_graph`.
## Task Commits
sm-seo-plugin (branch main, not pushed):
1. **Task 1 (tracer):** `ea2ce24` feat: JSON-LD graph, breadcrumbs and hreflang in the resolve answer (tracer)
2. **Task 2:**
- `fde5024` feat: publisher, local business and home crumb settings (group A)
- `032d3da` feat: pagination forms and suffixes in resolve and site (group B)
3. **Task 3:**
- `17da132` feat: graph validation and custom JSON-LD (groups A and C, plus the Go side of B)
- `10ff628` feat: structured data, graph preview and primary category in the SEO tab (group B: elements and dist)
- `d38049a` docs: structured data, breadcrumbs, hreflang and pagination in README (group D)
4. **Task 4:** no commit. The checkpoint answers are recorded at the top of this file.
The tracer gate ran in interactive end-of-phase mode. `TestGraphTracer` was re-run and passed before the expansion tasks started.
## Decisions Made
See `key-decisions` in the frontmatter.
## Deviations from Plan
### Adapted to the actual repositories
**1. [Plan shape] Fake content plugin `acme.magazine` instead of extending the fixture's pages plugin.**
- **Why:** `addresses_test.go` already registers an `acme.blog` plugin.
- **What I did:** the tracer and the per-kind tests use an in-memory plugin `acme.magazine` with three types: `acme-story` (article), `acme-section` (collection) and `acme-writer` (profile). It uses every 33-01 fact. fixture_test.go is unchanged.
**2. [Plan shape] Commit groups.**
- **Validator moved earlier:** validate.go landed in Task 2 group A, because P8 needs the findings.
- **Groups 3A and 3C are one commit**, together with the Go side of 3B (the SEO tab fields, the action, P1 and P2). The operations parity test binds the new `seo_graph` widget action to its tool, and the Go tests drive the SEO tab.
- **Group 3B** is the element commit: elements, labels, styles, dist and vitest.
**3. [Plan shape] `home_crumb` shape.**
- **The conflict:** the plan says "home_crumb per locale". The committed 33-08 layer reads `home_crumb.name` and `.url` (`SeoHtmlSitemapPage.vue`, `types.ts`).
- **What I did:** the site answer is `{"name": <default locale>, "url": <site>/, "names": {<locale>: <name>}}`. This serves both.
**4. [Rule 3 - Blocking] Repeater rows cannot have a trigger.**
- **The limit:** cabana refuses `trigger` on `type: repeater`.
- **Effect:** `local_hours` stays visible when the Local business switch is off. Every other local field hides as planned.
- **Logged** in deferred-items.md.
**5. [Rule 2 - Correctness] Additions outside the plan's list.**
- `classes/structured.go`: input composition, graph answers, the E9 action and the merge notices.
- `classes/owner.go`: `seo_set_url_seo` checks the new keys.
- `classes/history.go`: data values in history.
- `classes/sitemap_filter.go`: signature only. The production resolver skips the graph.
- New copy keys:
- `settings.url_invalid` and `settings.email_invalid` (website and email checks, T-33-25);
- `api.record_only`;
- `schema.field_price`, `schema.field_currency`, `schema.price_invalid` and `schema.currency_invalid`;
- `graphFailed` in E9 (the host gives the element no server message).
- Hreflang of page 2 and later points at the same page of each translation.
**6. [Plan shape] Files not touched.** These files from the plan's list needed no change:
- `updates/registry.go` (migrations register themselves);
- `controllers/settings/config_form.yaml`;
- `extension/_seo_status.htm` and `_seo_effective.htm`, which render the new callout and rows unchanged.
**7. [Plan shape] `field_types.go` (root) has no new lines.** The new types are registered through `classes.FieldTypes`, which the root file returns. The acceptance grep counts 2 lines in `classes/field_types.go`.
**8. [Rule 1 - Bug] The seoparent value round trip.**
- **Bug:** the admin sends the loaded `{selected, options}` object back when the editor leaves the field untouched.
- **Fix:** Decode reads `selected` from it.
**9. [Plan shape] E8 default for registry offers.** The element cannot know a registry page's kind, so the default option of a page-kind record reads "Default (WebPage)" even when the server's default is Service (offer and service kinds). The server applies Service correctly.
### TDD note
The plan is `type: execute` with `tdd="true"` tasks. Each group's tests were written alongside its code and run before the commit. The failures fixed before committing were:
- the local type copy keys;
- a nil `Settings{}` in TestTemplates;
- the tab expectations;
- the site JSON field order;
- the `<` check.
No separate RED commit was made: the plan needs green tests at every commit, as in 33-04 and 33-05.
### Auth gates
None.
**Total deviations:** 9. Five are plan shape, one is a blocking framework limit, one is a correctness addition, one is a bug and one is a UI default note.
**Impact on plan:** every must-have truth is met. The only exception is the visual hiding of the opening-hours repeater.
## Issues Encountered
- **Unvalidated per-locale values.** For the translatable settings (`page_segment`, the suffixes, `breadcrumb_home`, `publisher_name`), the admin's other-locale values go through the translation writer without Decode, so only the default locale's value is validated on save. An invalid segment in another locale falls back to the default segment when it is read.
- **Undo does not restore the new data values.** Undo of an entry history row restores the Phase 32 columns, not `primary_parent` or `schema`. History does record both.
- **`go test ./... -run X`** prints "no tests to run" for subpackages while the named root tests pass, as in 33-02 to 33-05.
## Known Stubs
None.
## Threat Flags
None outside the plan's threat model:
- **T-33-24** (custom JSON-LD): parse, objects only, 16 KB and depth 8, `<script` refused, re-serialised with `<` escaped. Covered by `TestCustomJSONLD`.
- **T-33-25** (sameAs and the website): https absolute URLs only, line-numbered refusal. Covered by `TestPublisherSettings`.
- **T-33-26** (breadcrumbs and alternates): built only from descriptors and settings.
- **T-33-27** (graph tool): the manage wrapper applies, with 401 and 403 in `TestGraphTool`.
- **T-33-28** (the parent chain): capped at 6 levels, cycle-safe.
- **T-33-SC** (installs): no package was installed.
## Verification run
- **sm-seo-plugin, after every commit:**
- `go vet ./...` and `go test -short ./...`: green;
- Docker `go test ./... -count=1`: green.
- **Named tests:** all pass.
- TestGraphTracer (with TestAlternates and TestBreadcrumbs), TestPublisherSettings, TestPagination (including its TestSiteEndpoint subtest), TestPagedAlternates, TestCustomJSONLD, TestGraphPerKind, TestGraphTool.
- TestSiteEndpoint, TestNormalize, TestSEOTab, TestEveryAdminOperationHasTool, TestLangKeysComplete, TestSettingsTypeTabs, TestResolverPrecedence and the Phase 32 and 33-05 suites.
- **Elements:**
- `npm run test`: 87 passed (10 new);
- `npm run build`, then `scripts/check-dist.sh`: matches.
- **From summercms.go:** `bash scripts/check-phase33.sh --plugins --marker`: PASS (vitest 87 and 244; marker clean).
- **Not run here:**
- `--layers`: known broken (deferred-items.md).
- `--docker`: it would also run sm-journal-plugin, which 33-09 is editing at the same time. The SEO Docker suite was run directly instead.
- `--go` and `--consumers`: no framework or consumer change.
- **Acceptance greps:**
- `func Build(`: 1;
- the four resolve json tags: 4;
- the settings_write keys: 15;
- `%pages%`: 3;
- `"pagination"`/`"home_crumb"` in site.go: 2;
- `"seoschema"`/`"seoparent"`: 2;
- the three element tags in index.ts: 3;
- `"get_graph"`: 1;
- `Product` in classes/schema: 0.
## User Setup Required
None for this plan. Optional per site:
- `golem15.seo.search_url` turns on the SearchAction.
- `golem15.seo.og_locales` overrides the og:locale values.
- The Publisher tab fills the publisher node; without it, the site name is used.
## Next Phase Readiness
- **33-07, 33-08 and 33-09:** the backend now answers the keys their fixtures expect (see the tracer answer above). Each layer fixture's `home_crumb` should gain `names`, or the layer can keep reading `name`.
- **33-10 and 33-11:** pin chromedp v0.21.0 and gen2brain/webp v0.6.4, each after its decision note.
- **33-14:** tag the next sm-seo-plugin minor, which includes 33-05 and this plan.
## Self-Check: PASSED
- FOUND: every file in key-files.created (checked with `[ -f ]`).
- FOUND commits in sm-seo-plugin: ea2ce24, fde5024, 032d3da, 17da132, 10ff628, d38049a. `git rev-list --count df93c86..HEAD` = 6.
- No uncommitted changes in sm-seo-plugin.
---
*Phase: 33-seo-output-audit-and-content*
*Completed: 2026-11-04*

View File

@@ -52,3 +52,10 @@ Out-of-scope discoveries logged during execution; not fixed in the plan that fou
status: open
- **Possible fix:** swap the two in the fixture API and adjust the expectation lines that name a neighbour.
- **Why deferred:** pre-existing, not caused by 33-09; changing it changes existing expectations.
## From 33-06 (sm-seo-plugin)
- **A repeater cannot carry a trigger (framework).** cabana refuses `trigger` on `type: repeater`, so the SEO settings' opening hours (`local_hours`, UI-SPEC C20) stay visible while the Local business switch is off; every other local field hides.
status: open
- **Possible fix:** allow `trigger` (hide/disable) on repeater fields in cabana and the SPA, then add it to `local_hours` in `sm-seo-plugin/models/settings/fields.yaml`.
- **Why deferred:** 33-06 commits only to sm-seo-plugin; the gap is generic, additive framework work.