docs(33): gap-closure plans 33-15 (Go plugins) and 33-16 (Nuxt layers)
This commit is contained in:
@@ -1692,7 +1692,7 @@ Plans:
|
||||
3. The crawler reports SEO issues per URL, and each issue can be fixed through MCP.
|
||||
4. The new code has unit tests, delivered in the phase's last plan.
|
||||
|
||||
**Plans:** 13/14 plans executed
|
||||
**Plans:** 13/16 plans executed
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
@@ -1727,6 +1727,10 @@ Plans:
|
||||
**Wave 9** *(blocked on Wave 8 completion)*
|
||||
- [ ] 33-14-PLAN.md — unit tests, check-phase33.sh --all, release tags in order, app re-pins, changelog post
|
||||
|
||||
**Wave 10** *(gap closure after the 33-14 release checkpoint; 33-14 Task 4 resumes after both)*
|
||||
- [ ] 33-15-PLAN.md — Go plugins: route report noindex paths and the filter (G1), Journal descriptors per translated locale (G3), crawl lifecycle and addresses, sitemap IndexNow/lock/lastmod/migration/headings, Journal content fixes
|
||||
- [ ] 33-16-PLAN.md — Nuxt layers: route report noindex paths (G1), HTML sitemap pages (redirect, catch-all, i18n), root files 503, post article meta, journal pagination routes, link: release guard and versions
|
||||
|
||||
### Phase 34: Plugin marketplace
|
||||
|
||||
**Goal:** People browse the public Golem15 plugins at summercms.io/marketplace and add one to their application with a single command, `summer plugin:install <id>`. The same command installs private plugins straight from a git URL without contacting summercms.io. The admin SPA gains a read-only Plugins screen listing installed plugins and searching the marketplace, which shows the install command with a copy button. v1 has no paid plugins, no developer registration and no installs from the admin panel.
|
||||
|
||||
595
.planning/phases/33-seo-output-audit-and-content/33-15-PLAN.md
Normal file
595
.planning/phases/33-seo-output-audit-and-content/33-15-PLAN.md
Normal file
@@ -0,0 +1,595 @@
|
||||
---
|
||||
phase: 33-seo-output-audit-and-content
|
||||
plan: 15
|
||||
type: execute
|
||||
wave: 10
|
||||
depends_on: ["33-13"]
|
||||
gap_closure: true
|
||||
files_modified:
|
||||
- ../sm-sitemap-plugin/contract/contract.go
|
||||
- ../sm-sitemap-plugin/classes/listing.go
|
||||
- ../sm-sitemap-plugin/plugin.go
|
||||
- ../sm-sitemap-plugin/classes/lastmod.go
|
||||
- ../sm-sitemap-plugin/classes/rebuild.go
|
||||
- ../sm-sitemap-plugin/classes/indexnow.go
|
||||
- ../sm-sitemap-plugin/classes/typesettings.go
|
||||
- ../sm-sitemap-plugin/models/url.go
|
||||
- ../sm-sitemap-plugin/models/legacy.go
|
||||
- ../sm-sitemap-plugin/models/legacy_test.go
|
||||
- ../sm-sitemap-plugin/updates/202610280002_update_golem15_sitemap_settings.go
|
||||
- ../sm-sitemap-plugin/updates/202611040001_add_golem15_sitemap_urls_indexnow_pending.go
|
||||
- ../sm-sitemap-plugin/extensions_settings.go
|
||||
- ../sm-sitemap-plugin/controllers/api/settings.go
|
||||
- ../sm-sitemap-plugin/lang/en/lang.yaml
|
||||
- ../sm-sitemap-plugin/lang/pl/lang.yaml
|
||||
- ../sm-sitemap-plugin/README.md
|
||||
- ../sm-sitemap-plugin/content_fixture_test.go
|
||||
- ../sm-sitemap-plugin/filter_fixture_test.go
|
||||
- ../sm-sitemap-plugin/sitemap_extras_test.go
|
||||
- ../sm-sitemap-plugin/sitemap_index_test.go
|
||||
- ../sm-sitemap-plugin/classes/lastmod_test.go
|
||||
- ../sm-seo-plugin/models/noindex_path.go
|
||||
- ../sm-seo-plugin/models/crawl.go
|
||||
- ../sm-seo-plugin/updates/202611040001_create_golem15_seo_noindex_paths.go
|
||||
- ../sm-seo-plugin/updates/202611040002_add_golem15_seo_crawls_heartbeat.go
|
||||
- ../sm-seo-plugin/classes/registry.go
|
||||
- ../sm-seo-plugin/classes/sitemap_filter.go
|
||||
- ../sm-seo-plugin/classes/linkgraph.go
|
||||
- ../sm-seo-plugin/classes/crawl/checks.go
|
||||
- ../sm-seo-plugin/classes/crawl/fix.go
|
||||
- ../sm-seo-plugin/classes/crawl/job.go
|
||||
- ../sm-seo-plugin/classes/crawl/store.go
|
||||
- ../sm-seo-plugin/classes/crawl/actions.go
|
||||
- ../sm-seo-plugin/classes/crawl/crawler.go
|
||||
- ../sm-seo-plugin/classes/crawl/fetch.go
|
||||
- ../sm-seo-plugin/classes/crawl/compare.go
|
||||
- ../sm-seo-plugin/classes/crawl/render.go
|
||||
- ../sm-seo-plugin/classes/crawl/render_chrome.go
|
||||
- ../sm-seo-plugin/jobs.go
|
||||
- ../sm-seo-plugin/controllers/api/registry.go
|
||||
- ../sm-seo-plugin/controllers/api/crawls.go
|
||||
- ../sm-seo-plugin/controllers/crawls.go
|
||||
- ../sm-seo-plugin/mcp.go
|
||||
- ../sm-seo-plugin/lang/en/lang.yaml
|
||||
- ../sm-seo-plugin/lang/pl/lang.yaml
|
||||
- ../sm-seo-plugin/README.md
|
||||
- ../sm-seo-plugin/sitemap_filter_test.go
|
||||
- ../sm-seo-plugin/registry_test.go
|
||||
- ../sm-seo-plugin/crawler_test.go
|
||||
- ../sm-seo-plugin/crawl_lifecycle_test.go
|
||||
- ../sm-seo-plugin/classes/alternates_test.go
|
||||
- ../sm-seo-plugin/classes/crawl/checks_test.go
|
||||
- ../sm-seo-plugin/classes/crawl/render_test.go
|
||||
- ../sm-seo-plugin/classes/crawl/fetch_test.go
|
||||
- ../sm-journal-plugin/classes/image_variants.go
|
||||
- ../sm-journal-plugin/classes/headings.go
|
||||
- ../sm-journal-plugin/classes/image_alt.go
|
||||
- ../sm-journal-plugin/classes/content_descriptors.go
|
||||
- ../sm-journal-plugin/classes/menu_item_types.go
|
||||
- ../sm-journal-plugin/console/fix_headings.go
|
||||
- ../sm-journal-plugin/controllers/api/maintenance.go
|
||||
- ../sm-journal-plugin/mcp.go
|
||||
- ../sm-journal-plugin/README.md
|
||||
- ../sm-journal-plugin/classes/headings_test.go
|
||||
- ../sm-journal-plugin/classes/image_alt_test.go
|
||||
- ../sm-journal-plugin/headings_test.go
|
||||
- ../sm-journal-plugin/related_media_test.go
|
||||
- ../sm-journal-plugin/image_variants_cli_test.go
|
||||
- ../sm-journal-plugin/content_descriptors_test.go
|
||||
- modules/lagoon/README.md
|
||||
- docs/database/attachments.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-journal-plugin-v0.4.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-seo-plugin-v0.2.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-sitemap-plugin-v0.2.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-summercms-v0.6.1.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-release-log.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/deferred-items.md
|
||||
autonomous: true
|
||||
requirements: [SC-1, SC-2, SC-3, D-14, D-24, D-30, D-31, D-32, D-33, D-34, D-50, D-55, D-57, D-85, D-95, D-10]
|
||||
|
||||
estimate:
|
||||
tokens: 210000
|
||||
raw_tokens: 210000
|
||||
tasks: 6
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "G1 (D-24, D-33, D-95, SC-1): POST /_seo/api/v1/registry/routes accepts the optional noindex array of the gap-2 contract below; the stored set is replaced when the key is present (also []), unchanged when it is absent; the SEO sitemap filter leaves every candidate whose normalized path is in the set out of the XML and HTML sitemaps with reason noindex; a changed set requests one debounced sitemap rebuild through the new contract.Rebuilder"
|
||||
- "G1 (SC-3, D-14): a sitemap_noindex finding on a listed address without a CMS record names sitemap_update_settings (removing the Additional address) while the sitemap plugin is mounted, and fix: code otherwise; one on a CMS record keeps seo_set_url_seo"
|
||||
- "L3 CR-01 (D-14): a crawl whose job context is cancelled or reaches the job timeout stores the pages, links and issues fetched so far through a context detached from the job with its own 10-minute bound, and ends cancelled (a requested cancel) or failed with a time-limit error (the timeout), with finished_at set; a LoadLimits failure in finish falls back to the default limits; a fetch cut short by the cancellation is recorded as canceled, not network_error"
|
||||
- "L3 CR-01: a running crawl whose heartbeat (written every minute by its job) is older than 10 minutes is ended (cancelled when a cancel was requested, else failed with a no-heartbeat error) before a crawl starts or crawls are deleted, so a row whose job is gone never stays running"
|
||||
- "L3 WR-01: cancelling a running crawl records the request (cancel_requested_at, cancelled_by) and leaves the row running until its job stops and writes cancelled; until then Start answers 409 crawls.already_running, DeleteCrawls refuses it and Prune keeps it"
|
||||
- "L3 WR-06: chromeRenderer.Render bounds opening the tab and rendering the page with one deadline (TabOpenTimeout 5 s plus RenderTimeout 20 s), so the crawl's render lock is never held longer than that"
|
||||
- "L3 WR-03: Fetcher.Allowed and Fetcher.Key and the Chrome request guard allowRequest accept a URL only on the site's effective port (explicit, else 443 for https and 80 for http); a link to https://<site>:8443/x is neither fetched nor reported as https://<site>/x"
|
||||
- "L3 WR-04: crawl keys, the rendered comparison, CMS lookups (Lookup, Keyphrases) and the link graph (LinksOfURL, LinksOfRecord) use one decoded path form; a non-ASCII address matches its CMS record and its stored crawl links; Fetcher.Absolute escapes the path for the request; a path that does not decode to valid UTF-8 text without NUL keeps its escaped form"
|
||||
- "L2 WR-01 (D-31): IndexNow is notified of a record deleted through pact.RecordDeleted (its listed addresses as removed) and of a content edit through pact.RecordSaved that moves lastmod (its listed addresses as changed): the listeners mark the rows indexnow_pending_at and the next rebuild sends them; TestIndexNow fires both events"
|
||||
- "L2 WR-02 (D-30): every rebuild reads settings, types and records and applies them under one transaction-scoped advisory lock, so a second rebuild collects only after the first has committed and an older snapshot never overwrites a newer one"
|
||||
- "L2 WR-03 (D-30): a rebuild never writes lastmod for a content row it did not create, so a lastmod that RecordSaved moved while the rebuild ran stays"
|
||||
- "L2 WR-04 (D-32): migration 202610280002 copies every convertible url item of the Phase 17 definition (a relative path gets its leading slash; spaces and the bytes \" < > \\ are percent-encoded), keeps the other items of a definition with one malformed item, and logs one warning per url item it cannot convert with the item and the reason; the v0.2.0 release-note draft says so"
|
||||
- "L2 WR-05 (D-34): Settings › Sitemaps loads no heading for a content type without a stored one, so a save that did not touch a heading stores none and each locale's HTML sitemap keeps its translated type label"
|
||||
- "L2 IN-01: classes.RecordSaved returns nil for a nil event without reading it"
|
||||
- "L1 CR-01 (D-85): a width whose variant height would exceed 4096 px is skipped with a warning and counted in Skipped; the image variants job and journal:image-variants go on with the next widths and images"
|
||||
- "L1 CR-02 (D-50): with the H1 setting on, ConvertContentH1 rewrites content only when it is HTML (classes.LooksLikeHTML); markdown content, fenced and inline code samples included, is kept byte for byte while content_html is converted; the count is content_html's headings; this holds for API saves, imports, journal:fix-headings and translations"
|
||||
- "L1 WR-03: ConvertH1 and SetImageAlt keep the bytes of an unfinished tag at the end of the input (input without a match comes back identical)"
|
||||
- "L1 WR-01 (D-50): journal:fix-headings runs from the generated application CLI: without a published database it opens database.dsn (lookupDB), and without a DSN it names SUMMER_DATABASE__DSN"
|
||||
- "L1 WR-05 (D-85, D-10): POST /_journal/api/v1/images/alt on a markdown post writes the alt into the markdown source (an inline image whose destination is the src, or a raw img tag, outside code), so the next content save keeps it; an image the markdown does not hold that way is refused with 422 changes.N.src and nothing is written"
|
||||
- "G3 (D-55, D-57, SC-2): the Journal post, category, tag and author descriptors give a non-default enabled locale a URL only when the record has content in it (P33-64); every other enabled locale keeps its entry with URL \"\" (pact: no public page); the menu item types' Resolve alternates are unchanged"
|
||||
- "G3: TestAlternates (sm-seo-plugin) and TestHreflangAlternates (sm-sitemap-plugin) use Journal-shaped records (every enabled locale present in sort order, an untranslated one with URL \"\" and the default locale's title, description and word count) and still emit no hreflang for the untranslated locale"
|
||||
- "L1 WR-02 (docs only): the lagoon README and docs/database/attachments.md say that importing attach/webp also takes over WebP decoding for the whole process and what -tags nodynamic changes; no framework Go file changes"
|
||||
- "G2 is not fixed, with the reason recorded in deferred-items.md and the SUMMARY: signpost.Activate refuses a menu item type without a DefaultPattern starting with /, so the change needs the framework"
|
||||
prohibitions:
|
||||
- requirement_id: "D-106"
|
||||
category: compatibility
|
||||
status: resolved
|
||||
verification: test
|
||||
statement: "sm-journal-plugin is a core plugin: no exported identifier is removed or renamed and no exported signature changes; the menu item types' Resolve output is unchanged (TestMenuItemTypesSmoke and menu_item_types_test.go pass unchanged)"
|
||||
- requirement_id: "D-90"
|
||||
category: scope
|
||||
status: resolved
|
||||
verification: test
|
||||
statement: "No Go file of summercms.go changes; the framework gets docs lines only (scripts/check-phase33.sh --additive --marker --docs pass)"
|
||||
- requirement_id: "D-95"
|
||||
category: safety
|
||||
status: resolved
|
||||
verification: test
|
||||
statement: "sm-seo-plugin still imports only sm-sitemap-plugin/contract outside tests, and sm-journal-plugin imports no SEO or sitemap package"
|
||||
artifacts:
|
||||
- path: "../sm-seo-plugin/updates/202611040001_create_golem15_seo_noindex_paths.go"
|
||||
provides: "the stored noindex paths of the route report"
|
||||
contains: "golem15_seo_noindex_paths"
|
||||
- path: "../sm-sitemap-plugin/contract/contract.go"
|
||||
provides: "contract.Rebuilder"
|
||||
contains: "Rebuilder interface"
|
||||
- path: "../sm-seo-plugin/updates/202611040002_add_golem15_seo_crawls_heartbeat.go"
|
||||
provides: "heartbeat_at and cancel_requested_at on golem15_seo_crawls"
|
||||
contains: "heartbeat_at"
|
||||
- path: "../sm-sitemap-plugin/updates/202611040001_add_golem15_sitemap_urls_indexnow_pending.go"
|
||||
provides: "indexnow_pending_at on golem15_sitemap_urls"
|
||||
contains: "indexnow_pending_at"
|
||||
- path: "../sm-seo-plugin/crawl_lifecycle_test.go"
|
||||
provides: "running cancel, timeout and stale recovery tests"
|
||||
contains: "TestCrawlCancelRunning"
|
||||
key_links:
|
||||
- from: "../sm-seo-plugin/controllers/api/registry.go"
|
||||
to: "../sm-seo-plugin/classes/registry.go"
|
||||
via: "RouteReport decodes noindex and calls ReportNoindex in the same write transaction"
|
||||
pattern: "ReportNoindex"
|
||||
- from: "../sm-seo-plugin/classes/sitemap_filter.go"
|
||||
to: "golem15_seo_noindex_paths"
|
||||
via: "newFilterBatch loads the set; decideTarget excludes with contract.ReasonNoindex"
|
||||
pattern: "noindex"
|
||||
- from: "../sm-seo-plugin/classes/registry.go"
|
||||
to: "../sm-sitemap-plugin/contract/contract.go"
|
||||
via: "after commit, app.Lookup[contract.Rebuilder]().RequestRebuild when the set changed"
|
||||
pattern: "contract.Rebuilder"
|
||||
- from: "../sm-seo-plugin/classes/crawl/job.go"
|
||||
to: "context.WithoutCancel"
|
||||
via: "finish runs on a detached, bounded context"
|
||||
pattern: "WithoutCancel"
|
||||
- from: "../sm-sitemap-plugin/classes/rebuild.go"
|
||||
to: "pg_advisory_xact_lock"
|
||||
via: "collect and apply inside one locked transaction"
|
||||
pattern: "pg_advisory_xact_lock"
|
||||
---
|
||||
|
||||
<!-- planner-discipline-allow: seo -->
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** site editor and as an SEO specialist auditing a SummerCMS site, **I want to** have the rendered sites emit complete SEO output (sitemaps, structured data, pagination, hreflang, headings, breadcrumbs) from the CMS, and an audit that finds the remaining problems, **so that** sites are indexed correctly and issues can be fixed through MCP.
|
||||
|
||||
This gap-closure plan fixes the Go side of the gaps and review findings that block the 33-14 release: G1 (CMS side), G3 and the in-scope findings of review lanes L1 (Journal), L2 (sitemap) and L3 (SEO). Plan 33-16 fixes the Nuxt layers in parallel (same wave, no shared files; the gap-2 contract below is fixed in both plans). 33-14 Task 4 (the release) resumes after both, followed by re-verification.
|
||||
|
||||
<objective>
|
||||
Fix, each with a test that fails before the fix, in three plugin repositories (each main, not pushed, not tagged):
|
||||
- sm-seo-plugin: the route report's noindex paths and the sitemap filter (G1, D-24, D-33); the crawl lifecycle (L3 CR-01, WR-01, WR-06, D-14); the crawl's port and path form (L3 WR-03, WR-04);
|
||||
- sm-sitemap-plugin: contract.Rebuilder (G1, D-95); IndexNow for deletes and edits, the rebuild lock, kept lastmods (L2 WR-01 to WR-03, IN-01, D-30, D-31); the definitions migration and the HTML sitemap headings (L2 WR-04, WR-05, D-32, D-34);
|
||||
- sm-journal-plugin (core plugin: additive and non-breaking): tall images (L1 CR-01, D-85), markdown left alone by the H1 conversion (L1 CR-02, D-50), the tokenizer tails (L1 WR-03), journal:fix-headings in the CLI (L1 WR-01), alt texts on markdown posts (L1 WR-05), descriptors listing only locales with content (G3, D-57);
|
||||
- summercms.go: docs lines only (L1 WR-02 decoder note) and the planning docs (release-note drafts, release log, deferred items).
|
||||
|
||||
Purpose: close G1 (CMS side) and G3 and the release-blocking L1 to L3 findings before the tags.
|
||||
|
||||
Output: commits in sm-seo-plugin, sm-sitemap-plugin, sm-journal-plugin and summercms.go (docs, then planning docs as a separate commit).
|
||||
|
||||
**Plan size (Jakub, 2026-11-04):** the gap fixes are two plans, fewer and larger, by Jakub's explicit choice (project rule "lean planning"); the plan checker's recommendation to split this plan is overridden by that decision, and the six tasks stay here. Earlier phase-33 executors ran plans of this size. Each task is independently committable and leaves every repository green (see the executor note under "Execution units").
|
||||
|
||||
**Planner decisions recorded here (P33-58 to P33-66; 33-16 continues at P33-67):**
|
||||
- **P33-58 (noindex paths):** table `golem15_seo_noindex_paths` (`path text PRIMARY KEY`, `reported_at timestamp NOT NULL`), model `models.NoindexPath`, migration 202611040001. `classes.(*Service).ReportNoindex(ctx, tx, paths []string) (stored int, skipped int, changed bool, err error)` replaces the set inside the route report's write transaction; entries are checked with `ValidPagePath` and stored through `normalPagePath(path, opts)` exactly like `routes`. `ReportRoutes` keeps its signature. The filter checks the set after the normalisation and redirect checks and before `resolveAt`.
|
||||
- **P33-59 (rebuild on request):** `contract.Rebuilder` with `RequestRebuild(ctx context.Context) error`: "queue one debounced rebuild, the same one a record event queues". `classes.Listing` implements it with `RequestDebouncedRebuild`; the sitemap plugin publishes it with `app.Publish[contract.Rebuilder]` in `Register`, next to `contract.Listing`. Absent (an older sitemap plugin) means no request; the nightly rebuild still picks the set up.
|
||||
- **P33-60 (crawl lifecycle):** `crawl.JobTimeout = 6 * time.Hour` (jobs.go uses it), `crawl.FinishTimeout = 10 * time.Minute`, `crawl.HeartbeatEvery = time.Minute`, `crawl.StaleAfter = 10 * time.Minute`; migration 202611040002 adds `heartbeat_at timestamp NULL` and `cancel_requested_at timestamp NULL` to `golem15_seo_crawls`. The job owns every terminal status of a running crawl; `RecoverStale(tx, now)` ends running rows whose `COALESCE(heartbeat_at, started_at, created_at)` is older than StaleAfter.
|
||||
- **P33-61 (one crawl path form):** the decoded `u.Path` ("/" when empty) plus `?` and the raw query is the crawl key everywhere; `Fetcher.Absolute` escapes it; a path whose decoded form is not valid UTF-8 without NUL (the resolver's `safeText` rule) keeps its escaped form.
|
||||
- **P33-62 (IndexNow pending):** column `golem15_sitemap_urls.indexnow_pending_at timestamp NULL` (migration 202611040001); the listeners set it, `apply` reads it into `RebuildResult.Changed` (listed rows) or `Removed` (rows that were listed), and clears it with `WHERE id = ? AND indexnow_pending_at = <the value read>`, so a mark made after the read survives for the next rebuild.
|
||||
- **P33-63 (rebuild lock):** `SELECT pg_advisory_xact_lock(hashtext('golem15.sitemap.rebuild'))` at the start of one transaction that loads the settings and types, runs `collect` and then `apply` (with the existing `FOR UPDATE` of the settings row before `apply`).
|
||||
- **P33-64 (G3 rule, "has content in a locale"):** the locale whose values are the host columns (translate's default code, else the unprefixed locale, else the first enabled one) always has content. Posts and categories: a non-default locale has content when the record's translate row in it holds a truthy (`phpTruthy`) value of a text field (post: title, content, content_html, excerpt; category: name, description). Tags and authors are not translatable: a non-default locale has content when at least one of their published posts has such a translation. The descriptors keep one entry per enabled locale (as now) and set `URL: ""` for the others, which is pact's documented "no public page" form that the SEO and sitemap plugins already drop; `menuAlternates` and every menu Resolve stay as they are.
|
||||
- **P33-65 (alt texts on markdown):** `classes.SetMarkdownImageAlt(md, src, alt string) (string, int, *string)`: inline images `` and `` outside fenced and indented code blocks and code spans (ranges from goldmark's AST, the FormatHTML engine) match when the destination, escaped as goldmark's HTML renderer escapes it, equals src, or equals src as written; the label is replaced by alt with `\`, `[` and `]` backslash-escaped and line breaks folded to spaces. For markdown content the raw-`<img>` rewrite (SetImageAlt) also runs only on the text outside those code ranges.
|
||||
- **P33-66 (legacy url items):** `models.ConvertLegacyItems(data []byte) (rows []ExtraURL, dropped []DroppedItem)` (pure): url items only; a value without a scheme and without a leading `/` gets one; spaces and `"`, `<`, `>`, `\` in the path are percent-encoded; the result must pass `models.AllowedAddress` (an absolute URL with `&` still does not, so it is dropped with that reason); duplicates are kept once without a log line; a malformed item is dropped with its parse error and the others are kept. The migration logs each dropped item at warn level as `golem15.sitemap: definition url item not copied` with the item label, the value and the reason.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-VERIFICATION.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-REVIEW.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/deferred-items.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-CONTEXT.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-PATTERNS.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-05-SUMMARY.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-10-SUMMARY.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
sm-seo-plugin:
|
||||
- controllers/api/registry.go: `routeReport{Routes []string}`, `maxRouteReportBody = MaxReportedRoutes*(MaxRouteBytes+8)+1024`, `RouteReport` handler (constant-time token, `write(ctx, svc, fn)` transaction, 422 `too_many_routes`).
|
||||
- classes/registry.go: `ReportRoutes(ctx, tx, paths) (RouteReport, error)`, `RouteReport{Inserted, Updated, Skipped, ReportedAt}`, `normalPagePath(path, opts)`, `ValidPagePath`, `MaxReportedRoutes` (2000), `MaxRouteBytes` (2048), `SettingsRow`, `NormalizeOptionsOf`.
|
||||
- classes/sitemap_filter.go: `SitemapFilter.Decide`, `newFilterBatch` (settings, redirect set, site), `decideTarget` (Normalize → redirect chain → `resolveAt` → robots/canonical), `candidatePath`.
|
||||
- classes/crawl/job.go: `Run` (db on the job ctx), `run`, `finish(ctx, svc, db, row, res, runErr)`, `Start` (settings row FOR UPDATE, `ActiveCrawl`), `Cancel` (writes status cancelled at once), `Lookup`, `Keyphrases`; store.go `LatestCrawl`, `ActiveCrawl`; actions.go `DeleteCrawls`, `Prune`; crawler.go worker loop (`pace.wait`, `f.Get`, `renderMu`, `opt.Progress`); fetch.go `Fetcher.Allowed`, `Key` (EscapedPath), `Absolute`, `Get` (`network_error`); render.go `allowRequest(rawURL, hosts, trusted)`; render_chrome.go `newChromeRenderer(path, hosts, trusted, ua)`, `Render` (`chromedp.NewContext(r.browserCtx)`, `context.AfterFunc(ctx, closeTab)`, `chromedp.Do(tab)`, then `RenderTimeout`); compare.go `same` (EscapedPath); checks.go `Env{Lookup, Staging, GenericAnchors, Keyphrases}`, `checkSitemaps` (sitemap_noindex with `ToolSetURLSEO`); fix.go tool constants and `Route`.
|
||||
- jobs.go `conga.Job(p.crawl, conga.MaxAttempts(1), conga.Timeout(6*time.Hour))`; mcp.go `cancel_crawl` description; controllers/crawls.go status labels; linkgraph.go `sitePath` (EscapedPath for absolute input).
|
||||
sm-sitemap-plugin:
|
||||
- contract/contract.go `Filter`, `Listing`, `ListingSettings`; plugin.go `Register` publishes `contract.Listing` (classes.Listing{App: app}).
|
||||
- classes/lastmod.go `RecordSaved` (nil check after reading ev), `RecordDeleted`, `RequestDebouncedRebuild(ctx, app, db)`, `contentTypeOf`.
|
||||
- classes/rebuild.go `Rebuild` (collect before the FOR UPDATE transaction), `collect`, `apply` (reads every slot row, writes every column of a dirty row including lastmod; skips a deleted row whose reason did not change before appending Removed), `RebuildResult{Added, Changed, Removed, Listed, Files}`; classes/indexnow.go `NotifyIndexNow`, `SiteURLs`.
|
||||
- updates/202610280002 `copyDefinitions` (ParseLegacyItems fails the whole definition on one bad item; AllowedAddress silently drops items); models/legacy.go `ParseLegacyItems`, `LegacyEntries`, `ParseLegacyItem`, `LegacyItemURL`; models/url.go `AllowedAddress`; models/settings.go `AllowedFrontendURL`.
|
||||
- extensions_settings.go `typeSettingsTab` Load (prefills a missing title with `{def: Phrase(ctx, tr, typ.Label)}` in the admin's UI locale); classes/typesettings.go `SaveTypeSettings`; classes/htmlgroups.go `pickTitle`, `translateIn`.
|
||||
- Tests: content_fixture_test.go `store.put/remove` (remove fires no event), filter_fixture_test.go `fakeFilter`/`theFilter`, sitemap_index_test.go fires events with `e.app.Events.Fire(ctx, &pact.RecordDeleted{Model: pageModel, ID: "1"})`, `TestIndexNow` (sitemap_extras_test.go), `TestHreflangAlternates`, `TestMigrateDefinitions`.
|
||||
sm-journal-plugin:
|
||||
- classes/image_variants.go `GenerateImageVariants` (h = sh*w/sw, `f.ThumbKey` errors above 4096 px abort the batch), `MaxImageWidth = 4096`, `variantsLog`.
|
||||
- classes/headings.go `ConvertH1`, `ConvertContentH1` (converts content too), `ConvertTranslationH1`; classes/image_alt.go `SetImageAlt` (both loops break on ErrorToken without writing the rest); classes/blog_editor.go `LooksLikeHTML`; classes/format_html.go `markdownEngine`, `FormatHTML`.
|
||||
- console/fix_headings.go uses `mustDB(app)`; console/database.go `lookupDB(ctx, app)`; image_variants_cli_test.go `TestImageVariantsCommandCLI` (pattern for the CLI test).
|
||||
- controllers/api/maintenance.go `SetImageAlt` (rewrites content_html, and content only when it holds a literal img), `loadImageAltText`, `writeImageAltTexts`.
|
||||
- classes/content_descriptors.go `describePosts`, `describeCategories`, `describeTags`; classes/menu_item_types.go `describeAuthors`, `menuAlternates`, `loadMenuLocales`, `menuTranslations.value`, `phpTruthy`, `publishedPosts`; models/post.go pivot `golem15_journal_posts_tags`; models Post/Category `Translatable()`, Tag not translatable.
|
||||
- signpost (framework, read only): `signpost.Activate` refuses an empty DefaultPattern (modules/signpost/signpost.go:85-86), the G2 reason.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Gap-2 contract (fixed; identical in 33-15 and 33-16)
|
||||
|
||||
The route report gains an optional `noindex` array of static paths, sent with the same token on the same request:
|
||||
|
||||
```
|
||||
POST /_seo/api/v1/registry/routes
|
||||
Authorization: Bearer <golem15.seo.route_report_token, else golem15.seo.notfound_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"routes": ["/", "/blog", "/kontakt"], "noindex": ["/konto", "/pl/konto", "/szkic"]}
|
||||
```
|
||||
|
||||
1. `routes`: unchanged (required array of strings).
|
||||
2. `noindex`: optional array of strings, at most 2000 entries, each starting with `/`, without a `:` parameter or `*`, at most 2048 bytes in UTF-8. It lists every static path the frontend's build serves noindex: pages whose meta has `requiresAuth: true` or a `seo.robots` value with `noindex` or `none`, and static pages under a `seo.noindexRoutes` pattern. The `@nuxtjs/i18n` copies of those pages in other locales are listed as their own locale-prefixed paths (`/pl/konto`). Patterns (`/p/**`) are never sent, the layer's HTML sitemap pages never, and a path is never in both lists.
|
||||
3. Plugin semantics: key absent → the stored set is unchanged; key present (also `[]`) → the stored set becomes exactly its valid entries, normalised like `routes` (`normalPagePath` with the site's address options); an invalid entry is skipped and counted in `skipped`; more than 2000 entries → 422 `{"error":"too_many_routes","max":2000}` and nothing is stored.
|
||||
4. The answer gains one key, the number of stored paths: `{"inserted":1,"updated":3,"skipped":0,"reported_at":"2026-11-04T20:00:00Z","noindex":3}`.
|
||||
5. The SEO sitemap filter excludes any candidate (record, registry page, additional address) whose normalised path is in the set, with reason `noindex`, from the XML and HTML sitemaps. A changed set requests one debounced sitemap rebuild.
|
||||
6. Compatibility: a layer that sends no `noindex` keeps working (the set stays as it is). A plugin built before this contract decodes the body into `{routes}` and ignores the unknown key (encoding/json without DisallowUnknownFields), and it reads at most `MaxReportedRoutes*(MaxRouteBytes+8)+1024` = 2000*(2048+8)+1024 = 4,113,024 bytes of body (sm-seo-plugin controllers/api/registry.go:21, `maxRouteReportBody`). The layer therefore keeps the serialised JSON body at or under 4,113,024 bytes: it drops `noindex` entries from the end first, and only when the body is still too large `routes` entries from the end, so an older plugin never refuses the body for its size. A plugin built with this contract accepts at least that size (its cap covers both lists). The layer ignores the answer body.
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Run from summercms.go. Commit groups per repository, staged by explicit path (never `git add -A`); no attribution trailers; code and planning docs in separate commits; a README or docs change goes in the same commit as the API change it describes. Nothing is pushed or tagged. STATE.md changes only through `msd_run query state.*` handlers and is never committed.
|
||||
- Every commit keeps that repository's `go vet ./...` and `go test -short ./...` green; the Docker suites (`go test ./...`) run per task. If the Docker socket is denied although the user is in the docker group, run the Docker tests through `sudo -u golem` (CLAUDE.md).
|
||||
- 33-16 runs in parallel and commits planning docs to summercms.go too: if git reports `.git/index.lock`, wait and retry; never remove another session's lock or stage its files. Leave other sessions' uncommitted edits (STATE.md, state.json, G15OFFICE.md) alone.
|
||||
- Resume check per repository: `git -C <repo> log --format=%s | grep -Fx '<subject>'`.
|
||||
- Before Task 1, record each repository's HEAD as BASE in the SUMMARY draft (summercms.go, sm-seo-plugin, sm-sitemap-plugin, sm-journal-plugin); the dependency and compatibility checks compare against it.
|
||||
- **Executor note (context budget):** if the context runs low, stop only after a complete task (all its commit groups committed and its verify green), never inside one; write the SUMMARY with the finished tasks and list the remaining tasks, with their resume points from the table below, for a continuation run. Every task is independently committable.
|
||||
- **Gate stages:** this plan runs only its own stages of `scripts/check-phase33.sh` (self-test, go, plugins, docker, admin, docs, marker, additive, compat), never layers or consumers, because 33-16 changes the layers in parallel. The orchestrator runs the full `PHASE33_REQUIRE_DOCKER=1 bash scripts/check-phase33.sh --all` once, after both 33-15-SUMMARY.md and 33-16-SUMMARY.md exist and before 33-14 Task 4.
|
||||
- Every `<automated>` command starts with `set -o pipefail`, or chains with `&&`, so a failing command is never hidden by a later pipe stage.
|
||||
|
||||
| Task | Commit groups, in order | Resume point |
|
||||
|------|-------------------------|--------------|
|
||||
| 1 | sitemap: A `feat: contract.Rebuilder asks for a debounced rebuild`; seo: B `feat: noindex paths from the route report leave the sitemaps (tracer)` | TestNoindexPathsTracer passes |
|
||||
| 2 | seo: A `fix: a cancelled or timed-out crawl stores what it fetched and ends`; B `fix: a crawl cancel waits for its job, and a stale crawl is ended`; C `fix: bound opening a Chrome tab` | TestCrawlCancelRunning, TestCrawlTimeout, TestCrawlStaleRecovery, TestRenderTabTimeout pass |
|
||||
| 3 | seo: A `fix: the crawler and the Chrome guard keep to the site's port`; B `fix: one decoded path form for crawl keys and CMS lookups` | TestFetcherPortAndKey and TestCrawlerNonASCII pass |
|
||||
| 4 | sitemap: A `fix: IndexNow hears of deleted and edited records`; B `fix: rebuilds collect under one lock and keep moved lastmods`; C `fix: the definitions migration keeps and logs every url item`; D `fix: HTML sitemap headings follow each locale until an editor sets one` | TestIndexNow, TestConcurrentRebuilds, TestRebuildKeepsMovedLastmod, TestConvertLegacyItems, TestTypeTitlesNotPinned pass |
|
||||
| 5 | journal: A `fix: tall originals no longer stop the image variants job`; B `fix: H1 conversion leaves markdown and unfinished tags alone`; C `fix: journal:fix-headings runs from the application CLI`; D `fix: image alt texts on markdown posts are written into the markdown` | TestImageVariantsTallOriginal, TestFixHeadingsCommandCLI, TestSetMarkdownImageAlt pass |
|
||||
| 6 | journal: A `fix: content descriptors list only locales with content`; seo: B `test: Journal-shaped records in the hreflang test`; sitemap: C `test: Journal-shaped records in the hreflang test`; framework: D `docs(lagoon): attach/webp also decodes WebP`; here: E `docs(33-15): release notes, release log and deferred items for the gap fixes` | plan complete |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "a route report names /subscriptions noindex and the next sitemap rebuild leaves that Additional address out" — one path only (G1, CMS side)</name>
|
||||
<files>../sm-sitemap-plugin/contract/contract.go, ../sm-sitemap-plugin/classes/listing.go, ../sm-sitemap-plugin/plugin.go, ../sm-sitemap-plugin/README.md, ../sm-seo-plugin/updates/202611040001_create_golem15_seo_noindex_paths.go, ../sm-seo-plugin/models/noindex_path.go, ../sm-seo-plugin/classes/registry.go, ../sm-seo-plugin/controllers/api/registry.go, ../sm-seo-plugin/classes/sitemap_filter.go, ../sm-seo-plugin/classes/crawl/checks.go, ../sm-seo-plugin/classes/crawl/fix.go, ../sm-seo-plugin/classes/crawl/job.go, ../sm-seo-plugin/README.md, ../sm-seo-plugin/sitemap_filter_test.go, ../sm-seo-plugin/registry_test.go, ../sm-seo-plugin/classes/crawl/checks_test.go</files>
|
||||
<read_first>
|
||||
- ../sm-seo-plugin/controllers/api/registry.go (RouteReport) and ../sm-seo-plugin/classes/registry.go (ReportRoutes, normalPagePath, ValidPagePath, RouteReport struct)
|
||||
- ../sm-seo-plugin/classes/sitemap_filter.go (newFilterBatch, decideTarget) and ../sm-seo-plugin/sitemap_filter_test.go (TestSitemapFilterTracer: how the test boots the sitemap plugin and calls sitemapclasses.Rebuild)
|
||||
- ../sm-seo-plugin/registry_test.go (TestRouteReport: token, body, answer)
|
||||
- ../sm-sitemap-plugin/contract/contract.go, ../sm-sitemap-plugin/plugin.go (Register publishes contract.Listing), ../sm-sitemap-plugin/classes/lastmod.go (RequestDebouncedRebuild)
|
||||
- ../sm-seo-plugin/classes/crawl/checks.go (checkSitemaps, Env) and fix.go (tool constants, Route)
|
||||
- ../sm-seo-plugin/updates/202610280007_create_golem15_seo_autolinks.go (migration pattern)
|
||||
- the "Gap-2 contract" section of this plan
|
||||
</read_first>
|
||||
<action>
|
||||
Per G1, D-24, D-33, D-95 and the gap-2 contract above (P33-58, P33-59), in this order:
|
||||
- (A, sitemap) Add `contract.Rebuilder` (`RequestRebuild(ctx context.Context) error`, doc comment: asks for one debounced rebuild, the same one a record event queues; the sitemap plugin publishes it with `app.Publish[contract.Rebuilder]` in its Register). `classes.Listing` implements it by looking up the app's `*gorm.DB` and calling `RequestDebouncedRebuild`; plugin.go publishes it next to `contract.Listing`. README: contract section names the interface. Add an assertion to an existing Docker test that publishes and calls it (for example TestListing: after `RequestRebuild`, `rebuild_pending_until` is set). Commit A.
|
||||
- (B, seo) Migration 202611040001 creates `golem15_seo_noindex_paths` (P33-58, with Rollback); model `models.NoindexPath`. `ReportNoindex` (P33-58) replaces the set in the caller's transaction and reports whether it changed; on a change it registers `lagoon.AfterCommit` that looks up `contract.Rebuilder` and calls `RequestRebuild` (a missing Rebuilder does nothing; an error is logged at warn). The handler decodes `noindex` as `*[]string` (absent vs present), raises the body cap to cover both arrays (two lists of MaxReportedRoutes entries), refuses more than MaxReportedRoutes noindex entries with the same 422, calls ReportNoindex in the same `write` transaction only when the key is present, and adds `noindex` (the stored count) to the answer; the handler doc comment states the semantics. `newFilterBatch` loads the set once per batch; `decideTarget` excludes a candidate whose normalised path (`norm.Path` after the normalisation and redirect checks) is in the set with `contract.ReasonNoindex`, before `resolveAt`. Crawler: add `ToolSitemapSettings = "sitemap_update_settings"` to fix.go; `Env` gains `SitemapTool string`, which the job fills (in `finish`) with ToolSitemapSettings when `svc.App()` publishes `contract.Listing`; in `checkSitemaps` a sitemap_noindex finding with a CMS record keeps ToolSetURLSEO, one without a record names `env.SitemapTool`, and with an empty SitemapTool gets no tool (Route then answers fix: code with What "the address list of the sitemap that names this page"); details gain `"extra": true` when there is no record. README: the route report section gets the exact JSON of the gap-2 contract and the filter's new reason source; the crawler section names the tool. Tests first: `TestNoindexPathsTracer` (Docker, in sitemap_filter_test.go): Additional address `/subscriptions` listed; a route report with `"noindex": ["/subscriptions"]` answers 200 with `"noindex": 1` and sets the sitemap's `rebuild_pending_until`; a rebuild leaves `/subscriptions` out with reason noindex (XML file and the HTML API); a report without the key keeps it out; a report with `"noindex": []` lists it again after the next rebuild; a second identical report requests no rebuild. TestRouteReport gains: a body with an invalid noindex entry counts it in `skipped`; 2001 noindex entries answer 422 and store nothing; a body with only `routes` answers as before plus `noindex`. checks_test.go `TestSitemapNoindexTool`: the three routes above. Commit B.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./... -run 'TestNoindexPathsTracer|TestRouteReport|TestSitemapFilterTracer|TestFilterReasons' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestNoindexPathsTracer" line</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./classes/crawl -run 'TestSitemapNoindexTool|TestRoute|TestRouteEveryKind' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -10</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestSitemapNoindexTool" line</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-sitemap-plugin test ./... -run 'TestListing' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -6</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, "no tests to run", or no "--- PASS: TestListing" line</fails_when>
|
||||
<automated>go -C ../sm-sitemap-plugin vet ./... && go -C ../sm-sitemap-plugin test -short ./... && go -C ../sm-seo-plugin vet ./... && go -C ../sm-seo-plugin test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "Rebuilder interface" ../sm-sitemap-plugin/contract/contract.go` prints one line; `grep -n "contract.Rebuilder" ../sm-sitemap-plugin/plugin.go` prints a line
|
||||
- `grep -n 'json:"noindex' ../sm-seo-plugin/controllers/api/registry.go` prints a line and `grep -n "ReportNoindex" ../sm-seo-plugin/controllers/api/registry.go ../sm-seo-plugin/classes/registry.go` prints a line in each
|
||||
- `grep -n "golem15_seo_noindex_paths" ../sm-seo-plugin/updates/202611040001_create_golem15_seo_noindex_paths.go` prints at least one line
|
||||
- `grep -n "sitemap_update_settings" ../sm-seo-plugin/classes/crawl/fix.go` prints one line
|
||||
- `grep -n '"noindex"' ../sm-seo-plugin/README.md` prints a line (the contract JSON)
|
||||
- `go -C ../sm-seo-plugin list -deps . | grep sm-sitemap-plugin` lists only `.../sm-sitemap-plugin/contract` (D-95)
|
||||
</acceptance_criteria>
|
||||
<done>A noindex static path the frontend reports leaves the sitemaps on the next rebuild, and the crawler names the tool that really removes a listed noindex address.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Crawl lifecycle: detached finish, timeout and stale recovery, cancel owned by the job, bounded Chrome tab (L3 CR-01, WR-01, WR-06)</name>
|
||||
<files>../sm-seo-plugin/updates/202611040002_add_golem15_seo_crawls_heartbeat.go, ../sm-seo-plugin/models/crawl.go, ../sm-seo-plugin/classes/crawl/job.go, ../sm-seo-plugin/classes/crawl/store.go, ../sm-seo-plugin/classes/crawl/actions.go, ../sm-seo-plugin/classes/crawl/crawler.go, ../sm-seo-plugin/classes/crawl/render_chrome.go, ../sm-seo-plugin/jobs.go, ../sm-seo-plugin/controllers/api/crawls.go, ../sm-seo-plugin/controllers/crawls.go, ../sm-seo-plugin/mcp.go, ../sm-seo-plugin/lang/en/lang.yaml, ../sm-seo-plugin/lang/pl/lang.yaml, ../sm-seo-plugin/README.md, ../sm-seo-plugin/crawl_lifecycle_test.go, ../sm-seo-plugin/classes/crawl/render_test.go</files>
|
||||
<read_first>
|
||||
- ../sm-seo-plugin/classes/crawl/job.go (Run, run, finish, Start, Cancel, LoadLimits) and store.go (LatestCrawl, ActiveCrawl), actions.go (DeleteCrawls, Prune)
|
||||
- ../sm-seo-plugin/classes/crawl/crawler.go (the worker loop: pace.wait, f.Get, renderMu, Progress)
|
||||
- ../sm-seo-plugin/classes/crawl/render_chrome.go (Render) and render_test.go (package crawl)
|
||||
- ../sm-seo-plugin/crawl_admin_test.go (how a crawl is started, run with crawl.Run and a fetchguard.WithTransport fixture, and cancelled) and crawler_test.go (TestCrawlerTracer fixture site)
|
||||
- ../summercms.go/modules/conga/conga.go (CancelJob, CheckIfCanceled, FailJob, StopJob)
|
||||
- 33-REVIEW.md lane 3 CR-01, WR-01, WR-06
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestCrawlCancelRunning (Docker): a crawl whose fixture transport blocks on its third page is cancelled through POST /_seo/api/v1/crawls/{id}/cancel; the row stays running with cancel_requested true; a Start answers 409 crawls.already_running; DELETE /crawls?ids= refuses it; Prune keeps it; then the job context is cancelled (as River does) and the transport released: the row ends cancelled with finished_at set, the two pages fetched before are stored with their issues, LatestCrawl returns it, and no page is stored as network_error
|
||||
- TestCrawlTimeout (Docker): crawl.Run under a context that reaches its deadline mid-crawl stores the pages fetched so far and ends failed with an error naming the time limit and finished_at set; a later Start succeeds
|
||||
- TestCrawlStaleRecovery (Docker): a running row with heartbeat_at 11 minutes old is ended failed (no-heartbeat error, finished_at) by the next Start, which then queues a new crawl; a running row with cancel_requested_at and an old heartbeat ends cancelled; a running row with a fresh heartbeat still blocks Start with 409
|
||||
- TestRenderTabTimeout (package crawl, no Chrome): a chromeRenderer whose tab opener blocks returns an error within TabOpenTimeout (set to 50 ms in the test) plus a margin, and a second Render call is not blocked by the first
|
||||
</behavior>
|
||||
<action>
|
||||
Per L3 CR-01, WR-01, WR-06, D-14 and P33-60, tests first, three commit groups:
|
||||
- (A, CR-01) jobs.go uses `crawl.JobTimeout`. `Run`: after `run`, finish on `fctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), FinishTimeout)` with `gdb.WithContext(fctx)`, passing the job context's `ctx.Err()`; `finish` decides: runErr → failed; `context.DeadlineExceeded` → failed with the error "the crawl reached its time limit of 6h"; a requested cancel (fresh read of the row's cancel_requested_at or status), `context.Canceled` or `res.Canceled` → cancelled; a `LoadLimits` error falls back to the defaults (DefaultMaxPages, DefaultDelay, DefaultKeep) with a warn log instead of returning; conga FailJob/StopJob/CompleteJob and Prune run on fctx. crawler.go: a page whose `f.Get` returned an error while `ctx.Err() != nil` gets `Err "canceled"` (the checks already skip it). Commit A.
|
||||
- (B, WR-01 and stale recovery) Migration 202611040002 adds `heartbeat_at` and `cancel_requested_at` (P33-60, with Rollback); the model gains both fields. `Run` writes heartbeat_at with started_at and starts a heartbeat goroutine (every HeartbeatEvery, each update on `context.WithoutCancel` with a 10 s timeout) stopped after finish returns. `Cancel`: a queued row becomes cancelled with finished_at through an update conditioned on `status = 'queued'`; when that matches nothing, or the row is running, it sets cancel_requested_at and cancelled_by on the running row only and cancels the conga job; the status stays running. The run's `Canceled` callback also stops on cancel_requested_at. `RecoverStale(tx, now)` (store.go) ends running rows per P33-60 (cancelled when cancel_requested_at is set, else failed with the error "the crawl job stopped without finishing (no heartbeat since <time>)"), finished_at now, and fails or stops their conga jobs best effort; `Start` calls it after locking the settings row and before ActiveCrawl, `DeleteCrawls` before its active check. `crawlMap` (API and MCP) adds `cancel_requested` (bool); the Crawls list shows the new label `crawls.st_cancelling` (en "Cancelling", pl "Anulowanie") for a running row with a requested cancel; the `cancel_crawl` MCP description says the crawl keeps running until its job stops, then ends cancelled with the pages fetched so far, and that no other crawl starts before that; README crawl section likewise. Commit B.
|
||||
- (C, WR-06) render_chrome.go: `TabOpenTimeout = 5 * time.Second` (a package var the test may lower); Render derives `rctx, cancel := context.WithTimeout(ctx, TabOpenTimeout+RenderTimeout)` and registers `context.AfterFunc(rctx, closeTab)` instead of the crawl context, so a hung `chromedp.Do(tab)` returns when rctx ends; a tab that did not open in time answers "Chrome did not open a tab within 5 s". Add a seam for the test (a renderer field for the tab opener, defaulting to chromedp.NewContext and chromedp.Do). The crawler's renderMu section is then bounded by the same deadline. Commit C.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./... -run 'TestCrawlCancelRunning|TestCrawlTimeout|TestCrawlStaleRecovery|TestCrawlAdmin|TestCrawlTools|TestCrawlSchedule|TestCrawlerTracer|TestCrawlerChecks' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -20</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or any of TestCrawlCancelRunning, TestCrawlTimeout, TestCrawlStaleRecovery missing from the "--- PASS" lines</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./classes/crawl -run 'TestRenderTabTimeout|TestNewRenderer|TestRenderMax' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -8</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestRenderTabTimeout" line</fails_when>
|
||||
<automated>go -C ../sm-seo-plugin vet ./... && go -C ../sm-seo-plugin test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "WithoutCancel" ../sm-seo-plugin/classes/crawl/job.go` prints at least one line
|
||||
- `grep -n "heartbeat_at\|cancel_requested_at" ../sm-seo-plugin/updates/202611040002_add_golem15_seo_crawls_heartbeat.go` prints at least two lines
|
||||
- `grep -n "JobTimeout" ../sm-seo-plugin/jobs.go` prints one line
|
||||
- `grep -n "func RecoverStale" ../sm-seo-plugin/classes/crawl/store.go` prints one line
|
||||
- `grep -n "st_cancelling" ../sm-seo-plugin/lang/en/lang.yaml ../sm-seo-plugin/lang/pl/lang.yaml` prints a line in each
|
||||
- `grep -n "TabOpenTimeout" ../sm-seo-plugin/classes/crawl/render_chrome.go` prints at least two lines
|
||||
</acceptance_criteria>
|
||||
<done>A cancelled or timed-out crawl keeps what it fetched and ends; no crawl stays running after its job is gone; a second crawl cannot start while a cancelled one is still stopping; a hung Chrome cannot stall the crawl.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Crawl addresses: the site's port and one decoded path form (L3 WR-03, WR-04)</name>
|
||||
<files>../sm-seo-plugin/classes/crawl/fetch.go, ../sm-seo-plugin/classes/crawl/render.go, ../sm-seo-plugin/classes/crawl/render_chrome.go, ../sm-seo-plugin/classes/crawl/compare.go, ../sm-seo-plugin/classes/linkgraph.go, ../sm-seo-plugin/classes/crawl/fetch_test.go, ../sm-seo-plugin/classes/crawl/render_test.go, ../sm-seo-plugin/crawler_test.go, ../sm-seo-plugin/README.md</files>
|
||||
<read_first>
|
||||
- ../sm-seo-plugin/classes/crawl/fetch.go (NewFetcher, Allowed, Key, Absolute, Get) and render.go (allowRequest, resolver rules), render_chrome.go (newChromeRenderer, NewRenderer callers)
|
||||
- ../sm-seo-plugin/classes/crawl/compare.go (same) and ../sm-seo-plugin/classes/linkgraph.go (sitePath, LinksOfURL, LinksOfRecord)
|
||||
- ../sm-seo-plugin/classes/resolver.go (safeText) and classes/crawl/job.go (Lookup, Keyphrases)
|
||||
- ../sm-seo-plugin/classes/crawl/render_test.go (TestAllowRequest table) and ../sm-seo-plugin/crawler_test.go (fixture site, fetchguard.WithTransport)
|
||||
- 33-REVIEW.md lane 3 WR-03, WR-04
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestAllowRequest gains rows: https://example.test:8443/x refused; https://example.test:443/x allowed; trusted http on the site's port allowed, on another port refused; data: and blob: unchanged
|
||||
- TestFetcherPortAndKey (package crawl): Allowed and Key refuse https://site:8443/x and accept https://site:443/x as /x; Key of https://site/blog/za%C5%BC%C3%B3%C5%82%C4%87?a=1 is /blog/zażółć?a=1; Absolute of that key is https://site/blog/za%C5%BC%C3%B3%C5%82%C4%87?a=1; Key of https://site/%FF keeps /%FF
|
||||
- TestCrawlerNonASCII (Docker): the fixture site links /blog/zażółć percent-encoded from / and lists it encoded in its sitemap; the crawl fetches it once under the key /blog/zażółć, Lookup finds the fixture CMS record at that address (its record-content finding names the record type's tool), no false content_js links finding appears, and LinksOfURL with the encoded absolute URL reports one inbound link; a link to the site host on port 8443 is neither fetched nor stored
|
||||
</behavior>
|
||||
<action>
|
||||
Per L3 WR-03, WR-04, D-14 and P33-61, tests first:
|
||||
- (A, WR-03) NewFetcher records the site's effective port (explicit, else 443 for https and 80 for http) through a small `effectivePort(u *url.URL) string` helper; `Fetcher.Allowed` compares it (so Key refuses another port too); NewRenderer and newChromeRenderer take the port and `allowRequest(rawURL, hosts, port, trusted)` refuses any http(s) request on another effective port (data: and blob: unchanged); compare.go keeps using Key. README: the Chrome guard paragraph names the port rule. Commit A.
|
||||
- (B, WR-04) `Fetcher.Key` returns the decoded `u.Path` ("/" when empty) plus `?` and the raw query, keeping the escaped path when the decoded one is not valid UTF-8 without NUL (P33-61); `Fetcher.Absolute` escapes the path part with `(&url.URL{Path: p}).EscapedPath()` and appends the query as is; compare.go `same` and linkgraph.go `sitePath` (both the `/…` and the absolute input, parsed with url.Parse) produce the same form. Nothing else changes: Lookup, Keyphrases and LinksOfRecord already work on decoded paths. Commit B.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./classes/crawl -run 'TestAllowRequest|TestFetcherPortAndKey|TestCompareRendered|TestResolverRules' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -10</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestFetcherPortAndKey" line</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./... -run 'TestCrawlerNonASCII|TestCrawlerTracer|TestCrawlerChecks|TestLinkSuggestions|TestChromeRenderer|TestRendererGuards' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -12</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestCrawlerNonASCII" line (TestChromeRenderer may skip only on a host without Chrome)</fails_when>
|
||||
<automated>go -C ../sm-seo-plugin vet ./... && go -C ../sm-seo-plugin test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "func effectivePort" ../sm-seo-plugin/classes/crawl/*.go` prints one line
|
||||
- `grep -n "func allowRequest" ../sm-seo-plugin/classes/crawl/render.go` prints one line whose parameters include the port
|
||||
- `grep -n "u.Path" ../sm-seo-plugin/classes/crawl/fetch.go` prints at least one line (Key builds the decoded form)
|
||||
</acceptance_criteria>
|
||||
<done>The crawler and headless Chrome stay on the site's port, and addresses with non-ASCII characters match their CMS records and link counts.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 4: Sitemap: IndexNow for deletes and edits, one rebuild lock, kept lastmods, the definitions migration, unpinned HTML headings (L2 WR-01 to WR-05, IN-01)</name>
|
||||
<files>../sm-sitemap-plugin/updates/202611040001_add_golem15_sitemap_urls_indexnow_pending.go, ../sm-sitemap-plugin/models/url.go, ../sm-sitemap-plugin/classes/lastmod.go, ../sm-sitemap-plugin/classes/rebuild.go, ../sm-sitemap-plugin/classes/indexnow.go, ../sm-sitemap-plugin/updates/202610280002_update_golem15_sitemap_settings.go, ../sm-sitemap-plugin/models/legacy.go, ../sm-sitemap-plugin/models/legacy_test.go, ../sm-sitemap-plugin/extensions_settings.go, ../sm-sitemap-plugin/classes/typesettings.go, ../sm-sitemap-plugin/controllers/api/settings.go, ../sm-sitemap-plugin/lang/en/lang.yaml, ../sm-sitemap-plugin/lang/pl/lang.yaml, ../sm-sitemap-plugin/README.md, ../sm-sitemap-plugin/content_fixture_test.go, ../sm-sitemap-plugin/filter_fixture_test.go, ../sm-sitemap-plugin/sitemap_extras_test.go, ../sm-sitemap-plugin/sitemap_index_test.go, ../sm-sitemap-plugin/classes/lastmod_test.go</files>
|
||||
<read_first>
|
||||
- ../sm-sitemap-plugin/classes/rebuild.go (Rebuild, collect, apply, rowChanged) and classes/lastmod.go (RecordSaved, RecordDeleted, RequestDebouncedRebuild), classes/indexnow.go (NotifyIndexNow)
|
||||
- ../sm-sitemap-plugin/sitemap_extras_test.go (TestIndexNow, TestHTMLGroups), sitemap_index_test.go (event firing at ~504-600, TestMigrateDefinitions, TestSettingsSingleton for the admin settings API), content_fixture_test.go (store.put/remove), filter_fixture_test.go (fakeFilter)
|
||||
- ../sm-sitemap-plugin/updates/202610280002_update_golem15_sitemap_settings.go (copyDefinitions) and models/legacy.go, models/url.go (AllowedAddress)
|
||||
- ../sm-sitemap-plugin/extensions_settings.go (typeSettingsTab Load), classes/typesettings.go (SaveTypeSettings), classes/htmlgroups.go (pickTitle, translateIn), controllers/api/settings.go (GET types)
|
||||
- 33-REVIEW.md lane 2 WR-01 to WR-05, IN-01
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestIndexNow (production fixture) through the fixed fixture: a record deleted with a fired pact.RecordDeleted and a content-only edit with a fired pact.RecordSaved (title change, same URL) lead the next rebuild to queue one IndexNow job holding the deleted address and the edited address; a following rebuild with nothing new queues nothing; outside production nothing is queued and the marks are cleared
|
||||
- TestConcurrentRebuilds (Docker): rebuild A blocks inside the fake Filter's Decide; record X is published; rebuild B starts and waits on the advisory lock (a not-granted advisory lock shows in pg_locks); A is released; both finish; X is listed and is not in any queued IndexNow removal
|
||||
- TestRebuildKeepsMovedLastmod (Docker): through a test seam called right after apply reads the slot rows, a RecordSaved moves the lastmod of a row the same rebuild also changes (title); after the rebuild the stored lastmod is the moved one
|
||||
- TestConvertLegacyItems (models, no Docker): o-nas becomes /o-nas; "/blog/zażółć gęślą" becomes "/blog/zażółć%20gęślą"; a duplicate is kept once without a dropped entry; https://site/x?a=1&b=2 is dropped with a reason naming the &; a malformed item is dropped with its parse error while the other items are kept; a non-url item is neither kept nor reported
|
||||
- TestMigrateDefinitions extended: a definition mixing those items migrates the convertible ones into extra_urls
|
||||
- TestTypeTitlesNotPinned (Docker): an English-UI admin loads Settings › Sitemaps and saves it back unchanged; no type title is stored; the HTML groups API answers the pl group title from the pl translation of the type label and the en one from en
|
||||
- TestRecordSavedNilEvent (classes): RecordSaved(ctx, app, nil) returns nil without a panic
|
||||
</behavior>
|
||||
<action>
|
||||
Per L2 WR-01 to WR-05, IN-01, D-30, D-31, D-32, D-34, P33-62, P33-63 and P33-66, tests first, four commit groups:
|
||||
- (A, WR-01, IN-01) Migration 202611040001 adds `indexnow_pending_at` (with Rollback); the model gains it. RecordSaved checks `ev == nil` before reading ev (IN-01) and, with the lastmod move, sets indexnow_pending_at on the record's rows; RecordDeleted sets indexnow_pending_at only on rows that were listed (excluded_reason '') in the same update that excludes them. `apply` turns pending rows into `Changed` (listed after this rebuild and not already Added or Changed) or `Removed` (no longer listed; handled before the "reason did not change" skip), and clears the mark per P33-62. Fixture fix: content_fixture_test.go gets helpers that delete or edit a record and fire `pact.RecordDeleted` or `pact.RecordSaved` through `e.app.Events.Fire`, as the admin does; TestIndexNow uses them. Commit A.
|
||||
- (B, WR-02, WR-03) Rebuild runs per P33-63: the advisory lock, then settings, types, `collect`, the settings row `FOR UPDATE` and `apply`, all in one transaction; the Rebuild doc comment says what is serialised. `apply` writes `lastmod` only for a created row or an additional address (`w.extra`), never for an existing content row (WR-03). Add a context seam (for example `classes.WithAfterSlotRead(ctx, fn)`, nil in production) called after apply reads the slot rows, and a hook on `fakeFilter` that can block Decide. Commit B.
|
||||
- (C, WR-04) `models.ConvertLegacyItems` and `DroppedItem` per P33-66 (parse the entries one by one with LegacyEntries and ParseLegacyItem); copyDefinitions uses it and logs each dropped item (`diary.For("sitemap")` Warn `golem15.sitemap: definition url item not copied` with item, url and reason), and a definition that is not a list logs one line. Editing the migration in place is safe: v0.2.0 is untagged and no production database has run 202610280002 (deploys follow the 33-14 tags). Local rehearsal databases that already ran the old 202610280002 (the 33-12 demo and 33-13 grzyby rehearsals) keep its old result: recreate them from their source dumps before the 33-14 rehearsal, and say so in the SUMMARY so 33-14 Task 4 does it. README upgrade paragraph states the conversion and the log line. Commit C.
|
||||
- (D, WR-05) typeSettingsTab Load answers only stored titles (an empty map for a type without one) and the field shows the type's label as its comment or placeholder (reuse the existing `type_title` comment if it fits; a new phrase goes into en and pl); `controllers/api/settings.go` GET answers stored titles only if it prefills too. Commit D.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-sitemap-plugin test ./... -run 'TestIndexNow|TestIndexNowStaging|TestConcurrentRebuilds|TestRebuildKeepsMovedLastmod|TestMigrateDefinitions|TestTypeTitlesNotPinned|TestHTMLGroups|TestLastmod|TestDebouncedRebuild|TestSitemapIndex|TestStableNumbering' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -20</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, "no tests to run", or any of TestIndexNow, TestMigrateDefinitions, TestConcurrentRebuilds, TestRebuildKeepsMovedLastmod, TestTypeTitlesNotPinned missing from the "--- PASS" lines</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-sitemap-plugin test ./classes ./models -run 'TestRecordSavedNilEvent|TestConvertLegacyItems|TestRealChanges' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -8</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or TestConvertLegacyItems or TestRecordSavedNilEvent missing from the "--- PASS" lines</fails_when>
|
||||
<automated>go -C ../sm-sitemap-plugin vet ./... && go -C ../sm-sitemap-plugin test -short ./...</automated>
|
||||
<fails_when>non-zero exit or a "FAIL" line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "pg_advisory_xact_lock" ../sm-sitemap-plugin/classes/rebuild.go` prints one line
|
||||
- `grep -n "indexnow_pending_at" ../sm-sitemap-plugin/updates/202611040001_add_golem15_sitemap_urls_indexnow_pending.go ../sm-sitemap-plugin/classes/lastmod.go ../sm-sitemap-plugin/classes/rebuild.go` prints a line in each
|
||||
- `grep -n "definition url item not copied" ../sm-sitemap-plugin/updates/202610280002_update_golem15_sitemap_settings.go` prints one line
|
||||
- `grep -n "func ConvertLegacyItems" ../sm-sitemap-plugin/models/legacy.go` prints one line
|
||||
- `grep -n "RecordDeleted\|RecordSaved" ../sm-sitemap-plugin/sitemap_extras_test.go` prints at least two lines (the fixture fires the events in TestIndexNow)
|
||||
</acceptance_criteria>
|
||||
<done>IndexNow hears of every removal and content edit, concurrent rebuilds and record saves never leave stale state, migrated url items are kept or reported, and HTML sitemap headings stay per language.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 5: Journal content fixes: tall images, markdown left alone, unfinished tags, fix-headings in the CLI, alt texts on markdown (L1 CR-01, CR-02, WR-03, WR-01, WR-05)</name>
|
||||
<files>../sm-journal-plugin/classes/image_variants.go, ../sm-journal-plugin/classes/headings.go, ../sm-journal-plugin/classes/image_alt.go, ../sm-journal-plugin/console/fix_headings.go, ../sm-journal-plugin/controllers/api/maintenance.go, ../sm-journal-plugin/mcp.go, ../sm-journal-plugin/README.md, ../sm-journal-plugin/classes/headings_test.go, ../sm-journal-plugin/classes/image_alt_test.go, ../sm-journal-plugin/headings_test.go, ../sm-journal-plugin/related_media_test.go, ../sm-journal-plugin/image_variants_cli_test.go</files>
|
||||
<read_first>
|
||||
- ../sm-journal-plugin/classes/image_variants.go (GenerateImageVariants) and related_media_test.go (TestImageVariants, jpegOf)
|
||||
- ../sm-journal-plugin/classes/headings.go, image_alt.go, blog_editor.go (LooksLikeHTML), format_html.go (markdownEngine), heading_fix.go (fixPost), console/import_posts.go (ConvertContentH1 call sites), controllers/api/posts_helpers.go (h1Conversion)
|
||||
- ../sm-journal-plugin/classes/headings_test.go and image_alt_test.go (TestConvertH1, TestConvertContentH1, TestConvertTranslationH1, TestSetImageAlt, TestSetImageAltEdges: the gate requires these names)
|
||||
- ../sm-journal-plugin/console/fix_headings.go, console/database.go (lookupDB) and image_variants_cli_test.go (TestImageVariantsCommandCLI)
|
||||
- ../sm-journal-plugin/controllers/api/maintenance.go (SetImageAlt, loadImageAltText, writeImageAltTexts) and headings_test.go (TestHeadingConversion, TestFixHeadingsCommand, TestSetImageAlt)
|
||||
- 33-REVIEW.md lane 1 CR-01, CR-02, WR-01, WR-03, WR-05
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestImageVariantsTallOriginal (Docker): one job with a 1000×9000 original and then a 900×600 one returns no error; the tall one gets no variant above 4096 px and is counted skipped; the normal one gets its variants; journal:image-variants --all over both finishes with exit code 0
|
||||
- TestConvertContentH1 gains: markdown with a fenced html sample holding h1 tags and an inline code h1 comes back byte for byte, its content_html (from FormatHTML) has the real heading converted and the code still escaped, and the count is 1; HTML content converts as before; TestConvertTranslationH1 gains the same markdown case
|
||||
- TestConvertH1 and TestSetImageAltEdges gain: input without a match (for example "p a <b" cut inside a tag, "x<y") comes back identical; an h1 followed by an unfinished tag keeps the tail
|
||||
- TestHeadingConversion, TestFixHeadingsCommand (Docker) gain a markdown post with a fenced h1 sample: an API save, an import and journal:fix-headings --apply keep its content byte for byte and convert its content_html
|
||||
- TestFixHeadingsCommandCLI (Docker): in the generated CLI without a published database the command names SUMMER_DATABASE__DSN without a DSN and prints the dry-run line with one
|
||||
- TestSetMarkdownImageAlt (classes):  and  get the alt; a destination written unescaped matches its goldmark-escaped src; an alt with ] and a line break is escaped and folded; the same image inside a fence or a code span is left alone; a reference-style image is not matched
|
||||
- TestSetImageAlt (Docker) gains: on a markdown post the alt lands in content and content_html and survives a following PUT that saves the same content; a markdown post whose image is reference-style answers 422 changes.0.src and writes nothing; an HTML post behaves as before
|
||||
</behavior>
|
||||
<action>
|
||||
Per L1 CR-01, CR-02, WR-01, WR-03, WR-05, D-50, D-85, D-10 and P33-65, tests first; core plugin rules: no exported identifier removed or renamed, no exported signature changed (new functions only):
|
||||
- (A, CR-01) In GenerateImageVariants, a width whose computed height exceeds MaxImageWidth (4096, the thumbnailer's edge limit) is skipped with `variantsLog.Warn` (file, width, height) and `out.Skipped++` before ThumbKey runs, so neither ThumbKey nor webp.Variant can fail the batch on it; storage and database errors still return (the job retries). Commit A.
|
||||
- (B, CR-02, WR-03) `ConvertContentH1` converts `content` only when `LooksLikeHTML(content)`; content_html is always converted; the count is content_html's, or content's when content_html has none and content is HTML; doc comments of ConvertContentH1 and ConvertTranslationH1 say markdown is never rewritten (every render path re-derives and re-converts content_html). In ConvertH1 and SetImageAlt, the loop tracks the bytes consumed and on the tokenizer's EOF appends the unconsumed rest of the input before returning. Update the tests that pinned converting a markdown source. Commit B.
|
||||
- (C, WR-01) journal:fix-headings opens the database with `lookupDB` (defer done) as journal:image-variants does; README command list notes it runs from the application CLI. Commit C.
|
||||
- (D, WR-05) `SetMarkdownImageAlt` per P33-65 in image_alt.go, plus a helper that runs a rewrite only on the text outside goldmark code ranges. In the images/alt handler, for each locale's text: HTML content (LooksLikeHTML) keeps today's behaviour; markdown content gets SetMarkdownImageAlt and SetImageAlt outside code; when neither matched while content_html did, the change is refused with 422 `changes.N.src` "The image is not an inline image of the post's markdown; edit the post." and nothing is written (the batch stays all or nothing). The images/alt API and the MCP tool are new in the unreleased v0.4.0, so the refusal is no break. mcp.go set_image_alt description and the README API section state the markdown rule. Commit D.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-journal-plugin test ./classes -run 'TestConvertH1|TestConvertContentH1|TestConvertTranslationH1|TestSetImageAlt|TestSetImageAltEdges|TestSetMarkdownImageAlt' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -10</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestSetMarkdownImageAlt" line</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-journal-plugin test ./... -run 'TestImageVariants|TestImageVariantsTallOriginal|TestImageVariantsCommandCLI|TestFixHeadingsCommand|TestFixHeadingsCommandCLI|TestHeadingConversion|TestHeadingConversionTracer|TestSetImageAlt|TestJournalSettingsTools' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -16</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or TestImageVariantsTallOriginal or TestFixHeadingsCommandCLI missing from the "--- PASS" lines</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -short ./... && npm --prefix ../sm-journal-plugin run test 2>&1 | tail -6</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, or a failed count above 0 in the vitest summary</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "lookupDB" ../sm-journal-plugin/console/fix_headings.go` prints one line
|
||||
- `grep -n "LooksLikeHTML" ../sm-journal-plugin/classes/headings.go` prints at least one line
|
||||
- `grep -n "func SetMarkdownImageAlt" ../sm-journal-plugin/classes/image_alt.go` prints one line
|
||||
- `grep -n "MaxImageWidth" ../sm-journal-plugin/classes/image_variants.go` prints at least two lines (the bound and the new height check)
|
||||
- with BASE the journal HEAD recorded before Task 5, `git -C ../sm-journal-plugin diff BASE..HEAD -U0 -- '*.go' ':!*_test.go' | grep -E '^-func [A-Z]|^-func \([^)]*\) [A-Z]'` prints nothing (no exported function removed or its signature changed)
|
||||
</acceptance_criteria>
|
||||
<done>Tall images, markdown code samples, cut-off tags and markdown alt texts are handled without corrupting content or stopping jobs, and journal:fix-headings runs from the application CLI.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 6: Journal descriptors list only locales with content (G3), Journal-shaped hreflang fixtures, the WebP decoder docs line, release notes and the record of G2</name>
|
||||
<files>../sm-journal-plugin/classes/content_descriptors.go, ../sm-journal-plugin/classes/menu_item_types.go, ../sm-journal-plugin/content_descriptors_test.go, ../sm-journal-plugin/README.md, ../sm-seo-plugin/classes/alternates_test.go, ../sm-sitemap-plugin/sitemap_index_test.go, modules/lagoon/README.md, docs/database/attachments.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-journal-plugin-v0.4.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-seo-plugin-v0.2.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-sitemap-plugin-v0.2.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-summercms-v0.6.1.md, .planning/phases/33-seo-output-audit-and-content/33-release-log.md, .planning/phases/33-seo-output-audit-and-content/deferred-items.md</files>
|
||||
<read_first>
|
||||
- ../sm-journal-plugin/classes/content_descriptors.go (describePosts, describeCategories, describeTags) and classes/menu_item_types.go (describeAuthors, menuAlternates, loadMenuLocales, menuTranslations, phpTruthy, publishedPosts)
|
||||
- ../sm-journal-plugin/content_descriptors_test.go (bootMenuItemTypes with translate, TestContentDescriptors, TestJournalTagType) and menu_item_types_smoke_test.go
|
||||
- ../sm-seo-plugin/classes/alternates_test.go (TestAlternates) and ../sm-sitemap-plugin/sitemap_index_test.go (TestHreflangAlternates)
|
||||
- ../summercms.go/modules/signpost/signpost.go (lines 80-90: the DefaultPattern rule, the G2 reason)
|
||||
- modules/lagoon/README.md (attach/webp lines 9, 15, 45) and docs/database/attachments.md (the WebP variants section)
|
||||
- the four 33-14-notes-sm-*/summercms drafts, 33-release-log.md ("Planned contents"), deferred-items.md, 33-VERIFICATION.md (G1, G2, G3, info rows)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- TestContentDescriptorsTranslatedLocales (Docker, translate with pl default, en and de enabled): a post with an en translation row (title, content) and none in de describes pl and en with URLs and de with URL "" and the pl title; a post without translations has only the pl URL; a category with a translated en name gets the en URL, one without gets ""; a tag whose only published post is translated into en gets the en URL and "" for de; an author likewise; the journal-post and journal-category menu Resolve of the same records still lists all three locales with URLs
|
||||
- TestAlternates (sm-seo-plugin) runs on a Journal-shaped record (pl default, en translated, de enabled and untranslated with URL "" and the pl title, description and word count) and answers pl, en and x-default only
|
||||
- TestHreflangAlternates (sm-sitemap-plugin) uses the same shape for its de entry and still writes no hreflang="de"
|
||||
</behavior>
|
||||
<action>
|
||||
Per G3, D-55, D-57, SC-2 and P33-64, then the docs and planning records:
|
||||
- (A, journal, G3) A helper decides per record and locale whether it has content (P33-64): posts and categories through the menuTranslations already loaded (a new method, for example `has(locale, id, fields...)`, using phpTruthy); tags and authors through one query per describe batch that loads their published posts (the posts-tags pivot `golem15_journal_posts_tags`, posts by user_id, `publishedPosts`) and those posts' translations (loadMenuTranslations). describePosts, describeCategories, describeTags and describeAuthors keep one entry per alternate and set URL "" for a locale without content; menuAlternates, the Resolve functions and the Match functions stay unchanged. README: the "Every resolved record carries alternates…" paragraph gains the descriptor rule. Commit A.
|
||||
- (B, seo) TestAlternates gets a `journalShaped` fixture per the behaviour above. Commit B.
|
||||
- (C, sitemap) TestHreflangAlternates gets the same shape for its untranslated entry. Commit C.
|
||||
- (D, framework, L1 WR-02 docs only) modules/lagoon/README.md (the attach/webp feature line) and docs/database/attachments.md (the encoder paragraph): importing attach/webp also registers the encoder library's WebP decoder ahead of the pure Go one, so decoding WebP uploads (thumbnails, size checks) goes through the system libwebp when it is installed; keep libwebp patched, or build with -tags nodynamic so decoding uses the transpiled copy. Use no new backticked identifiers outside lagoon and no SEO, sitemap or application words (marker stage). `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check` pass. Commit D (docs only; no Go file changes).
|
||||
- (E, planning docs) Release-note drafts: sm-seo-plugin v0.2.0 (route report noindex paths and the filter, crawl cancel and timeout behaviour with the stale recovery, port and non-ASCII crawl fixes), sm-sitemap-plugin v0.2.0 (contract.Rebuilder; IndexNow of removed and edited addresses now true; the corrected migration wording per P33-66 with the log line; the upgrade line about noindex Additional addresses: left out automatically when the frontend reports them, otherwise remove them), sm-journal-plugin v0.4.0 (descriptors list only locales with content, a behaviour change for hreflang and sitemaps, with the rule stated explicitly: posts and categories by their own translation in the locale, tags and authors when at least one of their published posts is translated into it, the default locale always; markdown never rewritten by the H1 conversion; alt texts on markdown posts; tall images; fix-headings from the CLI under Fixed), summercms v0.6.1 (the WebP decoder note under Changed). 33-release-log.md: add the docs commit of D under "Framework commits of later Phase 33 plans". deferred-items.md: G3's 33-13 entry and the 33-12 "noindex additional address" entry resolved (CMS side here, layer side 33-16), the "journal-tag pattern" entry deferred to v0.6.2 with the G2 reason (signpost.Activate refuses an empty DefaultPattern; dropping the descriptor would remove the tag form's SEO tab and the sitemap types row), the fix-headings note of "journal:image-variants cannot run" resolved, and the stale "Saving Settings › Journal answers 500" entry resolved (d2ce3a2); a new "From 33-15" section lists the v0.6.2 items of the Deferred section below. Commit E in summercms.go as planning docs.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; go -C ../sm-journal-plugin test ./... -run 'TestContentDescriptorsTranslatedLocales|TestContentDescriptors|TestContentDescriptorsMatrix|TestJournalTagType|TestJournalDescriptorFields|TestMenuItemTypesSmoke' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' | tail -12</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or no "--- PASS: TestContentDescriptorsTranslatedLocales" line</fails_when>
|
||||
<automated>set -o pipefail; go -C ../sm-seo-plugin test ./classes -run 'TestAlternates' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)' && go -C ../sm-sitemap-plugin test ./... -run 'TestHreflangAlternates' -count=1 -v 2>&1 | grep -E '^(--- |FAIL|ok)'</automated>
|
||||
<fails_when>a "FAIL" line, "no tests to run", or TestAlternates or TestHreflangAlternates missing from the "--- PASS" lines</fails_when>
|
||||
<automated>set -o pipefail; go test ./cmd/summer -run TestDocsTree -count=1 && sha=$(git log --format=%H -n1 --grep='^docs(lagoon): attach/webp also decodes WebP$') && test -n "$sha" && files=$(git show --name-only --format= "$sha") && printf '%s\n' "$files" | awk '/\.go$/ {n++} END {print "go-files=" n+0}'</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" line, no "go-files=" line (the docs commit is missing), or a value other than go-files=0 (the framework commit of this plan changes a Go file)</fails_when>
|
||||
<automated>set -o pipefail; PHASE33_REQUIRE_DOCKER=1 bash scripts/check-phase33.sh --self-test --go --plugins --docker --admin --docs --marker --additive --compat 2>&1 | tail -20</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" or "refuse:" line, or a missing "PASS <stage>" line for any of self-test, go, plugins, docker, admin, docs, marker, additive and compat (this plan never runs the layers or consumers stage; the orchestrator runs --all after both SUMMARYs)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "has content" ../sm-journal-plugin/README.md` prints a line stating the descriptor rule (a locale gets an address only when the record has content in it)
|
||||
- `grep -n "nodynamic" modules/lagoon/README.md docs/database/attachments.md` prints a line in each with the decoding note
|
||||
- `grep -n "not copied" .planning/phases/33-seo-output-audit-and-content/33-14-notes-sm-sitemap-plugin-v0.2.0.md` prints a line
|
||||
- deferred-items.md has a "From 33-15" section and the journal-tag entry names signpost's DefaultPattern rule
|
||||
- `git -C ../sm-seo-plugin status --short`, `git -C ../sm-sitemap-plugin status --short` and `git -C ../sm-journal-plugin status --short` print no tracked change left uncommitted by this plan
|
||||
</acceptance_criteria>
|
||||
<done>Untranslated locales leave hreflang and sitemaps for Journal records on the real descriptor path, the fixtures prove it with Journal-shaped records, and the release notes, release log and deferred items match the code.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| frontend server → route report | token-authenticated POST whose noindex list now changes what the sitemaps list |
|
||||
| site pages (untrusted HTML and JS) → headless Chrome | page code asks the browser to load URLs |
|
||||
| crawl job ↔ database | final writes after a cancel or timeout, stale rows |
|
||||
| admin/MCP → images/alt | untrusted alt text written into markdown source |
|
||||
| Phase 17 definition data → migration | admin-entered legacy URLs copied and logged |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-33-71 | Elevation of privilege (SSRF) | Chrome request guard and Fetcher (L3 WR-03) | high | mitigate | allowRequest and Fetcher.Allowed compare the effective port with the site's, so page code can no longer reach other TLS services on the site host's address; TestAllowRequest and TestFetcherPortAndKey pin it (Task 3) |
|
||||
| T-33-72 | Tampering | route report noindex paths | medium | mitigate | constant-time token as for routes; each entry validated (ValidPagePath, 2048 bytes) and capped at 2000; the set only feeds the sitemap filter (robots, canonical and the resolve answer are untouched); the README recommends a dedicated route_report_token over the 404 token fallback (Task 1) |
|
||||
| T-33-73 | Denial of service | stuck "running" crawls | medium | mitigate | detached bounded finish, heartbeat every minute, RecoverStale before Start and DeleteCrawls, cancel owned by the job (Task 2) |
|
||||
| T-33-74 | Tampering (content injection) | SetMarkdownImageAlt | medium | mitigate | only the matched label is replaced; `\`, `[` and `]` are escaped and line breaks folded, so an alt cannot close the image or add a link; code regions are never touched (Task 5) |
|
||||
| T-33-75 | Information disclosure | migration warn log of dropped url items | low | accept | the values are admin-entered site addresses written to the server log once at migration time |
|
||||
| T-33-76 | Tampering (integrity) | concurrent sitemap rebuilds | medium | mitigate | one transaction-scoped advisory lock around collect and apply; lastmod of existing content rows never rewritten (Task 4) |
|
||||
| T-33-SC | Tampering | npm/pip/cargo/Go installs | high | mitigate | no dependency is added in any repository (goldmark, conga and River are already required); go.mod, go.sum and package.json of the four repositories do not change, and the SUMMARY shows `git diff BASE..HEAD -- go.mod go.sum package.json` empty per repository |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- sm-seo-plugin: vet, `test -short`, the Docker suites TestNoindexPathsTracer, TestRouteReport, TestCrawlCancelRunning, TestCrawlTimeout, TestCrawlStaleRecovery, TestCrawlerNonASCII and the existing crawler and filter suites; package crawl tests TestRenderTabTimeout, TestFetcherPortAndKey, TestAllowRequest, TestSitemapNoindexTool; TestAlternates.
|
||||
- sm-sitemap-plugin: vet, `test -short`, TestIndexNow, TestConcurrentRebuilds, TestRebuildKeepsMovedLastmod, TestMigrateDefinitions, TestTypeTitlesNotPinned, TestHreflangAlternates, TestConvertLegacyItems, TestRecordSavedNilEvent.
|
||||
- sm-journal-plugin: vet, `test -short`, vitest, TestImageVariantsTallOriginal, TestFixHeadingsCommandCLI, TestSetMarkdownImageAlt, TestContentDescriptorsTranslatedLocales and the existing heading, alt and descriptor suites.
|
||||
- summercms.go: TestDocsTree, no Go file changed; finally `PHASE33_REQUIRE_DOCKER=1 bash scripts/check-phase33.sh --self-test --go --plugins --docker --admin --docs --marker --additive --compat` (never layers or consumers). The full `--all` gate is run by the orchestrator once, after both 33-15 and 33-16 SUMMARYs exist and before 33-14 Task 4.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- G1 is closed on the CMS side and G3 is closed; G2 is recorded as not fixable without the framework.
|
||||
- Every in-scope L1, L2 and L3 finding has a fix and a test that fails before it.
|
||||
- No repository is pushed or tagged; the release resumes in 33-14 Task 4 after 33-16, followed by re-verification.
|
||||
</success_criteria>
|
||||
|
||||
## Deferred (v0.6.2 backlog item, grouped by repository)
|
||||
|
||||
- **sm-sitemap-plugin:** L2 WR-06 (default slug collisions), WR-07 (no 50 MB byte guard), WR-08 (Additional addresses on other hosts); info IN-02 to IN-11.
|
||||
- **sm-seo-plugin:** L3 WR-02 (crawl limits beyond the job timeout and memory), WR-05 (recheck fetches inside a write transaction); info IN-01 to IN-07; G2 (journal-tag pattern while tag pages are off: needs an additive pact field such as ContentDescriptor.Routable, a framework change, or a signpost rule for an empty DefaultPattern).
|
||||
- **sm-journal-plugin:** L1 WR-04 (images/alt locale keying), WR-06 (author pages publish admin logins), WR-07 (settings API string/url workaround); info IN-01 to IN-04, IN-08; an untranslated locale's address still answers with the default-locale content and a self canonical (no longer advertised in hreflang or sitemaps); a canonical to the default locale or a 404 is a separate decision.
|
||||
- **summercms.go:** L1 WR-02 code change (decode WebP in attach with the pure Go decoder explicitly, or a decision note); info IN-05, IN-06, IN-07, IN-09; verifier warnings W1 (home title template once / is registered), W2 (swr localhost canonicals), W3 (route report names without diacritics), W4 (GORM local timestamps), W5 (min/max length change in a patch release, a release decision).
|
||||
- **Nuxt layers (planned in 33-16, deferred there):** L4 WR-06, WR-07 and the L4 info items.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- sm-seo-plugin: table `golem15_seo_noindex_paths` (migration 202611040001), model `models.NoindexPath`, `(*classes.Service).ReportNoindex`, route report key `noindex` (request and answer), filter reason source; crawl columns `heartbeat_at`, `cancel_requested_at` (migration 202611040002), `crawl.JobTimeout`, `crawl.FinishTimeout`, `crawl.HeartbeatEvery`, `crawl.StaleAfter`, `crawl.RecoverStale`, `crawl.TabOpenTimeout`, `crawl.ToolSitemapSettings`, `Env.SitemapTool`, API and MCP key `cancel_requested`, phrase `crawls.st_cancelling`; `allowRequest` port parameter.
|
||||
- sm-sitemap-plugin: `contract.Rebuilder` (published by the plugin), column `indexnow_pending_at` (migration 202611040001), `models.ConvertLegacyItems`, `models.DroppedItem`, the rebuild advisory lock, the `WithAfterSlotRead` test seam.
|
||||
- sm-journal-plugin: `classes.SetMarkdownImageAlt`; descriptor rule for locales with content; images/alt 422 for markdown images it cannot rewrite.
|
||||
- summercms.go: docs lines in modules/lagoon/README.md and docs/database/attachments.md; updated release-note drafts, release log and deferred items.
|
||||
- Tests: TestNoindexPathsTracer, TestSitemapNoindexTool, TestCrawlCancelRunning, TestCrawlTimeout, TestCrawlStaleRecovery, TestRenderTabTimeout, TestFetcherPortAndKey, TestCrawlerNonASCII, TestConcurrentRebuilds, TestRebuildKeepsMovedLastmod, TestConvertLegacyItems, TestTypeTitlesNotPinned, TestRecordSavedNilEvent, TestImageVariantsTallOriginal, TestFixHeadingsCommandCLI, TestSetMarkdownImageAlt, TestContentDescriptorsTranslatedLocales; extended TestRouteReport, TestIndexNow, TestMigrateDefinitions, TestConvertH1, TestConvertContentH1, TestConvertTranslationH1, TestSetImageAltEdges, TestHeadingConversion, TestFixHeadingsCommand, TestSetImageAlt, TestAlternates, TestHreflangAlternates, TestAllowRequest.
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/33-seo-output-audit-and-content/33-15-SUMMARY.md` when done: the commits per repository, the finding-to-test table, the number of framework Go files changed since v0.6.0 before and after (unchanged), the G2 record, the gate stages' result, and, if the context ran low, the remaining tasks with their resume points.
|
||||
</output>
|
||||
405
.planning/phases/33-seo-output-audit-and-content/33-16-PLAN.md
Normal file
405
.planning/phases/33-seo-output-audit-and-content/33-16-PLAN.md
Normal file
@@ -0,0 +1,405 @@
|
||||
---
|
||||
phase: 33-seo-output-audit-and-content
|
||||
plan: 16
|
||||
type: execute
|
||||
wave: 10
|
||||
depends_on: ["33-13"]
|
||||
gap_closure: true
|
||||
files_modified:
|
||||
- ../vue-seo-layer/lib/routes.ts
|
||||
- ../vue-seo-layer/lib/seo-module.ts
|
||||
- ../vue-seo-layer/lib/options.ts
|
||||
- ../vue-seo-layer/lib/sitemapPages.ts
|
||||
- ../vue-seo-layer/server/seo-routes.d.ts
|
||||
- ../vue-seo-layer/server/lib/routeReport.ts
|
||||
- ../vue-seo-layer/server/lib/site.ts
|
||||
- ../vue-seo-layer/server/middleware/seo-root.ts
|
||||
- ../vue-seo-layer/server/plugins/seo-route-report.ts
|
||||
- ../vue-seo-layer/app/components/global/SeoHtmlSitemapPage.vue
|
||||
- ../vue-seo-layer/app/components/Seo/SitemapLink.vue
|
||||
- ../vue-seo-layer/app/utils/pageHref.ts
|
||||
- ../vue-seo-layer/.playground/nuxt.config.ts
|
||||
- ../vue-seo-layer/testing/smoke-ssr.mjs
|
||||
- ../vue-seo-layer/testing/expect-seo.txt
|
||||
- ../vue-seo-layer/testing/expect-seo-down.txt
|
||||
- ../vue-seo-layer/tests/routes.test.ts
|
||||
- ../vue-seo-layer/tests/sitemapPages.test.ts
|
||||
- ../vue-seo-layer/tests/pageHref.test.ts
|
||||
- ../vue-seo-layer/tests/rootFiles.test.ts
|
||||
- ../vue-seo-layer/package.json
|
||||
- ../vue-seo-layer/README.md
|
||||
- ../vue-journal-layer/.playground/nuxt.config.ts
|
||||
- ../vue-journal-layer/app/journal-pages/PostPage.vue
|
||||
- ../vue-journal-layer/app/utils/seo.ts
|
||||
- ../vue-journal-layer/app/utils/routes.ts
|
||||
- ../vue-journal-layer/lib/journal-module.ts
|
||||
- ../vue-journal-layer/tests/seo.test.ts
|
||||
- ../vue-journal-layer/tests/routes.test.ts
|
||||
- ../vue-journal-layer/testing/expect-acme.txt
|
||||
- ../vue-journal-layer/scripts/check-git-install.sh
|
||||
- ../vue-journal-layer/package.json
|
||||
- ../vue-journal-layer/README.md
|
||||
- ../vue-menu-layer/package.json
|
||||
- ../vue-menu-layer/README.md
|
||||
- ../vue-faq-layer/package.json
|
||||
- ../vue-faq-layer/README.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-seo-layer-v0.2.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-journal-layer-v0.8.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-menu-layer-v0.5.0.md
|
||||
- .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-faq-layer-v0.5.0.md
|
||||
autonomous: true
|
||||
requirements: [SC-1, SC-2, D-08, D-22, D-23, D-24, D-33, D-34, D-40, D-42, D-97]
|
||||
|
||||
estimate:
|
||||
tokens: 150000
|
||||
raw_tokens: 150000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "G1 layer side (D-23, D-24, D-33, SC-1): the route report body carries noindex, the static paths the build serves noindex (requiresAuth, a noindex or none seo.robots, a static page under seo.noindexRoutes), including the @nuxtjs/i18n locale-prefixed copies, never patterns and never the layer's sitemap pages, within 2000 entries of at most 2048 bytes, and the whole JSON body stays at or under 4,113,024 bytes (the older plugin's limit), dropping noindex entries first, exactly as the gap-2 contract below; it is always sent (also []), with the same token and request"
|
||||
- "L4 WR-01 (D-34, D-42): a path-form group page 1 of the HTML sitemap (/mapa-strony/<group>/strona/1) answers one 301 to the unnumbered group page, using the locale's segment; the page never redirects to its own URL"
|
||||
- "L4 WR-04 (D-34): when an app page renders the sitemap path through a catch-all (a root /** page, or a prefixed one covering the path), the layer registers no sitemap pages for that locale and logs it at build, unless seo.htmlSitemap.overCatchAll is true; the switch and the upgrade note are documented in the vue-seo-layer README and the journal, menu and FAQ upgrade notes"
|
||||
- "L4 WR-05 (D-34): with @nuxtjs/i18n the HTML sitemap pages exist once per locale, at the configured path, named for that locale (no copy under another locale's prefix or name); SitemapLink goes to the visitor's locale route and renders only when that path is the layer's sitemap page"
|
||||
- "L4 WR-08 (D-08, D-22): while the backend has not answered since the server started, /robots.txt, /sitemap.xml and /llms.txt answer 503 with Retry-After: 60 instead of falling through to the app; a backend that answers 404 for the site endpoint (older than SEO v0.2.0) still passes through; a staging site therefore never exposes an allow-all robots.txt"
|
||||
- "L4 WR-02 (D-97, SC-2): a journal post page emits exactly one og:type article and article:published_time and article:modified_time from the post data whether or not the CMS answers; the CMS-owned keys still come from the CMS when it answers"
|
||||
- "L4 WR-03 (D-40, D-42): the journal registers its path-form listing routes from the SEO module's resolved pagination option (defaults included), so with seo.pagination.form path alone the Polish listings get /<listing>/strona/:page routes"
|
||||
- "L4 CR-01 (release guard): vue-journal-layer/scripts/check-git-install.sh --check-links <layer dirs> fails while any given layer's package.json has a link:, file:, workspace: or portal: dependency, the tag mode fails when the installed tag still has one, and --self-test covers both; the swap itself stays in 33-14 Task 4"
|
||||
- "L4 CR-01: the version fields read vue-seo-layer 0.2.0, vue-journal-layer 0.8.0, vue-menu-layer 0.5.0 and vue-faq-layer 0.5.0, and each README's install line pins that tag"
|
||||
- "The 33-14-notes-vue-*.md drafts describe the changed behaviour (route report noindex, root files 503, sitemap pages per locale and over a catch-all, journal pagination routes)"
|
||||
prohibitions:
|
||||
- requirement_id: "D-97"
|
||||
category: compatibility
|
||||
status: resolved
|
||||
verification: test
|
||||
statement: "The route report stays compatible both ways: an SEO plugin built before the gap-2 contract still answers 200 to the new body (the fixture API ignores noindex like encoding/json), and the layer still works against a backend without the registry (404, not retried)"
|
||||
- requirement_id: "D-70"
|
||||
category: safety
|
||||
status: resolved
|
||||
verification: test
|
||||
statement: "No change weakens the build guard or the noindex headers: guard:check and the expect-seo.txt noindex lines pass unchanged"
|
||||
artifacts:
|
||||
- path: "../vue-seo-layer/lib/sitemapPages.ts"
|
||||
provides: "the pure builder of the HTML sitemap routes (per locale, i18n names, catch-all rule)"
|
||||
contains: "overCatchAll"
|
||||
- path: "../vue-seo-layer/lib/routes.ts"
|
||||
provides: "noindexStaticRoutes for the route report"
|
||||
contains: "noindexStaticRoutes"
|
||||
- path: "../vue-journal-layer/scripts/check-git-install.sh"
|
||||
provides: "the --check-links release guard"
|
||||
contains: "--check-links"
|
||||
key_links:
|
||||
- from: "../vue-seo-layer/server/plugins/seo-route-report.ts"
|
||||
to: "#seo-routes noindexPaths"
|
||||
via: "routeReportBody(staticRoutes, noindexPaths) posts {routes, noindex}"
|
||||
pattern: "noindexPaths"
|
||||
- from: "../vue-seo-layer/lib/seo-module.ts"
|
||||
to: "../vue-seo-layer/lib/sitemapPages.ts"
|
||||
via: "a pages:resolved hook registered at modules:done, after @nuxtjs/i18n has localised the routes"
|
||||
pattern: "modules:done"
|
||||
- from: "../vue-journal-layer/lib/journal-module.ts"
|
||||
to: "runtimeConfig.public.seo.pagination"
|
||||
via: "journalPaginationSource inside the extendPages callback"
|
||||
pattern: "journalPaginationSource"
|
||||
---
|
||||
|
||||
<!-- planner-discipline-allow: seo -->
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** site editor and as an SEO specialist auditing a SummerCMS site, **I want to** have the rendered sites emit complete SEO output (sitemaps, structured data, pagination, hreflang, headings, breadcrumbs) from the CMS, and an audit that finds the remaining problems, **so that** sites are indexed correctly and issues can be fixed through MCP.
|
||||
|
||||
This gap-closure plan fixes the Nuxt-layer side of the gaps and review findings that block the 33-14 release: G1 (layer side) and the in-scope findings of review lane L4. Plan 33-15 fixes the Go plugins in parallel (same wave, no shared files; the gap-2 contract below is fixed in both plans). 33-14 Task 4 (the release, including the `link:../vue-seo-layer` swap) resumes after both, followed by re-verification.
|
||||
|
||||
<objective>
|
||||
Fix, each with a test, in the four Nuxt layers (each main, not pushed, not tagged):
|
||||
- vue-seo-layer: the route report's noindex paths (G1, D-23, D-24, D-33); the HTML sitemap pages: the page-1 redirect, catch-all apps, i18n copies, the footer link (L4 WR-01, WR-04, WR-05, D-34); root files while the backend is down (L4 WR-08, D-08, D-22);
|
||||
- vue-journal-layer: article meta on post pages (L4 WR-02, D-97) and path-form pagination routes from the SEO module's resolved option (L4 WR-03, D-40, D-42);
|
||||
- all four layers: the release guard against link: dependencies, the version fields and the README install pins (L4 CR-01), and the catch-all upgrade notes (L4 WR-04);
|
||||
- summercms.go: the four vue release-note drafts (planning docs).
|
||||
|
||||
Purpose: close G1 on the layer side and the release-blocking L4 findings before the tags.
|
||||
|
||||
Output: commits in vue-seo-layer, vue-journal-layer, vue-menu-layer, vue-faq-layer, and a planning-docs commit in summercms.go.
|
||||
|
||||
**Planner decisions recorded here (P33-67 to P33-72; 33-15 used P33-58 to P33-66):**
|
||||
- **P33-67 (noindex paths in the build artefact):** `noindexStaticRoutes(pages, noindexRoutes)` in lib/routes.ts takes the same flattened page list as `staticRoutes` and keeps the paths without `:` or `*` that are noindex by meta (`pageMetaRobots` with `robotsNoindex`) or under `seo.noindexRoutes` (`isNoindex`), client-only pages included, the layer's `seo-sitemap-` pages excluded, i18n copies kept (no `localeCopySuffixes` filter), sorted. `#seo-routes` exports it as `noindexPaths`; `routeReportBody(paths, noindex)` answers `{routes, noindex}` with the same limits for both lists; the report always sends `noindex`, so the plugin's set follows each build.
|
||||
- **P33-68 (sitemap page registration):** a pure builder `seoSitemapPages(input)` in lib/sitemapPages.ts answers the routes to add and the skipped locales with a reason (`taken`, `catch-all`). The module calls it from a `pages:resolved` hook that it registers in `modules:done`, so it runs after @nuxtjs/i18n has localised the app's routes in either `scanPageMeta` mode, and it first removes routes it added before (dev regeneration). Per locale of `htmlSitemap.paths`: the overview at the path, the group page at `<path>/:group` and, in the path form, `<path>/:group/<segment>/:page(\d+)` with the locale's segment; with i18n active (strategy other than `no_prefix`) each name gets `<routesNameSeparator, default ___><locale>` so i18n reads the right locale; a final page at the same path is `taken`; a catch-all page whose pattern covers the path (`patternCovers` from lib/guard.ts) makes it `catch-all` unless `htmlSitemap.overCatchAll` is true. Skips are logged at build with the reason and the switch.
|
||||
- **P33-69 (first page of a group):** `seoFirstPageRedirect(fullPath, form, segment, param): string | null` in app/utils/pageHref.ts answers the unnumbered target, or null when the target equals `fullPath`; SeoHtmlSitemapPage passes the locale's segment from the site answer's `pagination.segments`, else the build option's, else the param.
|
||||
- **P33-70 (root files while unknown):** `rootFileTargets` answers the backend target of the fixed names (robots.txt, sitemap.xml, llms.txt) also in state `unknown` without a last answer, so the fetch fails and the middleware's 503 with Retry-After applies; `absent` (an older backend) passes through as before; sitemap files and verification files keep needing a site answer.
|
||||
- **P33-71 (journal pagination source):** `journalPaginationSource(nuxtOptions)` in app/utils/routes.ts answers `runtimeConfig.public.seo.pagination` (written by the SEO module's setup with its defaults) when present, else the raw `seo.pagination`; the journal module calls it inside its `extendPages` callback, which runs after every module's setup. No import from vue-seo-layer (33-09: a git-installed layer's .ts under node_modules is not type-stripped by Node).
|
||||
- **P33-72 (release guard):** `scripts/check-git-install.sh --check-links DIR...` reads each `DIR/package.json` with node and prints `check-git-install: <dir>: <name> depends on <spec>` for every dependencies, devDependencies, peerDependencies or optionalDependencies entry whose spec starts with `link:`, `file:`, `workspace:` or `portal:`, exiting 1 when any is found; the tag mode runs the same check on the installed `node_modules/vue-journal-layer` before building; `--self-test` adds planted package.json cases.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-VERIFICATION.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-REVIEW.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/deferred-items.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-CONTEXT.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-08-SUMMARY.md
|
||||
@.planning/phases/33-seo-output-audit-and-content/33-09-SUMMARY.md
|
||||
@CLAUDE.md
|
||||
|
||||
<interfaces>
|
||||
vue-seo-layer:
|
||||
- lib/seo-module.ts: `SEO_MODULE_DEFAULTS` (pagination `{form:'query', param:'page', segments:{pl:'strona', en:'page'}}`, htmlSitemap `{paths:{pl:'/mapa-strony', en:'/sitemap'}}`); setup: guard hooks (`nitro:config`, `nitro:init`, `pages:resolved`, `nitro:build:before`), `extendPages` registering `seo-sitemap-<locale>`, `seo-sitemap-group-<locale>`, `seo-sitemap-group-page-<locale>` with `meta.seoSitemap.locale` (a `taken` exact-path check only), `addServerTemplate('#seo-routes')` from `staticRoutes(pages, noindexRoutes, localeCopySuffixes(i18n))` and `routePatterns(pages)`, `runtime.public.seo = {…, pagination, htmlSitemap}`.
|
||||
- lib/routes.ts: `robotsNoindex`, `pageMetaRobots`, `isLayerPage`, `localeCopySuffixes`, `staticRoutes`, `routePatterns`, `routesTemplate(staticPaths, patterns)`; lib/guard.ts: `flattenPages`, `isNoindex`, `patternCovers`, `pagePattern`; lib/options.ts: `paginationOption`, `htmlSitemapOption`, `SeoHtmlSitemapOption`.
|
||||
- server/lib/routeReport.ts: `ROUTE_REPORT_MAX_ROUTES` 2000, `ROUTE_REPORT_MAX_BYTES` 2048, `routeReportBody(paths)`, `routeReportOutcome`; server/plugins/seo-route-report.ts posts `body` once per start with one retry; server/seo-routes.d.ts types `#seo-routes`.
|
||||
- server/lib/site.ts: `SeoSiteState` (ok, absent, unknown), `siteAnswer` (keeps lastOk), `rootFileTargets(path, site, backendBase, apiBase)` (null unless state ok), `FIXED`; server/middleware/seo-root.ts (503 with Retry-After on a failed fetch).
|
||||
- app/components/global/SeoHtmlSitemapPage.vue: `explicitFirst` → `navigateTo(seoPageHref(route.fullPath, 1, form, '', param), {redirectCode: 301})`; app/components/Seo/SitemapLink.vue: locale from props, the resolve answer, html_lang, the site default; href from `config.public.seo.htmlSitemap.paths[locale]`; app/utils/pageHref.ts `seoPageHref`, `seoPageNumber`.
|
||||
- testing/smoke-ssr.mjs grammar `routereport N [has=PATH]... [lacks=PATH]...` (checks the last report's routes; the token); expect-seo.txt line `routereport 1 has=/ … lacks=/konto lacks=/szkic lacks=/prywatne lacks=/mapa-strony`; expect-seo-down.txt expects `404 /robots.txt noheader=Retry-After`; the playground has a root `[...slug].vue`, `konto.vue` (requiresAuth), `szkic.vue` (seo.robots noindex, follow), `/prywatne/**` in noindexRoutes, htmlSitemap paths pl `/mapa-strony`, en `/en/sitemap`.
|
||||
vue-journal-layer:
|
||||
- app/journal-pages/PostPage.vue: `fallback` null when `cmsHeadPresent(answer)`, every `useSeoMeta` key from it (og:type and article times lost when the CMS answers); app/utils/seo.ts `postSeoMeta`, `cmsHeadPresent`.
|
||||
- lib/journal-module.ts: `journalPagedRoutes(routes, nuxt.options.seo?.pagination)` computed in setup from the raw option; app/utils/routes.ts `journalPagedRoutes`, `journalPagedRouteName`.
|
||||
- .playground: i18n `prefix_except_default`, en default, pl prefixed; no pages of its own; its built server today contains the route names seo-sitemap-en___pl and seo-sitemap-pl___en (the WR-05 evidence).
|
||||
- scripts/check-git-install.sh: `--self-test` (lockfile reading), tag mode (scratch app, `pnpm install`, lockfile pin, build, expect-acme.txt); package.json version 0.7.1 and `"vue-seo-layer": "link:../vue-seo-layer"`.
|
||||
vue-menu-layer, vue-faq-layer: package.json version 0.4.0 with the same link dependency; README install lines pin #v0.2.0; "Upgrading from 0.4" sections.
|
||||
Gate: `scripts/check-phase33.sh --layers` runs every layer's `npm run test`, `build:playground`, vue-seo-layer's `smoke:ssr`, `guard:check`, `purge:check`, the journal's expect-acme*.txt runs and `check-git-install.sh --self-test`, menu and FAQ `smoke:ssr`.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Gap-2 contract (fixed; identical in 33-15 and 33-16)
|
||||
|
||||
The route report gains an optional `noindex` array of static paths, sent with the same token on the same request:
|
||||
|
||||
```
|
||||
POST /_seo/api/v1/registry/routes
|
||||
Authorization: Bearer <golem15.seo.route_report_token, else golem15.seo.notfound_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"routes": ["/", "/blog", "/kontakt"], "noindex": ["/konto", "/pl/konto", "/szkic"]}
|
||||
```
|
||||
|
||||
1. `routes`: unchanged (required array of strings).
|
||||
2. `noindex`: optional array of strings, at most 2000 entries, each starting with `/`, without a `:` parameter or `*`, at most 2048 bytes in UTF-8. It lists every static path the frontend's build serves noindex: pages whose meta has `requiresAuth: true` or a `seo.robots` value with `noindex` or `none`, and static pages under a `seo.noindexRoutes` pattern. The `@nuxtjs/i18n` copies of those pages in other locales are listed as their own locale-prefixed paths (`/pl/konto`). Patterns (`/p/**`) are never sent, the layer's HTML sitemap pages never, and a path is never in both lists.
|
||||
3. Plugin semantics: key absent → the stored set is unchanged; key present (also `[]`) → the stored set becomes exactly its valid entries, normalised like `routes` (`normalPagePath` with the site's address options); an invalid entry is skipped and counted in `skipped`; more than 2000 entries → 422 `{"error":"too_many_routes","max":2000}` and nothing is stored.
|
||||
4. The answer gains one key, the number of stored paths: `{"inserted":1,"updated":3,"skipped":0,"reported_at":"2026-11-04T20:00:00Z","noindex":3}`.
|
||||
5. The SEO sitemap filter excludes any candidate (record, registry page, additional address) whose normalised path is in the set, with reason `noindex`, from the XML and HTML sitemaps. A changed set requests one debounced sitemap rebuild.
|
||||
6. Compatibility: a layer that sends no `noindex` keeps working (the set stays as it is). A plugin built before this contract decodes the body into `{routes}` and ignores the unknown key (encoding/json without DisallowUnknownFields), and it reads at most `MaxReportedRoutes*(MaxRouteBytes+8)+1024` = 2000*(2048+8)+1024 = 4,113,024 bytes of body (sm-seo-plugin controllers/api/registry.go:21, `maxRouteReportBody`). The layer therefore keeps the serialised JSON body at or under 4,113,024 bytes: it drops `noindex` entries from the end first, and only when the body is still too large `routes` entries from the end, so an older plugin never refuses the body for its size. A plugin built with this contract accepts at least that size (its cap covers both lists). The layer ignores the answer body.
|
||||
|
||||
## Execution units and resume points
|
||||
|
||||
- Run from summercms.go. Commit groups per repository, staged by explicit path (never `git add -A`); no attribution trailers; code and planning docs in separate commits; a README change goes in the same commit as the behaviour or option it describes. Nothing is pushed or tagged. STATE.md changes only through `msd_run query state.*` handlers and is never committed.
|
||||
- Each layer commit keeps that layer's `npm run test` green; a task's verify adds the playground build and smokes. Node ≥ 22.18 as in the layers' engines.
|
||||
- 33-15 runs in parallel and commits to summercms.go too: if git reports `.git/index.lock`, wait and retry; never remove another session's lock or stage its files. deferred-items.md and 33-release-log.md belong to 33-15 in this wave; report the layer-side statuses in this plan's SUMMARY instead of editing them.
|
||||
- Do not replace the `link:../vue-seo-layer` dependencies: 33-14 Task 4 does that before tagging.
|
||||
- **Gate stages:** this plan runs only `scripts/check-phase33.sh --self-test --layers`, because 33-15 changes the Go plugins in parallel. It does not run consumers: that stage builds the four apps' Go binaries from their own plugin submodules (`plugins/golem15/*` in each app's go.work) and their Nuxt apps against the layer tags pinned in their package.json (git+https, not these checkouts), and checks the 33-12/33-13/33-14 patches against the apps, so no change of this plan reaches it. The orchestrator runs the full `PHASE33_REQUIRE_DOCKER=1 bash scripts/check-phase33.sh --all` once, after both 33-15-SUMMARY.md and 33-16-SUMMARY.md exist and before 33-14 Task 4.
|
||||
- Every `<automated>` command starts with `set -o pipefail`, chains with `&&`, or captures the exit status explicitly, so a failing command is never hidden by a later pipe stage.
|
||||
- **Executor note (context budget):** if the context runs low, stop only after a complete task (all its commit groups committed and its verify green), write the SUMMARY with the remaining tasks and their resume points for a continuation run. Every task is independently committable.
|
||||
- Resume check per repository: `git -C <repo> log --format=%s | grep -Fx '<subject>'`.
|
||||
|
||||
| Task | Commit groups, in order | Resume point |
|
||||
|------|-------------------------|--------------|
|
||||
| 1 | seo-layer: `feat: the route report sends the noindex static paths (tracer)` | the routereport noindex smoke line passes |
|
||||
| 2 | seo-layer: A `fix: HTML sitemap page 1 redirects with the locale's segment`; B `fix: HTML sitemap pages once per locale, never over a catch-all by default`; C `fix: root files answer 503 while the backend has not answered`; journal-layer: D `test: the playground's HTML sitemap paths per locale` | sitemapPages.test.ts passes and the journal playground build has one sitemap route per locale |
|
||||
| 3 | journal-layer: A `fix: post pages keep og:type article and their article times`; B `fix: path-form listing routes use the SEO module's resolved pagination` | the og:type smoke line and the routes test pass |
|
||||
| 4 | journal-layer: A `feat: check-git-install --check-links refuses local dependencies`; each layer: B `chore: version <x.y.z> and install pin` with its README notes; here: C `docs(33-16): release-note drafts for the layer fixes` | plan complete |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end "the Nuxt server's route report tells the CMS which static paths it serves noindex" — one path only (G1, layer side)</name>
|
||||
<files>../vue-seo-layer/lib/routes.ts, ../vue-seo-layer/lib/seo-module.ts, ../vue-seo-layer/server/seo-routes.d.ts, ../vue-seo-layer/server/lib/routeReport.ts, ../vue-seo-layer/server/plugins/seo-route-report.ts, ../vue-seo-layer/testing/smoke-ssr.mjs, ../vue-seo-layer/testing/expect-seo.txt, ../vue-seo-layer/tests/routes.test.ts, ../vue-seo-layer/README.md</files>
|
||||
<read_first>
|
||||
- ../vue-seo-layer/lib/routes.ts and lib/guard.ts (flattenPages, isNoindex)
|
||||
- ../vue-seo-layer/lib/seo-module.ts (the #seo-routes server template) and server/seo-routes.d.ts
|
||||
- ../vue-seo-layer/server/lib/routeReport.ts and server/plugins/seo-route-report.ts
|
||||
- ../vue-seo-layer/testing/smoke-ssr.mjs (the routereport grammar, lines ~15 and ~236-257) and testing/expect-seo.txt (the routereport line), testing/fixture-api.mjs (records the request body)
|
||||
- ../vue-seo-layer/tests/routes.test.ts (staticRoutes, localeCopySuffixes, routeReportBody cases)
|
||||
- the "Gap-2 contract" section of this plan
|
||||
</read_first>
|
||||
<behavior>
|
||||
- noindexStaticRoutes: a requiresAuth page, a page with seo.robots "noindex, follow" and a static page under /prywatne/** are listed; an ordinary page, a page with parameters, a catch-all and a layer seo-sitemap- page are not; with i18n copies (names ending in ___pl) the /pl/... copy of a noindex page is listed too; the result is sorted and has no path that staticRoutes lists
|
||||
- routesTemplate exports noindexPaths; routeReportBody(paths, noindex) answers {routes, noindex}, both filtered to paths starting with / within 2048 bytes and cut to 2000
|
||||
- routeReportBody keeps the serialised JSON at or under ROUTE_REPORT_MAX_BODY (2000*(2048+8)+1024 = 4,113,024 bytes, the older plugin's limit): 2000 routes of 2048 bytes plus 100 noindex paths of 200 bytes answer all 2000 routes and only the leading noindex entries that fit (a prefix of the input); small lists come back whole; routes that alone exceed the limit (paths full of characters JSON escapes) are cut from the end with noindex empty
|
||||
- Smoke: routereport 1 has=/ lacks=/konto noindex=/konto noindex=/szkic noindex=/prywatne lacksnoindex=/ lacksnoindex=/mapa-strony
|
||||
</behavior>
|
||||
<action>
|
||||
Per G1, D-23, D-24, D-33 and the gap-2 contract above (P33-67): tests first; `noindexStaticRoutes` in lib/routes.ts; `routesTemplate(staticPaths, patterns, noindexPaths)` adds `export const noindexPaths`; the module passes it; server/seo-routes.d.ts declares it; `routeReportBody(paths, noindex)` applies the same filter and limits to both lists and then keeps `Buffer.byteLength(JSON.stringify(body))` at or under the new `ROUTE_REPORT_MAX_BODY = ROUTE_REPORT_MAX_ROUTES * (ROUTE_REPORT_MAX_BYTES + 8) + 1024` (the older plugin's `maxRouteReportBody`, gap-2 contract point 6) by dropping `noindex` entries from the end first and then `routes` entries, with tests first in tests/routes.test.ts (the behaviour above); the server plugin imports `noindexPaths` and logs "route report: N routes and M noindex paths sent". smoke-ssr.mjs: the routereport line accepts `noindex=PATH` (present in the last report's noindex list) and `lacksnoindex=PATH`, and the header comment documents them; expect-seo.txt's routereport line gains the checks of the behaviour above. README: the route report paragraph gets the exact JSON of the gap-2 contract and says which pages land in `noindex`; the `routeReport` option row mentions it. Commit as listed.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-seo-layer run test 2>&1 | tail -8</automated>
|
||||
<fails_when>non-zero exit, a "not ok" line, or a "# fail" count above 0</fails_when>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-seo-layer run build:playground 2>&1 | tail -3 && node ../vue-seo-layer/testing/smoke-ssr.mjs --output ../vue-seo-layer/.playground/.output --fixtures ../vue-seo-layer/testing/fixtures --expect ../vue-seo-layer/testing/expect-seo.txt 2>&1 | tail -10</automated>
|
||||
<fails_when>non-zero exit, or a "not ok", "FAIL" or "routereport" failure line</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "export function noindexStaticRoutes" ../vue-seo-layer/lib/routes.ts` prints one line
|
||||
- `grep -n "ROUTE_REPORT_MAX_BODY" ../vue-seo-layer/server/lib/routeReport.ts ../vue-seo-layer/tests/routes.test.ts` prints a line in each
|
||||
- `grep -n "noindexPaths" ../vue-seo-layer/server/plugins/seo-route-report.ts ../vue-seo-layer/server/seo-routes.d.ts` prints a line in each
|
||||
- `grep -n "noindex=/konto" ../vue-seo-layer/testing/expect-seo.txt` prints one line
|
||||
- `grep -n '"noindex"' ../vue-seo-layer/README.md` prints a line (the contract JSON)
|
||||
</acceptance_criteria>
|
||||
<done>Each server start tells the CMS the static paths it serves noindex, so the sitemaps can leave them out.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: HTML sitemap pages: page-1 redirect, catch-all apps, once per locale with i18n, the footer link; root files 503 while the backend is down (L4 WR-01, WR-04, WR-05, WR-08)</name>
|
||||
<files>../vue-seo-layer/lib/sitemapPages.ts, ../vue-seo-layer/lib/seo-module.ts, ../vue-seo-layer/lib/options.ts, ../vue-seo-layer/app/components/global/SeoHtmlSitemapPage.vue, ../vue-seo-layer/app/components/Seo/SitemapLink.vue, ../vue-seo-layer/app/utils/pageHref.ts, ../vue-seo-layer/server/lib/site.ts, ../vue-seo-layer/server/middleware/seo-root.ts, ../vue-seo-layer/.playground/nuxt.config.ts, ../vue-seo-layer/testing/expect-seo-down.txt, ../vue-seo-layer/tests/sitemapPages.test.ts, ../vue-seo-layer/tests/pageHref.test.ts, ../vue-seo-layer/tests/rootFiles.test.ts, ../vue-seo-layer/README.md, ../vue-journal-layer/.playground/nuxt.config.ts</files>
|
||||
<read_first>
|
||||
- ../vue-seo-layer/lib/seo-module.ts (the extendPages block, the guard hooks, runtime.public.seo) and lib/options.ts (htmlSitemapOption), lib/guard.ts (patternCovers, flattenPages, pagePattern), lib/routes.ts (localeCopySuffixes)
|
||||
- ../vue-seo-layer/app/components/global/SeoHtmlSitemapPage.vue, app/components/Seo/SitemapLink.vue, app/seo-pages/SitemapPage.vue and SitemapGroupPage.vue (route.meta.seoSitemap), app/utils/pageHref.ts and tests/pageHref.test.ts
|
||||
- ../vue-seo-layer/server/lib/site.ts (rootFileTargets, siteAnswer), server/middleware/seo-root.ts, tests/rootFiles.test.ts, testing/expect-seo-down.txt, testing/expect-seo.txt (the /mapa-strony lines)
|
||||
- ../vue-journal-layer/.playground/nuxt.config.ts (i18n prefix_except_default) and node_modules/@nuxtjs/i18n/dist/module.mjs around the pages:extend / pages:resolved localisation hook
|
||||
- 33-REVIEW.md lane 4 WR-01, WR-04, WR-05, WR-08; 33-09-SUMMARY deviation 7 (an app without pages)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- seoFirstPageRedirect: ('/mapa-strony/posty/strona/1', 'path', 'strona', 'page') → '/mapa-strony/posty'; ('/sitemap/posts/page/1', 'path', 'page', 'page') → '/sitemap/posts'; ('/mapa-strony/posty?page=1', 'query', '', 'page') → '/mapa-strony/posty'; a target equal to fullPath (for example the segment missing) → null
|
||||
- seoSitemapPages: without i18n the names are seo-sitemap-<locale>, seo-sitemap-group-<locale> (and the page route in the path form with the locale's segment); with i18n prefix_except_default and locales en, pl each route is named <name>___<locale> at the configured path and no route exists for another locale; an app page at the path skips the locale as taken; a root catch-all page skips every locale as catch-all unless overCatchAll is true; a /pl/** catch-all skips only a pl path under /pl/; htmlSitemap false answers nothing
|
||||
- rootFileTargets: state unknown without a site answer gives the backend target for /robots.txt, /sitemap.xml and /llms.txt and null for sitemap files and verification files; state absent stays null; state ok unchanged
|
||||
- Smokes: expect-seo-down.txt answers 503 /robots.txt and 503 /sitemap.xml with Retry-After: 60; expect-seo.txt's /mapa-strony lines still pass with the playground's overCatchAll: true
|
||||
- Journal playground build: the built server's seo-sitemap route names are exactly seo-sitemap-en___en, seo-sitemap-group-en___en, seo-sitemap-pl___pl and seo-sitemap-group-pl___pl, at /sitemap and /pl/mapa-strony
|
||||
</behavior>
|
||||
<action>
|
||||
Per L4 WR-01, WR-04, WR-05, WR-08, D-08, D-22, D-34, D-42 and P33-68 to P33-70, tests first:
|
||||
- (A, WR-01) `seoFirstPageRedirect` in app/utils/pageHref.ts (P33-69); SeoHtmlSitemapPage takes the locale's segment (site answer `pagination.segments[locale]`, else `config.public.seo.pagination.segments[locale]`, else the param) and redirects only when the helper answers a target. Commit A.
|
||||
- (B, WR-04, WR-05) `htmlSitemapOption` accepts `overCatchAll` (boolean, default false) and keeps it in the runtime option; lib/sitemapPages.ts `seoSitemapPages` (P33-68, pure, unit-tested); seo-module.ts drops the extendPages registration and calls the builder from a `pages:resolved` hook registered in `modules:done`, removing its earlier routes first, pushing the result into the resolved list and logging each skip ("HTML sitemap: <locale> <path> is rendered by the app's catch-all page; the layer's page is not registered (set seo.htmlSitemap.overCatchAll to serve it ahead of the catch-all)"). SitemapLink: the locale falls back to the i18n current locale (`useNuxtApp().$i18n?.locale?.value`, guarded: the layer does not depend on i18n) before the site default, and the link renders only when `useRouter().resolve(href)` matches a route whose `meta.seoSitemap.locale` is that locale. The vue-seo-layer playground sets `htmlSitemap.overCatchAll: true` (its root `[...slug].vue`) with a comment; the journal playground sets `seo.htmlSitemap.paths` to en `/sitemap`, pl `/pl/mapa-strony` (the README's full-path rule for i18n apps) — commit D in vue-journal-layer. README: the `htmlSitemap` option row documents `overCatchAll`, the catch-all rule, one route per locale with i18n, and the footer link rule. After the change, build the menu playground once without `htmlSitemap: false` to see whether the 33-09 "app without pages" item is gone; record the result in the SUMMARY and keep both playgrounds' config unchanged. Commit B.
|
||||
- (C, WR-08) rootFileTargets per P33-70, with its doc comment and the seo-root.ts header comment updated; expect-seo-down.txt answers 503 with `header=Retry-After: 60` for /robots.txt and /sitemap.xml and keeps 404 for a sitemap file and a verification file, and its comment states the staging reason; tests/rootFiles.test.ts splits the unknown and absent cases. README root files paragraph. Commit C.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-seo-layer run test 2>&1 | tail -8</automated>
|
||||
<fails_when>non-zero exit, a "not ok" line, a "# fail" count above 0, or sitemapPages.test.ts missing from the run</fails_when>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-seo-layer run build:playground 2>&1 | tail -3 && npm --prefix ../vue-seo-layer run smoke:ssr 2>&1 | tail -12 && npm --prefix ../vue-seo-layer run guard:check 2>&1 | tail -4 && npm --prefix ../vue-seo-layer run purge:check 2>&1 | tail -4</automated>
|
||||
<fails_when>non-zero exit from any command, or a "not ok" or "FAIL" line</fails_when>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-journal-layer run build:playground 2>&1 | tail -2 && grep -rhoE '"seo-sitemap-[a-z-]+(___[a-z]+)?"' ../vue-journal-layer/.playground/.output/server | sort -u</automated>
|
||||
<fails_when>non-zero exit, any printed name whose suffix differs from its locale (en___pl, pl___en), or the list differs from exactly "seo-sitemap-en___en", "seo-sitemap-group-en___en", "seo-sitemap-group-pl___pl", "seo-sitemap-pl___pl"</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "export function seoSitemapPages" ../vue-seo-layer/lib/sitemapPages.ts` prints one line
|
||||
- `grep -n "modules:done" ../vue-seo-layer/lib/seo-module.ts` prints one line
|
||||
- `grep -n "export function seoFirstPageRedirect" ../vue-seo-layer/app/utils/pageHref.ts` prints one line
|
||||
- `grep -n "overCatchAll" ../vue-seo-layer/README.md ../vue-seo-layer/.playground/nuxt.config.ts` prints a line in each
|
||||
- `grep -n "^503 /robots.txt" ../vue-seo-layer/testing/expect-seo-down.txt` prints one line
|
||||
</acceptance_criteria>
|
||||
<done>The HTML sitemap pages never loop, never shadow CMS pages behind a catch-all unless asked, exist once per locale with i18n, and robots.txt never reads as allow-all while the backend is down.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Journal post meta and path-form pagination routes (L4 WR-02, WR-03)</name>
|
||||
<files>../vue-journal-layer/app/journal-pages/PostPage.vue, ../vue-journal-layer/app/utils/seo.ts, ../vue-journal-layer/app/utils/routes.ts, ../vue-journal-layer/lib/journal-module.ts, ../vue-journal-layer/tests/seo.test.ts, ../vue-journal-layer/tests/routes.test.ts, ../vue-journal-layer/testing/expect-acme.txt, ../vue-journal-layer/README.md</files>
|
||||
<read_first>
|
||||
- ../vue-journal-layer/app/journal-pages/PostPage.vue (fallback and useSeoMeta) and app/utils/seo.ts (postSeoMeta, cmsHeadPresent), tests/seo.test.ts
|
||||
- ../vue-seo-layer/app/utils/seo.ts (seoHeadInput: which head keys the CMS answer sets) to confirm the layer never writes og:type or article:*
|
||||
- ../vue-journal-layer/lib/journal-module.ts (setup, extendPages) and app/utils/routes.ts (journalPagedRoutes), tests/routes.test.ts
|
||||
- ../vue-seo-layer/lib/seo-module.ts (runtime.public.seo.pagination is written in its setup)
|
||||
- ../vue-journal-layer/testing/expect-acme.txt (heading-on and uncategorised-note lines) and testing/fixtures/acme/posts.json (published_at, updated_at)
|
||||
- 33-REVIEW.md lane 4 WR-02, WR-03
|
||||
</read_first>
|
||||
<behavior>
|
||||
- seo.test.ts: journalArticleMeta(post) answers ogType article with the post's published_at and updated_at, and leaves a missing date out
|
||||
- Smoke (expect-acme.txt): /blog/heading-on (a CMS answer) has exactly one og:type, of value article, and an article:published_time; /blog/uncategorised-note (no CMS answer) still has exactly one og:type
|
||||
- routes.test.ts: journalPaginationSource answers runtimeConfig.public.seo.pagination when present and the raw seo.pagination otherwise; journalPagedRoutes of the resolved option for an app that set only form path lists /blog/strona/:page(\d+) and /blog/page/:page(\d+)
|
||||
</behavior>
|
||||
<action>
|
||||
Per L4 WR-02, WR-03, D-40, D-42, D-97 and P33-71, tests first:
|
||||
- (A, WR-02) `journalArticleMeta(post)` in app/utils/seo.ts (prefixed, auto-imported); PostPage always sets `ogType`, `articlePublishedTime` and `articleModifiedTime` from it, and only the CMS-owned keys (title, description, og title, og description, og image) keep the fallback; README SEO section states which head keys the post page always sets. Commit A.
|
||||
- (B, WR-03) `journalPaginationSource(nuxtOptions)` in app/utils/routes.ts (P33-71); journal-module.ts computes the paged routes inside its extendPages callback from it instead of in setup from the raw option; README pagination paragraph says the segments follow vue-seo-layer's resolved option (defaults pl strona, en page). Commit B.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-journal-layer run test 2>&1 | tail -8</automated>
|
||||
<fails_when>non-zero exit, a "not ok" line or a "# fail" count above 0</fails_when>
|
||||
<automated>set -o pipefail; npm --prefix ../vue-journal-layer run build:playground 2>&1 | tail -2 && npm --prefix ../vue-journal-layer run smoke:ssr 2>&1 | tail -10</automated>
|
||||
<fails_when>non-zero exit, or a "not ok" or "FAIL" line in any of the five expectation runs</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n "export function journalArticleMeta" ../vue-journal-layer/app/utils/seo.ts` prints one line
|
||||
- `grep -n 'og:type' ../vue-journal-layer/testing/expect-acme.txt` prints at least two lines
|
||||
- `grep -n "journalPaginationSource" ../vue-journal-layer/lib/journal-module.ts ../vue-journal-layer/app/utils/routes.ts` prints a line in each
|
||||
</acceptance_criteria>
|
||||
<done>Post pages keep their article metadata next to the CMS head, and Polish path-form listing pages have routes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Release guard against link: dependencies, version fields and install pins, upgrade notes and release-note drafts (L4 CR-01, WR-04 docs)</name>
|
||||
<files>../vue-journal-layer/scripts/check-git-install.sh, ../vue-journal-layer/package.json, ../vue-journal-layer/README.md, ../vue-seo-layer/package.json, ../vue-seo-layer/README.md, ../vue-menu-layer/package.json, ../vue-menu-layer/README.md, ../vue-faq-layer/package.json, ../vue-faq-layer/README.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-seo-layer-v0.2.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-journal-layer-v0.8.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-menu-layer-v0.5.0.md, .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-faq-layer-v0.5.0.md</files>
|
||||
<read_first>
|
||||
- ../vue-journal-layer/scripts/check-git-install.sh (usage, --self-test, the tag mode's scratch install)
|
||||
- the four layers' package.json and README (Install line, "Upgrading from …" sections, Releasing)
|
||||
- the four 33-14-notes-vue-*.md drafts
|
||||
- 33-REVIEW.md lane 4 CR-01 and WR-04; 33-14-PLAN.md Task 4 step 5 (read only: the swap and `check-git-install.sh <tag>` run there)
|
||||
</read_first>
|
||||
<action>
|
||||
Per L4 CR-01, WR-04, D-97 and P33-72:
|
||||
- (A, journal) check-git-install.sh gains `--check-links DIR...` (P33-72, with the usage comment) and runs the same check on the installed layer in the tag mode before building ("the tag's package.json still depends on <name> through <spec>"); `--self-test` adds a planted package.json with a link: dependency (reported, exit 1) and one with a git+https pin (passes), next to the lockfile case. README Releasing: before tagging any content layer, replace the link dependency with the vue-seo-layer tag pin, run `bash scripts/check-git-install.sh --check-links ../vue-seo-layer ../vue-journal-layer ../vue-menu-layer ../vue-faq-layer`, then the tag mode after pushing. Commit A.
|
||||
- (B, each layer) version fields: vue-seo-layer 0.2.0, vue-journal-layer 0.8.0, vue-menu-layer 0.5.0, vue-faq-layer 0.5.0; README install lines pin `#v0.2.0`, `#v0.8.0`, `#v0.5.0`, `#v0.5.0` (and any other pin of these tags in the READMEs). Upgrade notes (WR-04): the journal's "Upgrading from 0.7" and the menu and FAQ "Upgrading from 0.4" sections say that the layer brings vue-seo-layer's HTML sitemap pages (pl /mapa-strony, en /sitemap by default), that an app rendering pages through a root catch-all gets none unless it sets `seo.htmlSitemap.overCatchAll: true`, that an app with an editor page at such a path keeps its page, and that `seo: { htmlSitemap: false }` turns them off; the journal note also states that the post page keeps og:type article and its article times. Leave the link: dependency in place (33-14 Task 4). One commit per layer.
|
||||
- (C, here) Release-note drafts: vue-seo-layer v0.2.0 (the route report sends the noindex static paths; the HTML sitemap pages once per locale with i18n, not over a catch-all unless overCatchAll; the "Fixed" line about root files becomes: robots.txt, sitemap.xml and llms.txt answer 503 with Retry-After while the backend has not answered since the server started, so a crawler retries); vue-journal-layer v0.8.0 (path-form listing routes follow the SEO layer's segments; the catch-all upgrade note); vue-menu-layer and vue-faq-layer v0.5.0 (the catch-all upgrade note). Commit C in summercms.go as planning docs.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash ../vue-journal-layer/scripts/check-git-install.sh --self-test</automated>
|
||||
<fails_when>non-zero exit or no "self-test ok" line</fails_when>
|
||||
<automated>out=$(bash ../vue-journal-layer/scripts/check-git-install.sh --check-links ../vue-seo-layer ../vue-journal-layer ../vue-menu-layer ../vue-faq-layer 2>&1); code=$?; printf '%s\n' "$out"; echo "exit=$code"</automated>
|
||||
<fails_when>"exit=0" (the guard missed the link dependencies that stay until 33-14 Task 4), fewer than three lines naming vue-seo-layer through link:, or a line naming ../vue-seo-layer itself (the exit status is captured on purpose: the guard must fail here)</fails_when>
|
||||
<automated>set -o pipefail; for d in seo journal menu faq; do v=$(node -p "require('../vue-$d-layer/package.json').version") && pins=$(grep -oE "vue-$d-layer\.git#v[0-9.]+" "../vue-$d-layer/README.md" | sort -u | tr '\n' ' ') && echo "$d $v $pins" || echo "missing $d"; done</automated>
|
||||
<fails_when>a "missing" line, a version other than seo 0.2.0, journal 0.8.0, menu 0.5.0, faq 0.5.0, or a README pin other than that version's tag</fails_when>
|
||||
<automated>set -o pipefail; bash scripts/check-phase33.sh --self-test --layers 2>&1 | tail -12</automated>
|
||||
<fails_when>non-zero exit, a "FAIL" or "refuse:" line, or a missing "PASS self-test" or "PASS layers" line (this plan never runs the go, plugins, docker or consumers stages; the orchestrator runs --all after both SUMMARYs)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n -- "--check-links" ../vue-journal-layer/scripts/check-git-install.sh ../vue-journal-layer/README.md` prints a line in each
|
||||
- `grep -n "overCatchAll" ../vue-journal-layer/README.md ../vue-menu-layer/README.md ../vue-faq-layer/README.md` prints a line in each
|
||||
- `grep -n "503" .planning/phases/33-seo-output-audit-and-content/33-14-notes-vue-seo-layer-v0.2.0.md` prints a line
|
||||
- each content layer's package.json still holds its `link:../vue-seo-layer` dependency (the swap belongs to 33-14 Task 4)
|
||||
</acceptance_criteria>
|
||||
<done>A layer cannot be tagged with a local dependency unnoticed, the versions and install lines match the release, and the upgrade and release notes describe the new behaviour.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Nuxt server → CMS route report | server-to-server POST with a private token, now also carrying noindex paths |
|
||||
| crawler → robots.txt via the layer | a missing robots.txt reads as allow-all |
|
||||
| tagged layer → consuming app install | a local link: dependency resolves outside the package |
|
||||
| visitor → HTML sitemap pages | request paths drive redirects |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-33-77 | Information disclosure | route report noindex paths | low | accept | the paths are the site's public route names (signed-in pages included) sent server-side to the configured apiBase only, with the token kept in the server-only runtime config; no visitor data |
|
||||
| T-33-78 | Tampering (supply chain) | content layers with a link: dependency | medium | mitigate | `--check-links` before tagging and the same check in the tag mode of check-git-install.sh; --self-test in the gate's layers stage (Task 4) |
|
||||
| T-33-79 | Information disclosure / integrity | robots.txt while the backend is down | medium | mitigate | 503 with Retry-After for the fixed root files in the unknown state, so a staging site's Disallow is never replaced by an app 404 (Task 2) |
|
||||
| T-33-80 | Denial of service | HTML sitemap page-1 redirect | low | mitigate | the redirect uses the locale's segment and never targets the current URL (seoFirstPageRedirect), so no 301 loop (Task 2) |
|
||||
| T-33-SC | Tampering | npm/pip/cargo/Go installs | high | mitigate | no npm package is added or upgraded; package.json changes are the version fields only; pnpm-lock.yaml files do not change (the SUMMARY shows `git diff BASE..HEAD -- pnpm-lock.yaml` empty per layer) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- vue-seo-layer: `npm run test`, `build:playground`, `smoke:ssr` (all seven expectation runs, including expect-seo-down.txt), `guard:check`, `purge:check`.
|
||||
- vue-journal-layer: `npm run test`, `build:playground`, `smoke:ssr` (five runs), the built route-name check, `check-git-install.sh --self-test` and `--check-links`.
|
||||
- vue-menu-layer, vue-faq-layer: `npm run test`, `build:playground`, `smoke:ssr` (through the gate's layers stage).
|
||||
- summercms.go: `check-phase33.sh --self-test --layers`. The full `--all` gate is run by the orchestrator once, after both 33-15 and 33-16 SUMMARYs exist and before 33-14 Task 4.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- G1 is closed on the layer side (together with 33-15 on the CMS side).
|
||||
- Every in-scope L4 finding has a fix and a test or a gate check.
|
||||
- No layer is pushed or tagged and no link: dependency is swapped; 33-14 Task 4 resumes after 33-15 and 33-16, followed by re-verification.
|
||||
</success_criteria>
|
||||
|
||||
## Deferred (v0.6.2 backlog item, grouped by repository)
|
||||
|
||||
- **vue-journal-layer:** L4 WR-06 (`sizes` on cards and the post image do not match the grid); info IN-03, IN-07.
|
||||
- **vue-seo-layer:** L4 WR-07 (sitemap group-page breadcrumbs use the default locale's home crumb); info IN-01, IN-02, IN-04, IN-05, IN-06, IN-08, IN-09.
|
||||
- **vue-faq-layer:** info IN-07 (host-header origins in FaqBlock).
|
||||
- **Go plugins and framework (planned in 33-15, deferred there):** L1 WR-02 code change, WR-04, WR-06, WR-07; L2 WR-06, WR-07, WR-08; L3 WR-02, WR-05; G2; the L1 to L3 info items; verifier warnings W1 to W5.
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- vue-seo-layer: `noindexStaticRoutes`, `#seo-routes` export `noindexPaths`, `routeReportBody(paths, noindex)`, route report key `noindex`; `lib/sitemapPages.ts` with `seoSitemapPages`; option `seo.htmlSitemap.overCatchAll`; `seoFirstPageRedirect`; root files 503 in the unknown state; smoke grammar `noindex=` and `lacksnoindex=`; version 0.2.0.
|
||||
- vue-journal-layer: `journalArticleMeta`, `journalPaginationSource`; `check-git-install.sh --check-links` and the tag-mode guard; the playground's per-locale HTML sitemap paths; version 0.8.0.
|
||||
- vue-menu-layer, vue-faq-layer: version 0.5.0, install pins, upgrade notes.
|
||||
- summercms.go: updated 33-14-notes-vue-*.md drafts.
|
||||
- Tests: tests/sitemapPages.test.ts; new cases in tests/routes.test.ts, tests/pageHref.test.ts, tests/rootFiles.test.ts (vue-seo-layer) and tests/seo.test.ts, tests/routes.test.ts (vue-journal-layer); smoke lines in expect-seo.txt, expect-seo-down.txt, expect-acme.txt; check-git-install.sh self-test cases.
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/33-seo-output-audit-and-content/33-16-SUMMARY.md` when done: the commits per layer, the finding-to-test table, the built route names of the journal playground, whether the 33-09 "app without pages" item is gone, the layer-side status of G1 and the 33-09 check-git-install item for deferred-items.md (edited by 33-15), and the `--self-test --layers` gate result.
|
||||
</output>
|
||||
Reference in New Issue
Block a user