From 8800137d16b10368f7c38686045bf194e924b950 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 11:56:20 +0100 Subject: [PATCH] docs(32-04): complete redirects, 404 log and robots.txt plan --- .../phases/32-seo-foundation/32-04-SUMMARY.md | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 .planning/phases/32-seo-foundation/32-04-SUMMARY.md diff --git a/.planning/phases/32-seo-foundation/32-04-SUMMARY.md b/.planning/phases/32-seo-foundation/32-04-SUMMARY.md new file mode 100644 index 00000000..1de6e77e --- /dev/null +++ b/.planning/phases/32-seo-foundation/32-04-SUMMARY.md @@ -0,0 +1,402 @@ +--- +phase: 32-seo-foundation +plan: 04 +subsystem: sm-seo-plugin +tags: [seo, sm-seo-plugin, redirects, 404, robots.txt, grobotstxt, normalisation, strict-urls, custom-elements] + +requires: + - phase: 32-01 + provides: pact.AdminFormExtension (yaml-document form, Save notices, AdminFormErrors, field-less extensions, extension widget fills) + - phase: 32-02 + provides: pact.RecordSaved with Changes, cabana.EmitRecordSaved, signpost Describe/MatchPath/ContentTypeForModel + - phase: 32-03 + provides: sm-seo-plugin core (classes.Service, Resolver, entries with URL snapshots, history, listeners, routes, settings address and verification columns, element bundle) +provides: + - "golem15_seo_redirects with the redirect engine (301/302/410; exact, pattern, regex; ignore case, trailing slash, query ignore/pass), save-time loop refusal and chain notices, runtime 10-hop cap" + - "AddressChanged: the automatic 301 on an address change with chain flattening, in the SEO tab save transaction and after commit for writes outside the admin" + - "golem15.seo.redirects and golem15.seo.notfound screens; POST /_seo/api/v1/notfound with bearer token" + - "Normalize, strict URLs and the one-hop redirect decision in GET /_seo/api/v1/resolve (actions redirect and gone)" + - "golem15.seo.robots singleton with the robotstxt field type (E4) and the robots_test tester widget (E5) on grobotstxt; GET /robots.txt with AI bot groups, Sitemap lines and the non-production lock" + - "GET /_seo/api/v1/verification/{name}" + - "Decision note .planning/notes/2026-10-27-robots-txt-grobotstxt.md and a STACK.md row" +affects: [32-05, 32-06, 33, vue-seo-layer, sm-sitemap-plugin] + +actuals: + tokens: 60000 + tasks: 4 + commits: 1 +plan_head_before: 930d91a3c1df3da828c5108e984ffae700708635 +plan_head_after: 99889f0fe82c5cbdda0bd1b9e4185c49db8dd9fd + +tech-stack: + added: + - "github.com/jimsmart/grobotstxt v1.0.3 (sm-seo-plugin only; approved at the 32-03 package gate; decision note and STACK row)" + patterns: + - "One service path for redirect writes: SaveRedirect, AddressChanged, DeleteRedirect, ResolveNotFound run validation, the loop simulation and history inside the caller's transaction" + - "Field-less self-extension on the plugin's own controller to refuse with AdminFormErrors and answer save notices from inside the save transaction (redirects)" + - "A form extension on the plugin's own singleton to carry fields an owner-form widget may not fill (robots.txt editor and bot switches for the tester)" + - "Resolve as a pipeline: normalisation, redirects followed to the final target, strict URLs, then SEO; one hop" + +key-files: + created: + - ../sm-seo-plugin/classes/redirects.go + - ../sm-seo-plugin/classes/redirect_graph.go + - ../sm-seo-plugin/classes/addresses.go + - ../sm-seo-plugin/classes/notfound.go + - ../sm-seo-plugin/classes/normalize.go + - ../sm-seo-plugin/classes/strict.go + - ../sm-seo-plugin/classes/robots.go + - ../sm-seo-plugin/classes/bots.go + - ../sm-seo-plugin/classes/verification.go + - ../sm-seo-plugin/controllers/redirects.go + - ../sm-seo-plugin/controllers/notfound.go + - ../sm-seo-plugin/controllers/robots.go + - ../sm-seo-plugin/controllers/api/notfound.go + - ../sm-seo-plugin/controllers/api/robots.go + - ../sm-seo-plugin/controllers/api/verification.go + - ../sm-seo-plugin/extensions_robots.go + - ../sm-seo-plugin/models/redirect.go + - ../sm-seo-plugin/models/notfound.go + - ../sm-seo-plugin/models/robots.go + - ../sm-seo-plugin/updates/202610270006_create_golem15_seo_redirects.go + - ../sm-seo-plugin/updates/202610270007_create_golem15_seo_notfound.go + - ../sm-seo-plugin/updates/202610270008_create_golem15_seo_robots.go + - ../sm-seo-plugin/assets/src/elements/robotstxt.ts + - ../sm-seo-plugin/assets/src/elements/robotstester.ts + - ../sm-seo-plugin/redirects_test.go + - ../sm-seo-plugin/addresses_test.go + - ../sm-seo-plugin/robots_test.go + - .planning/notes/2026-10-27-robots-txt-grobotstxt.md + modified: + - ../sm-seo-plugin/classes/resolver.go + - ../sm-seo-plugin/classes/partials.go + - ../sm-seo-plugin/classes/field_types.go + - ../sm-seo-plugin/classes/service.go + - ../sm-seo-plugin/extensions.go + - ../sm-seo-plugin/listeners.go + - ../sm-seo-plugin/routes.go + - ../sm-seo-plugin/admin_navigation.go + - ../sm-seo-plugin/controllers/admin_registry.go + - ../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/go.mod + - ../sm-seo-plugin/assets/dist/seo.js + - ../sm-seo-plugin/assets/dist/seo.css + - .planning/research/STACK.md + +key-decisions: + - "Redirect statuses are models.RedirectStatus (an int with UnmarshalText): the admin dropdown posts \"301\" as text and lagoon.Fill cannot fill an int from a string" + - "Loop detection refuses a repeated URL, a rule that fires twice, or reaching 10 hops; pattern probes replace * with seoprobe, regex probes are built from regexp/syntax literals" + - "Exact redirects are read per request by lower(source) (functional index added to the unreleased redirects migration); pattern and regex rules are loaded each request, their expressions compiled once per source" + - "A capture is percent-encoded (?, #, \\, %, non-ASCII) before substitution; a relative target collapses leading slashes; an absolute target must keep its literal's scheme and host, else the rule does not match" + - "A redirect chain that ends on a 410 answers gone for every source in it; a 302 anywhere in a chain makes the answer 302" + - "Strict URLs compare the normalised request path with the record's normalised Describe URL in the matched locale; MatchPath gets the path without a trailing slash, so records match under url_slash add" + - "Normalisation uses the request URL's scheme and host only when the host is the site URL's host or its www twin; any other host is ignored" + - "The robots.txt editor, tester and AI bot switches are fields of the plugin's own form extension on golem15.seo.robots (an owner-form widget may fill built-in scalar fields only); the owner form keeps the staging callout and P4" + - "Bot switch labels are \" ()\" and their comments carry no :operator, because fields.yaml copy takes no parameters" + - "Record action Confirm texts take no parameters and the SPA shows a bulk Confirm as is, so notfound.redirect_confirm, gone_confirm and gone_selected_confirm name \"this address\" / \"the selected addresses\" instead of :url / :count" + - "The last production robots.txt body is cached on the service and served when the store fails; without one the body is allow-all; always 200" + - "Verification files are served with nosniff and Content-Security-Policy default-src 'none'; sandbox" + +patterns-established: + - "Service methods return typed refusals (ProblemsError, LoopError, NotFoundHasRuleError) that each caller maps: 422 field lines on a form, 403 toast messages on record and bulk actions" + +requirements-completed: [SC-3, D-16, D-17, D-18, D-19, D-20, D-20a, D-20b, D-20c, D-22, D-24, D-25, D-05, D-08, D-06, D-99, D-103, D-105, D-92, D-102] + +coverage: + - id: D1 + description: "Tracer: an admin slug change creates the 301 in the save transaction, flattens an earlier redirect to the old address, answers redirects.auto_created on slug, and resolve redirects the old address to the absolute new one" + requirement: "SC-3" + verification: + - kind: integration + ref: "../sm-seo-plugin/redirects_test.go#TestSlugChange301" + status: pass + human_judgment: false + - id: D2 + description: "Redirect engine: exact, pattern and regex sources with ignore case, trailing slash and query ignore/pass, order exact > pattern > regex, 410 gone, field refusals, loop refusal (exact, pattern and regex probes), chain checks, slug back and forth ending with A→C and B→C, open-redirect safety" + requirement: "D-17" + verification: + - kind: integration + ref: "../sm-seo-plugin/redirects_test.go#TestRedirectEngine" + status: pass + human_judgment: false + - id: D3 + description: "Redirects screen: a loop is refused with redirects.loop on target and nothing saved (create and update); a chain is saved with redirects.chain_notice and marked negative in the list; target_required and regex_invalid answer 422; history rows" + requirement: "D-19" + verification: + - kind: integration + ref: "../sm-seo-plugin/redirects_test.go#TestLoopRefused" + status: pass + human_judgment: false + - id: D4 + description: "404 log: the bearer-token report endpoint (401, 404 when off, 422 over 2048 bytes), hits, first seen, last hit and referer; the prompted redirect and 410 record actions, Resolved rows, resolved hint, bulk 410 refused for a selection with a covered address, open filter" + requirement: "D-20" + verification: + - kind: integration + ref: "../sm-seo-plugin/redirects_test.go#TestNotFoundLog" + status: pass + human_judgment: false + - id: D5 + description: "Writes outside the admin: RecordSaved without a controller creates the 301 after commit (via auto); no change and unpublished records create nothing; an admin save that already applied it changes nothing more" + requirement: "D-102" + verification: + - kind: integration + ref: "../sm-seo-plugin/redirects_test.go#TestAddressChangeFromEvent" + status: pass + human_judgment: false + - id: D6 + description: "URL normalisation as a pure function: https, www/bare, double slashes, index files, trailing slash add/remove (root and files kept), ASCII-only lowercasing with Polish letters and percent-encoding kept" + requirement: "D-20b" + verification: + - kind: unit + ref: "../sm-seo-plugin/addresses_test.go#TestNormalize" + status: pass + human_judgment: false + - id: D7 + description: "Strict URLs: a wrong category or another locale's slug 301s to the record's address, queries kept; only the record's address answers ok" + requirement: "D-20c" + verification: + - kind: integration + ref: "../sm-seo-plugin/addresses_test.go#TestStrictURL" + status: pass + human_judgment: false + - id: D8 + description: "One-hop redirect decision: a non-normalised URL whose normal form is a redirect source (and a chain) answers one 301 to the final normalised target; a runtime cycle answers not_found; P1 shows status.canonical_redirected" + requirement: "D-20a" + verification: + - kind: integration + ref: "../sm-seo-plugin/addresses_test.go#TestOneHop" + status: pass + human_judgment: false + - id: D9 + description: "GET /robots.txt: stored rules, AI bot groups and the Sitemap line in production; the lock with X-Robots-Tag outside production whatever is stored; 200 with the last good body or allow-all when the store fails" + requirement: "D-22" + verification: + - kind: integration + ref: "../sm-seo-plugin/robots_test.go#TestRobotsServed" + status: pass + human_judgment: false + - id: D10 + description: "Tester on grobotstxt against Google's documented examples (longest match, Allow wins a tie, $ anchor, group choice) with the deciding line; a switched-off bot is reported as blocked by its switch" + requirement: "D-99" + verification: + - kind: integration + ref: "../sm-seo-plugin/robots_test.go#TestRobotsTester" + status: pass + human_judgment: false + - id: D11 + description: "robots.txt screen through the admin API: field order, save of rules and switches with history, robots.blocks_noindex notice, 500 KB refusal, tester action on unsaved values incl. Claude-User, served-file partial" + requirement: "D-24" + verification: + - kind: integration + ref: "../sm-seo-plugin/robots_test.go#TestRobotsScreen" + 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: D12 + description: "Verification files through /_seo/api/v1/verification/{name} with content type, nosniff, sandbox CSP and noindex; unknown, malformed and root paths 404" + requirement: "D-05" + verification: + - kind: integration + ref: "../sm-seo-plugin/robots_test.go#TestVerificationFile" + status: pass + human_judgment: false + - id: D13 + description: "E4 editor and E5 tester, the redirects and 404 log lists (badges, filters, prompted action, danger confirm) and the robots.txt screen look and behave as UI-SPEC C8-C10 in the real admin SPA" + verification: [] + human_judgment: true + rationale: "Visual and interaction behaviour inside the SPA; only a throwaway happy-dom smoke of E4/E5 ran here, element unit tests are plan 32-06" + +duration: 43min +completed: 2026-10-27 +status: complete +--- + +# Phase 32 Plan 04: Redirects, 404 log, URL rules and robots.txt Summary + +**Redirect engine with save-time loop refusal and chain notices, the automatic 301 on an address change (inside the admin save and after commit for API writes), the 404 log with a token-protected report endpoint, URL normalisation and strict URLs folded into a one-hop redirect decision in resolve, and robots.txt with a grobotstxt tester, eleven AI bot switches, a non-production lock that never answers 5xx, and verification files served through the API.** + +## Performance + +- **Duration:** 43 min +- **Started:** 2026-10-27T10:11:34Z +- **Completed:** 2026-10-27T10:54:32Z +- **Tasks:** 4 (Task 1 tracer, Task 2 in four commit groups, Task 3, Task 4 in five commit groups) +- **Files:** 64 files changed in ../sm-seo-plugin, 2 in this repository + +## Accomplishments + +- **Redirects (D-16, D-17, D-19).** + - Matching: exact, pattern and regex sources, ignore case, trailing slash, query ignore/pass, and a source with `?` that matches its query exactly (Q-9). + - Safety: stdlib RE2 regexes capped at 512 characters, and captures that cannot leave the site. + - Save-time simulation: a loop is refused and a chain is saved with a notice. + - Runtime: chains are followed to the final target, and a cycle stops after 10 hops. +- **Address changes (D-18, D-92, D-102, P-20).** `AddressChanged` runs inside the SEO tab save with the `redirects.auto_created` notice on the slug field. A `RecordSaved` listener does the same after commit for writers outside the admin. Unpublished and deleted records get no redirect. +- **Redirects screen (C8).** Six columns with the Chain badge, the status/match/origin/chain filters, and the navigation item landing here. +- **404 log (D-20, P-22, C9).** + - Report endpoint `POST /_seo/api/v1/notfound`: bearer token compared in constant time, off when unset, URL and referer caps, a row cap with an hourly warning. + - List by hits, with Resolved rows and the resolved hint. + - Actions: a prompted 301, a danger 410, and a bulk 410 that refuses the whole selection when one row is covered. +- **Redirect decision (D-20a, D-20b, D-20c, D-103, P-21).** Resolve now runs normalisation, then redirects, then strict URLs, then SEO. It answers `redirect` with an absolute `location` (one hop) or `gone`. P1 warns when a canonical URL redirects (D-06). +- **robots.txt (D-22, D-24, D-25, D-99, D-105, P-23, C10).** + - Field type `robotstxt` with element E4 (live lint, size meter). + - Tester E5 on `robots_test`. It works on unsaved text and switches. + - AI bots tab with eleven switches, including `Claude-User`. + - A `robots.blocks_noindex` save notice. + - `GET /robots.txt`: rules, bot groups and Sitemap lines. Outside production it serves the lock with `X-Robots-Tag`. It never answers 5xx. +- **Verification files (D-05, D-103).** `GET /_seo/api/v1/verification/{name}`. There is no root route. +- **Planning docs.** Decision note `2026-10-27-robots-txt-grobotstxt.md` and a STACK.md row. + +## Task Commits + +../sm-seo-plugin, branch main, not pushed: + +1. **Task 1 (tracer):** `e31ac7c` feat: address-change 301 (tracer) +2. **Task 2:** + - A `34a65b8` feat: redirect engine with loop refusal and chain notices + - B `163b0aa` feat: redirects screen + - C `6eb6cb7` feat: 404 log and report endpoint + - D `327925c` feat: address-change redirects for writes outside the admin +3. **Task 3:** `4cb0fbf` feat: URL normalisation, strict URLs and the one-hop redirect decision +4. **Task 4:** + - G1 (this repository) `99889f0` docs(32-04): grobotstxt decision note + - G2 `3dd9362` feat: robots.txt served file, AI bots and the non-production lock + - G3 `51413fb` feat: robots.txt editor and tester screen + - G4 `ec74062` feat: verification files API + - G5 `b398161` docs: robots.txt, AI bots and verification files in README + +Commit counts: +- ../sm-seo-plugin: `git rev-list --count b38eb4b..HEAD` = 10. +- This repository before this SUMMARY's commit: `git rev-list --count 930d91a..HEAD` = 1 (G1), which is the frontmatter's `commits`. + +Tracer feedback gate: the run was interactive, in end-of-phase mode, with an automated-only verify. TestSlugChange301, vet and the `-short` tests re-ran green before Task 2. + +## TDD Gate Compliance + +Tasks 2 and 3 are marked `tdd="true"`, and `workflow.tdd_mode` is false. +- **Commits:** the plan defines one commit per resume unit, with the tests included, so there are no separate `test(...)` RED commits. +- **RED evidence:** none. The tests were written next to the implementation, not before it. Only TestAddressChangeFromEvent failed first, and that was on the test's own transaction: `EmitRecordSaved` needs `lagoon.Transaction`. + +## Files Created/Modified + +See `key-files` in the frontmatter. +- Classes: `classes/redirects.go`, `redirect_graph.go` and `addresses.go` hold the engine, the simulation and the address change. `normalize.go` and `strict.go` hold the redirect-decision stages, and `robots.go`, `bots.go` and `verification.go` the robots.txt side. +- Extension: `extensions_robots.go` builds the robots form extension. Its document is generated from `classes.Bots`. + +## Decisions Made + +See `key-decisions` in the frontmatter. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Redirect status type** +- **Found during:** Task 2B +- **Issue:** the admin dropdown posts `"301"` as text, and `lagoon.Fill` cannot fill an `int` from a string. +- **Fix:** `models.RedirectStatus` (an int with `UnmarshalText`). The SPA compares option values as strings, so `show` matches. +- **Commit:** 163b0aa + +**2. [Rule 3 - Blocking, framework constraint] The tester needs unsaved editor text** +- **Found during:** Task 4 G3 +- **Issue:** an owner-form widget may fill built-in scalar fields only, so the tester could not fill a `robotstxt` field from the owner form. +- **Fix:** the editor, the tester and the bot switches are fields of the plugin's own form extension on `golem15.seo.robots`. An extension widget may fill its own value fields, plugin types included. + - Load and Save read and write `golem15_seo_robots` (`content`, `bots` jsonb), so the plan's table shape is kept. + - The owner `fields.yaml` holds the staging callout and P4. +- **Files outside the plan's list:** `extensions_robots.go` (new), `extensions.go`. +- **Commit:** 51413fb + +**3. [Framework constraint] Copy without parameters** +- **Record action confirms:** `Confirm` is translated without parameters, so `notfound.redirect_confirm` and `notfound.gone_confirm` say "this address" instead of “:url”. +- **Bulk confirm:** the SPA shows a bulk `Confirm` as is, with no plural and no `:count`, so `notfound.gone_selected_confirm` is one plain sentence. +- **Bot switches:** `fields.yaml` comments take no parameters, so the labels are " ()" and `bots.*_comment` has no `:operator`. +- **Commits:** 6eb6cb7, 51413fb + +**4. [Plan detail] Lowercased-source index added to the redirects migration** +- `202610270006` (committed in Task 1, unreleased, not pushed) gained `CREATE INDEX ... (lower(source))` in group A. The resolver reads exact rules by that key. +- **Commit:** 34a65b8 + +**5. [Plan detail] resolver.go changed outside Task 3's commit** +- `classes/resolver.go` changed in the Task 1 and Task 2 A/B commits, so each commit compiles with the engine of that commit. +- **Commits:** e31ac7c, 34a65b8, 163b0aa + +**6. [Plan detail] Extra files and keys** +- Filters: `controllers/redirects/config_filter.yaml` and `controllers/notfound/config_filter.yaml`. +- Classes: + - `classes/partials.go` gained the canonical-redirected callout. `extension/_seo_status.htm` needed no change, because callouts render generically. + - `classes/service.go` caches the last good robots.txt. +- Test: `TestRobotsScreen`, an admin-path smoke test. +- Lang keys not in the UI-SPEC: + - `redirects.target_invalid`: a target that is neither a path nor an http(s) URL; + - `robots.test_invalid` and `robots.served_production` (P4's "Production" heading); + - `robots.staging_text` reuses the UI-SPEC copy. + +**7. [Plan detail] Strict URLs and trailing slashes** +- The strict check gives `MatchPath` the path without a trailing slash, so records still match when the site adds slashes. +- With the default `keep`, `/pages/a/` therefore 301s to the record's address `/pages/a` (before this plan it was `not_found`). + +**8. [Plan detail] Filters versus badges** +- The 404 log's open/resolved filter runs in SQL over exact rules only. The Resolved badge and the actions use the full rule set, patterns and regex included. +- The redirects Chain badge and the chain filter compare the target with another rule's source literally. + +**9. [Plan detail] README timing** +- The plan's G5 grouping moved the README updates for redirects, the 404 log and normalisation into the docs commit. They did not land in each feature commit. + +--- + +**Total deviations:** 9. Two were blocking, and one is a framework copy constraint; the other six are plan details. +**Impact on plan:** behaviour is as planned, and copy changes are limited to the confirm texts and the bot comments. + +**Framework candidates:** +- parameters in record-action and bulk confirms; +- parameters in fields.yaml comments; +- owner-form widget fill of plugin field types. + +## Issues Encountered + +None blocking: +- File permissions in summercms.go were fixed before this run, so this SUMMARY and the planning commit were written in place. +- summercms.go `go vet ./...` and `go test -short ./...` are green. + +## Known Stubs + +None. +- MCP tools, batch writes, undo and CSV import of redirects are plan 32-05; they reuse `SaveRedirect` and `AddressChanged`. +- Element unit tests are plan 32-06. +- The Nuxt-side enforcement of the redirect decision and of verification files is Phase 33 (D-103). + +## User Setup Required + +None. To feed the 404 log, a site sets `golem15.seo.notfound_token` and lets its frontend server report 404s; without a token the endpoint stays off. + +## Next Phase Readiness + +- Plan 32-05 builds on `SaveRedirect`, `CheckRedirect`, `DeleteRedirect`, `AddressChanged`, `ResolveNotFound` and the typed refusals. History is already recorded for redirect and robots writes. +- Phase 33 takes the resolve `redirect`/`gone` actions, the verification API and `robots_sitemaps`. The sitemap rewrite replaces the default. +- Nothing pushed: ../sm-seo-plugin `main` has no upstream. + +## Self-Check: PASSED + +- **Commits present:** + - ../sm-seo-plugin: e31ac7c, 34a65b8, 163b0aa, 6eb6cb7, 327925c, 4cb0fbf, 3dd9362, 51413fb, ec74062, b398161; + - this repository: 99889f0. +- **Key files:** all exist. +- **../sm-seo-plugin:** + - `go vet ./...`, `go test -short ./...` and `go test -count=1 ./...` (Docker) are green. The Docker run includes TestSlugChange301, TestRedirectEngine, TestLoopRefused, TestNotFoundLog, TestAddressChangeFromEvent, TestNormalize, TestStrictURL, TestOneHop, TestRobotsServed, TestRobotsTester, TestRobotsScreen, TestVerificationFile and TestLangKeysComplete. + - `npm run typecheck` and `scripts/check-dist.sh` are green. +- **summercms.go:** `go vet ./...` and `go test -short ./...` are green. +- **Acceptance greps:** + - `AddressChanged(` appears in `extensions.go` and `listeners.go`; `*pact.RecordSaved` appears once in `listeners.go`. + - `subtle.ConstantTimeCompare` appears in `controllers/api/notfound.go`. + - There is no `regexp.MustCompile(.*source`, dlclark or regexp2. + - grobotstxt v1.0.3 is in the plugin's go.mod and not in the framework's. + - `Claude-User` appears once in `classes/bots.go`. + - `routes.go` has no `Get("/{`. + +--- +*Phase: 32-seo-foundation* +*Completed: 2026-10-27*