From 930d91a3c1df3da828c5108e984ffae700708635 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 11:09:44 +0100 Subject: [PATCH] docs(32-03): complete SEO plugin core plan --- .planning/ROADMAP.md | 6 +- .../phases/32-seo-foundation/32-03-SUMMARY.md | 364 ++++++++++++++++++ 2 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/32-seo-foundation/32-03-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index baa9c83d..b72e4bff 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1595,7 +1595,7 @@ Plans: 4. Every SEO operation in the admin is available as an MCP tool, and a batch change can be previewed with dry-run and undone from history. 5. The framework release with the seams is tagged, and the new code has unit tests, delivered in the phase's last plan. -**Plans:** 2/6 plans executed +**Plans:** 3/6 plans executed Plans: **Wave 1** @@ -1605,7 +1605,7 @@ Plans: - [x] 32-02-PLAN.md — D-92 record events and D-93 content descriptors; Journal descriptors, journal-tag, API event emission, translated slugs **Wave 3** *(blocked on Wave 2 completion)* -- [ ] 32-03-PLAN.md — sm-seo-plugin core: SEO tab, URL rules, settings with per-type tabs, resolver, history, generate job, elements E1-E3/E7 +- [x] 32-03-PLAN.md — sm-seo-plugin core: SEO tab, URL rules, settings with per-type tabs, resolver, history, generate job, elements E1-E3/E7 **Wave 4** *(blocked on Wave 3 completion)* - [ ] 32-04-PLAN.md — redirects and address-change 301, 404 log, normalisation, strict URLs, robots.txt with AI bots, verification files @@ -1698,7 +1698,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 20. Demo instance with every shared plugin | 0/TBD | Not started | - | | 30. Płytarium cutover | 0/TBD | Not started | - | | 31. Chat polish against WavePath clients | 0/TBD | Not started | - | -| 32. SEO foundation | 2/6 | In Progress| | +| 32. SEO foundation | 3/6 | In Progress| | | 33. SEO output, audit and content | 0/TBD | Not started | - | ## Backlog diff --git a/.planning/phases/32-seo-foundation/32-03-SUMMARY.md b/.planning/phases/32-seo-foundation/32-03-SUMMARY.md new file mode 100644 index 00000000..10c874f3 --- /dev/null +++ b/.planning/phases/32-seo-foundation/32-03-SUMMARY.md @@ -0,0 +1,364 @@ +--- +phase: 32-seo-foundation +plan: 03 +subsystem: sm-seo-plugin +tags: [seo, sm-seo-plugin, cabana, form-extensions, signpost, resolver, history, conga, custom-elements, vite] + +requires: + - phase: 32-01 + provides: pact.AdminFormExtension (yaml-document form, FieldsYAML, Dir, Actions, PartialData, Load, Save, Visible, Permissions) + - phase: 32-02 + provides: pact.RecordSaved/RecordDeleted, AdminFormSave.Changes, pact.ContentDescriptor and the signpost ContentTypes/ContentType/ContentTypeForController/ContentTypeForModel/Describe/Records/MatchPath/MatchPattern lookups +provides: + - "New repository ../sm-seo-plugin (module git.golem15.com/golem15/sm-seo-plugin, package seo, plugin golem15.seo), branch main, 9 local commits, nothing pushed" + - "Tables golem15_seo_entries (unique model_type, record_id, locale; data jsonb), golem15_seo_revisions, golem15_seo_settings (row 1, seeded D-07 list), golem15_seo_type_settings, golem15_seo_rules (unique url, match)" + - "SEO tab on every content type with Content.Controller through one yaml-document AdminFormExtension per type (UI-SPEC C3 field order, anchors seo_robots and seo_og_image reserved)" + - "Settings singleton golem15.seo.settings (settings tile link entry seo_settings) with per-type tabs added by the plugin's own FieldsYAML extension (D-104)" + - "URL rules golem15.seo.rules and history golem15.seo.history screens; navigation item seo; permission golem15.seo.manage" + - "classes.Service (single writer, history), classes.Resolver.Resolve/ResolveRecord with Result.Sources, robots grammar, templates, canonical and D-07 parameter handling, non-production lock" + - "GET /_seo/api/v1/resolve (bucket seo-resolve), config keys golem15.seo.site_url and golem15.seo.unprefixed_locale" + - "GenerateEntriesArgs conga job, seo:generate-entries command, listeners for *pact.RecordDeleted and created *pact.RecordSaved" + - "Field types seotext, mlseotext, seorobots, mlseorobots, seoimage; elements golem15-seo-text, golem15-seo-robots, golem15-seo-serp, golem15-seo-image; widget action seo_snippet; partials seo_status, seo_effective, placeholders, type_placeholders, history_diff, staging" +affects: [32-04, 32-05, 32-06, 33, sm-sitemap-plugin, vue-seo-layer] + +actuals: + tokens: 87600 + tasks: 5 + commits: 9 +plan_head_before: 1d3fa326162605533bd8ce4bf9702c05f1f92c9f +plan_head_after: 1d3fa326162605533bd8ce4bf9702c05f1f92c9f + +tech-stack: + added: + - "npm dev dependencies (exact pins, approved in Task 4): typescript 5.9.3, vite 7.3.5, vitest 3.2.7, happy-dom 20.11.6" + - "Go: no new module outside the decided set (gorm, gormigrate, pgx, testcontainers, gocloud.dev/blob, goccy/go-yaml are framework/sibling dependencies)" + patterns: + - "Field-less self-extension on the plugin's own controllers (rules, settings) to record history from AdminFormSave.Changes inside the save transaction" + - "Per-type settings tabs as a FieldsYAML document built at boot from signpost ContentTypes (tab chain after General)" + - "Validation messages with :params translated in the controller hook (FormBeforeCreate/Update) because cabana localizes ValidationError details without params" + - "Fake TranslationWriter in the test fixture plugin (en default, pl enabled) instead of a dependency on sm-translate-plugin" + +key-files: + created: + - ../sm-seo-plugin/plugin.go + - ../sm-seo-plugin/extensions.go + - ../sm-seo-plugin/extensions_settings.go + - ../sm-seo-plugin/classes/resolver.go + - ../sm-seo-plugin/classes/entries.go + - ../sm-seo-plugin/classes/history.go + - ../sm-seo-plugin/classes/generate.go + - ../sm-seo-plugin/classes/snippet.go + - ../sm-seo-plugin/classes/partials.go + - ../sm-seo-plugin/classes/robots_value.go + - ../sm-seo-plugin/classes/field_types.go + - ../sm-seo-plugin/updates/202610270001_create_golem15_seo_entries.go + - ../sm-seo-plugin/assets/src/elements/serp.ts + - ../sm-seo-plugin/assets/dist/seo.js + - ../sm-seo-plugin/seo_tab_test.go + - ../sm-seo-plugin/settings_test.go + - ../sm-seo-plugin/README.md + modified: [] + +key-decisions: + - "Task 4 (package legitimacy, gate blocking-human) answered by Jakub before dispatch: approved. Installed exactly typescript 5.9.3, vite 7.3.5, vitest 3.2.7, happy-dom 20.11.6 (npm install --save-exact --ignore-scripts); grobotstxt v1.0.3 is approved for plan 32-04" + - "D-91 used in its published yaml-document form: the SEO tab is one FieldsYAML document (tabs: [golem15.seo::lang.tab.seo], no anchor) and the per-type settings tabs are a FieldsYAML document built at boot; pact.AdminFormTab/AdminFormField do not exist" + - "The resolve endpoint answers HTTP 200 for both ok and not_found (the answer's status field carries 404); 422 for a url that is neither a path nor an http(s) URL; every answer carries X-Robots-Tag: noindex" + - "The home page / is always a known page (home template, verification meta) even without a rule or record" + - "URL rules apply per value, most specific first: exact rules, then patterns by address length; a rule address with ? is matched against path and query" + - "Unknown query parameters are dropped from the canonical without noindex; a path-scoped parameter row wins over a site-wide one" + - "Robots are serialised with \",\" when stored and \", \" in the meta/X-Robots-Tag answer; the robots source is the layer that decided indexing" + - "History stores What as text rendered in the app locale (searchable); via comes from bouncer.Dispatched (mcp) or a WithVia mark (import, auto, undo); entry deletes on RecordDeleted are recorded with via auto" + - "The social image (seo_og_image) is one value per record written to every locale row (Q-5); seoimage uploads require golem15.seo.manage" + - "Per-type settings and SEO tab field labels carry no :type parameter (fields.yaml labels take none; the tab title names the type)" + +patterns-established: + - "sm-seo-plugin controllers get the service on demand with classes.NewService(app) (no DB needed at boot, so console commands boot without a database)" + - "Partials return curated view models with text already translated (classes.StatusView, EffectiveView, PlaceholdersView)" + +requirements-completed: [SC-2, D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-15, D-21, D-22, D-23, D-90, D-91, D-92, D-93, D-94, D-95, D-100, D-104, D-13] + +coverage: + - id: D1 + description: "SEO tab tracer: a content type the plugin does not know gets the SEO tab through D-91; a saved SEO title lands in golem15_seo_entries and GET /_seo/api/v1/resolve returns it with absolute URLs from the site URL; the owner table has no SEO column" + requirement: "SC-2" + verification: + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestSEOTabTracer" + status: pass + human_judgment: false + - id: D2 + description: "Precedence per value (rule > record > type/general template > content > settings) with sources; exact and pattern rules; D-07 parameter list incl. path scope; never-indexed and search paths; tag-type default robots; home template and verification meta; drafts and unknown paths not_found; non-production lock" + requirement: "D-02" + verification: + - kind: integration + ref: "../sm-seo-plugin/settings_test.go#TestResolverPrecedence" + status: pass + human_judgment: false + - id: D3 + description: "Settings singleton with one tab per content type in registration order between General and Indexing (four fixed tabs with none), per-type defaults, template placeholder refusal, inject switch hiding the SEO tab while stored values keep working" + requirement: "D-104" + verification: + - kind: integration + ref: "../sm-seo-plugin/settings_test.go#TestSettingsTypeTabs" + status: pass + human_judgment: false + - id: D4 + description: "One permission golem15.seo.manage: without it no SEO fields in schema or show and a posted seo_title changes nothing; seoimage upload refused" + requirement: "D-15" + verification: + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestSEOTabPermission" + status: pass + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestSEOTab" + status: pass + human_judgment: false + - id: D5 + description: "History of admin writes (SEO tab, rule create/update/delete, settings and type settings) with author, via, changed fields before/after; history list and P5 diff; via mcp under an MCP dispatch" + requirement: "D-13" + verification: + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestHistoryRecordsAdminWrite" + status: pass + human_judgment: false + - id: D6 + description: "Created records get their address snapshot; deleting a record removes its entries after commit and history keeps the values (via auto); GenerateEntries creates entries per published record and locale and refreshes addresses without touching editor values" + requirement: "D-94" + verification: + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestEntryDeletedWithRecord" + status: pass + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestGenerateEntries" + status: pass + human_judgment: false + - id: D7 + description: "Complete SEO tab (C3 order, last tab on a tabbed form, after default on a tab-less form), malformed robots and canonical 422, seoimage upload refuses a non-image and stores one image for every locale, seo_snippet context and duplicates, P1 rule callout and P2 effective values; URL rule form partials and preview; every lang key present in en and pl" + requirement: "D-09" + verification: + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestSEOTab" + status: pass + - kind: integration + ref: "../sm-seo-plugin/seo_tab_test.go#TestRuleFormPreview" + status: pass + - kind: unit + ref: "../sm-seo-plugin/seo_tab_test.go#TestLangKeysComplete" + status: pass + - kind: other + ref: "npm --prefix ../sm-seo-plugin run typecheck && bash ../sm-seo-plugin/scripts/check-dist.sh" + status: pass + human_judgment: false + - id: D8 + description: "Elements E1 counter, E2 robots editor, E3 result preview (pixel truncation, duplicates, Q-4 keyphrase stems), E7 image upload look and behave as UI-SPEC C4-C6/C14 in the real admin SPA (layout, dark mode, keyboard, locale switcher)" + verification: [] + human_judgment: true + rationale: "Visual and interaction behaviour inside the SPA; only a throwaway happy-dom smoke ran here, element unit tests are plan 32-06" + +duration: 48min +completed: 2026-10-27 +status: complete +--- + +# Phase 32 Plan 03: SEO plugin core Summary + +**New sm-seo-plugin (golem15.seo): an SEO tab injected into every signpost content type through the D-91 yaml-document extension, URL rules, a settings singleton with per-type tabs, change history, a conga entry-generation job, and the public GET /_seo/api/v1/resolve answering the effective SEO of any URL with the rule > record > template/global > content precedence and a source per value.** + +## Task 4 answer (package legitimacy) + +Jakub answered Task 4 (checkpoint:human-verify, gate blocking-human) before this run: **approved**. The orchestrator checked three things: sm-journal-plugin locks the same versions, the npm packages are published from vitejs/vite, vitest-dev/vitest and capricorn86/happy-dom through GitHub Actions, and grobotstxt resolves through proxy.golang.org. + +- Installed exactly `typescript 5.9.3`, `vite 7.3.5`, `vitest 3.2.7` and `happy-dom 20.11.6` as dev dependencies, with `--save-exact --ignore-scripts`. `package.json` has no `dependencies` block. +- `npm install` reported audit advisories in the dev toolchain. Journal locks the same pins. Nothing was upgraded. +- `github.com/jimsmart/grobotstxt v1.0.3` is approved for plan 32-04. It was not installed here. + +## Framework tag in go.mod + +`go.mod` requires `git.golem15.com/golem15/summercms v0.5.0`, the latest existing tag. The 32-01 and 32-02 seams are not in v0.5.0 yet. Both `go.mod` and the tracked `go.work` therefore replace the framework with `../summercms.go`, as the sibling plugins do. Plan 32-06 bumps the require to the new patch tag. + +## Cabana asset-dedup observation + +Cabana accepted `assets/dist/seo.js` and `seo.css` listed on all five SEO field types without a boot refusal, so every type lists them. The rules controller also declares them through `pact.AdminClientAssets`, because a `type: widget` field on an owner form needs controller-declared JS. + +## Performance + +- **Duration:** 48 min +- **Started:** 2026-10-27T09:16:35Z +- **Completed:** 2026-10-27T10:05:19Z +- **Tasks:** 5 (Task 4 was answered before dispatch) +- **Files:** 89 tracked files in the new repository + +## Accomplishments + +- **Plugin scaffold.** Built on the 32-01/32-02 seams only. Content plugins import nothing: the fixture `acme.pages` registers descriptors and gets the SEO tab. +- **Effective-SEO resolver.** Covers: + - a source for each value; + - canonical URLs built from `golem15.seo.site_url`; + - the D-07 parameter list; + - robots per directive in the C5 grammar; + - social and verification values; + - a non-production lock that depends on the environment only. +- **Admin:** + - the SEO tab (C3); + - URL rules (C7); + - settings (C11) with per-type tabs; + - history (C12) with the P5 diff; + - the navigation item, the settings tile and one permission. +- **History and entries.** History records every admin SEO write. A record's entries are removed when it is deleted, and a new record gets an address snapshot. The generation job and its command create missing entries. +- **Element bundle.** E1, E2, E3 and E7 with en/pl labels, built, committed and guarded by a drift check. + +## Task Commits (../sm-seo-plugin, branch main, not pushed) + +1. **Task 1: tracer**: `fb9590f` feat: plugin scaffold, entries and the SEO tab tracer +2. **Task 2:** + - A `ce60c8a` feat: URL rules and the settings singleton + - B `30edb8a` feat: per-type settings tabs and the SEO tab permission + - C `9f1082d` feat: full precedence resolver with value sources +3. **Task 3:** + - A `e8a6b9f` feat: SEO change history + - B `ab8905c` feat: entry generation job and cleanup on delete +4. **Task 4:** no commit (approved before dispatch, recorded above) +5. **Task 5:** + - A `deebd9a` feat: SEO field elements and bundle + - B `93ddbcc` feat: SEO field types and the complete SEO tab + - C `b38eb4b` docs: plugin README + +Tracer feedback gate: the run was interactive, in end-of-phase mode, with an automated-only verify. TestSEOTabTracer, vet and the -short tests re-ran green before Task 2. + +`commits: 9` is counted in ../sm-seo-plugin with `git rev-list --count HEAD` (the repository started empty). summercms.go gets only this SUMMARY's docs commit, so its `plan_head_before` and `plan_head_after` are the same SHA, taken before that commit. + +## Files Created + +- Root: `plugin.go`, `logger.go`, `admin.go`, `admin_permissions.go`, `admin_navigation.go`, `routes.go`, `extensions.go`, `extensions_settings.go`, `field_types.go`, `jobs.go`, `commands.go`, `listeners.go`. +- `models/`: entry, revision, settings, type_setting and rule, with their YAML. +- `updates/`: five raw-SQL migrations. +- `classes/`: service, entries, resolver, templates, urls, robots_value, type_settings, history, generate, snippet, partials, image, field_types. +- `controllers/`: admin_registry, settings, rules, history and api/resolve, with their YAML and partials. +- `extension/`: the SEO tab partials and the per-type partial. +- Element toolchain: + - sources `assets/src/elements/*` and the build `assets/dist/seo.{js,css}`; + - `package.json`, `package-lock.json`, `tsconfig.json` and `vite.config.ts`; + - `scripts/check-dist.sh`. +- Tests: `testmain_test.go`, `fixture_test.go`, `seo_tab_test.go`, `settings_test.go`. +- `README.md`. + +## Acceptance-criteria adjustments (yaml-document D-91 and other notes) + +- **Task 1, go.mod:** `go.mod` also carries the `replace`, as Journal and Sitemap do. The go.work criterion holds. +- **Task 1, locale map:** the tracer PUT sends `seo_title` as a locale map. cabana refuses locale maps without a `cabana.TranslationWriter`, so the fixture publishes a fake one (en default, pl enabled). +- **Task 1, tab label:** the schema serves translated tab labels. The tracer compares the tab with "SEO", the label of `golem15.seo::lang.tab.seo`. +- **Task 2:** `grep golem15.seo.manage admin_permissions.go` matches the doc comment. The code uses `controllers.PermissionManage`. +- **Task 3, command grep:** `grep -n "seo:generate-entries" commands.go` prints four lines: the name, the comment and two examples. +- **Task 3, Dispatched grep:** `grep -n "bouncer.Dispatched" classes/history.go` prints one line, as required. +- **Task 5:** the extension's per-type placeholder partial is `extension/_type_placeholders.htm` (path `type_placeholders`), not `_placeholders.htm`. cabana refuses an extension partial name that the owner form already uses, and the settings form owns `placeholders`. + +## Decisions Made + +See `key-decisions` in the frontmatter. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] config/config.yaml shipped in Task 1** +- **Issue:** the plugin asserts `pact.HasConfig` from Task 1, and the resolver needs `golem15.seo.site_url`. +- **Fix:** `config/config.yaml` (site_url, unprefixed_locale) was committed with the tracer. Task 2 added no further key. +- **Commit:** fb9590f + +**2. [Rule 1 - Bug] Boot no longer requires a database** +- **Issue:** `classes.NewService` refused to boot without a published `*gorm.DB`. The generated main publishes none for console commands, so every command of an app with the plugin would fail. +- **Fix:** the service looks the database up when it is used. +- **Commit:** e8a6b9f + +**3. [Rule 3 - Blocking] Phrasebook pipes** +- **Issue:** the phrasebook parses any `|` as plural forms. `settings.title_template_comment` ("Default: %title% | %site%") therefore failed the boot. +- **Fix:** the copy names the default in words ("the page title, a vertical bar and the site name"). This is the only UI-SPEC copy change. +- **Commit:** ce60c8a + +**4. [Rule 3 - Blocking] Validation messages with parameters** +- **Issue:** cabana localizes `ValidationError` details without parameters such as `:placeholder`. +- **Fix:** the settings and rules checks run in `FormBeforeCreate/Update` and translate with params there (`models.Problem`). Extension refusals translate themselves. +- **Commit:** ce60c8a, 30edb8a + +**5. [Rule 3 - Blocking] Owner-form widget fill** +- **Issue:** cabana lets an owner form's widget fill built-in scalar fields only. The URL rule preview therefore cannot fill `title` and `description`, which are seotext fields. +- **Fix:** the rule form's `_seo_snippet` has no fill. Its context answers the saved rule's title and description, so the preview shows the saved rule; `savedNote` already says so. +- **Not affected:** the SEO tab extension keeps `fill: [seo_title, seo_description, seo_keyphrase]`. +- **Commit:** 93ddbcc + +**6. [Rule 2 - Missing critical] Upload permission (T-32-12/T-32-13)** +- **Issue:** cabana's field upload route does not apply extension visibility. An administrator without golem15.seo.manage could store files through the SEO tab's image field. +- **Fix:** the `seoimage` Upload requires golem15.seo.manage. TestSEOTab covers it. +- **Commit:** 93ddbcc + +**7. [Rule 2 - Missing critical] History for rules and settings inside the save transaction** +- **Fix:** field-less self-extensions on `golem15.seo.rules` and on the settings controller record `AdminFormSave.Changes`. Rule deletes are recorded in `FormAfterDelete`. +- **Files outside the plan's Task 3 list:** `extensions.go`, `extensions_settings.go`, `controllers/rules.go`, `classes/type_settings.go`. +- **Commit:** e8a6b9f + +**8. [Rule 2 - Missing critical] Effective-value cache on admin saves** +- **Issue:** duplicate checks read `effective_title` and `effective_description`, which only the job filled. +- **Fix:** `SaveRecord` refreshes the cache for every entry it writes. +- **Commit:** 93ddbcc + +**9. [Plan detail] Extra files** +- Filters: `controllers/rules/config_filter.yaml` and `controllers/history/config_filter.yaml` (C7/C12). +- Classes: `classes/type_settings.go` and `classes/partials.go`; the extension file `extensions_settings.go`. +- Tests: `TestRuleFormPreview` and `TestLangKeysComplete`. +- The resolver gained `ResolveRecord`, so P2 works on drafts and on records being edited. + +**10. [Plan detail] Per-type labels without :type** +- fields.yaml labels take no parameters. `settings.type_inject`, `type_title_template`, `type_description_template` and `type_robots` therefore use copy without `:type`; the tab title names the type. + +--- + +**Total deviations:** 10 (4 blocking, 1 bug, 3 missing critical, 2 plan detail). Of the 4 blocking ones, 3 are framework constraints. +**Impact on plan:** behaviour is as planned. Three framework constraints are worked around inside the plugin and are candidates for framework notes: +- phrasebook pipes; +- validation details without parameters; +- owner-form widget fill. + +## Issues Encountered + +- **BLOCKER, file permissions:** the executor runs as `golem` (uid 1002, groups golem and docker). Everything in summercms.go is owned by `jin:jin` with mode 775, and `golem` is not in group `jin`. Affected: + - `.git`; + - `.planning/phases/32-seo-foundation`; + - `STATE.md`, `ROADMAP.md`, `REQUIREMENTS.md`. + + This SUMMARY could therefore not be written to the phase directory. STATE.md and ROADMAP.md could not be updated, and no docs commit could be made. The SUMMARY was staged in the executor scratchpad instead. +- **Pre-existing failure:** `go vet ./...` and `go test -short ./...` in summercms.go fail only on `docs/examples/blog`. `docs/examples/blog/registry.gen.go` is owned by user `jin` with mode 0600; it is dated Sep 30 and carries another session's uncommitted edit. Every other package is green, and this plan does not change the framework. +- **Element tests:** element unit tests belong to plan 32-06. A throwaway happy-dom smoke passed and was not committed. It covered the robots round-trip, the stem rule and E1/E2/E3 behaviour. + +## Known Stubs + +None. Redirects, robots.txt, the 404 log, MCP, undo and CSV belong to plans 32-04 and 32-05 by design. The navigation item lands on URL rules until the redirects screen exists. + +## Threat Flags + +| Flag | File | Description | +|------|------|-------------| +| threat_flag: upload | ../sm-seo-plugin/classes/image.go | New file-storage path (media/seo/) through the seoimage upload route; mitigated by the permission check, content sniffing, a size cap and a random file name (T-32-12) | + +## User Setup Required + +None. A site sets `golem15.seo.site_url`, or relies on `app.url`. + +## Next Phase Readiness + +- **Plan 32-04** builds on `classes.Service`, `Resolver.Resolve`, the settings address columns, the listeners and `routes.go`. +- **Plan 32-05** builds on the history core (`Record`, `WithVia`), the rule and settings writers, and `golem15.seo.manage`. +- **Nothing pushed:** `git -C ../sm-seo-plugin status -sb` shows `## main` with no upstream, and `ls-remote origin` is empty. + +## Self-Check: PASSED + +- All key files exist. Commits fb9590f, ce60c8a, 30edb8a, 9f1082d, e8a6b9f, ab8905c, deebd9a, 93ddbcc and b38eb4b are present in ../sm-seo-plugin. +- `go -C ../sm-seo-plugin vet ./...`, `go test -short ./...` and `go test -count=1 ./...` (Docker) are green. The Docker run covers: + - TestSEOTabTracer, TestSEOTabPermission, TestSEOTab, TestRuleFormPreview; + - TestHistoryRecordsAdminWrite, TestEntryDeletedWithRecord, TestGenerateEntries; + - TestLangKeysComplete, TestSettingsTypeTabs, TestResolverPrecedence. +- `npm run typecheck` and `scripts/check-dist.sh` are green. +- `grep -rn sm-seo-plugin ../sm-journal-plugin --include=*.go` is empty. + +--- +*Phase: 32-seo-foundation* +*Completed: 2026-10-27*