diff --git a/.planning/phases/33-seo-output-audit-and-content/33-16-SUMMARY.md b/.planning/phases/33-seo-output-audit-and-content/33-16-SUMMARY.md new file mode 100644 index 00000000..5a1b0e76 --- /dev/null +++ b/.planning/phases/33-seo-output-audit-and-content/33-16-SUMMARY.md @@ -0,0 +1,224 @@ +--- +phase: 33-seo-output-audit-and-content +plan: 16 +subsystem: nuxt-layers +tags: [seo, nuxt, vue-seo-layer, vue-journal-layer, vue-menu-layer, vue-faq-layer, gap-closure, route-report, html-sitemap, i18n, release-guard] +status: complete + +requires: + - phase: 33-07..33-09 + provides: vue-seo-layer v0.2.0 features (route report, root files, HTML sitemap pages, build guard) and the content layers on it + - phase: 33-13 + provides: the release decision (after-tags) and the 33-14 release checkpoint this plan unblocks +provides: + - "vue-seo-layer: noindexStaticRoutes, #seo-routes noindexPaths, routeReportBody(paths, noindex) with ROUTE_REPORT_MAX_BODY, route report key noindex (gap-2 contract), smoke grammar noindex= / lacksnoindex=" + - "vue-seo-layer: lib/sitemapPages.ts (seoSitemapPages, isSitemapRoute, sitemapSkipMessage), option seo.htmlSitemap.overCatchAll, seoFirstPageRedirect, SitemapLink route check and i18n locale fallback, root files 503 in the unknown state" + - "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 fields seo 0.2.0, journal 0.8.0, menu 0.5.0, faq 0.5.0 with README install pins and upgrade notes; the four 33-14-notes-vue-*.md drafts updated" +affects: [33-14, 33-15] + +actuals: + tokens: 16400 + tasks: 4 + commits: 13 +plan_head_before: 8d562004cb05d8d40d12253c23e406259de61006 +plan_head_after: c2e56ef +plan_heads: + vue-seo-layer: { before: b3a0078, after: 973142a, commits: 5 } + vue-journal-layer: { before: e8f307b, after: 761c7fc, commits: 5 } + vue-menu-layer: { before: 8b5da67, after: a7602c9, commits: 1 } + vue-faq-layer: { before: 7d952fa, after: 5e5950a, commits: 1 } + summercms.go: { before: 8d56200, after: c2e56ef, commits: 1 } + +tech-stack: + added: [] + patterns: + - "Layer routes that must not be localised by @nuxtjs/i18n are added from a pages:resolved hook registered on ready (after every module's modules:done handler), named " + - "Report bodies bounded by the older consumer's read limit: drop the optional list from the end first, then the required one" + - "Release guard for git-installed layers: package.json local-spec scan (link:, file:, workspace:, portal:) before tagging and on the installed tag" + +key-files: + created: + - ../vue-seo-layer/lib/sitemapPages.ts + - ../vue-seo-layer/tests/sitemapPages.test.ts + modified: + - ../vue-seo-layer/lib/routes.ts + - ../vue-seo-layer/lib/seo-module.ts + - ../vue-seo-layer/lib/options.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/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 + +key-decisions: + - "The HTML sitemap pages are added from a pages:resolved hook registered on ready, not in modules:done: vue-seo-layer's module is set up before @nuxtjs/i18n, so a hook added from its modules:done handler ran before i18n's and i18n localised the layer's routes again (seo-sitemap-en___en___pl)" + - "routeReportBody drops every noindex entry before it cuts routes (gap-2 contract point 6): when routes alone exceed 4,113,024 bytes, noindex is [] even if a short entry would fit in the slack" + - "An app with only app.vue no longer needs seo.htmlSitemap: false: without the pages module no pages:resolved runs, so no sitemap route is added and the router stays off; such an app has no HTML sitemap pages" + - "check-git-install --check-links prints `check-git-install: : depends on through (
)`, one wording for both modes" + +requirements-completed: [SC-1, SC-2, D-08, D-22, D-23, D-24, D-33, D-34, D-40, D-42, D-97] + +metrics: + duration: "19 min (2026-11-04T21:38:00Z to 21:57:35Z by the session clock)" + completed: 2026-11-04 +--- + +# Phase 33 Plan 16: Nuxt layers gap closure Summary + +The Nuxt layers now send the noindex static paths in the route report, inside the older plugin's body limit. The HTML sitemap pages exist once per locale with @nuxtjs/i18n, are never put over a catch-all unless `overCatchAll` is set, and page 1 never redirects to itself. robots.txt, sitemap.xml and llms.txt answer 503 while the backend is unknown. Journal post pages keep og:type article, and the journal registers its path-form routes from the SEO module's resolved pagination. A `--check-links` guard blocks tags with `link:` dependencies, and the versions and pins match the release. + +## Commits + +| Repo | Commit | Subject | +|------|--------|---------| +| vue-seo-layer | 7a26692 | feat: the route report sends the noindex static paths (tracer) | +| vue-seo-layer | 7588aa5 | fix: HTML sitemap page 1 redirects with the locale's segment | +| vue-seo-layer | a651de4 | fix: HTML sitemap pages once per locale, never over a catch-all by default | +| vue-seo-layer | 8cdf359 | fix: root files answer 503 while the backend has not answered | +| vue-seo-layer | 973142a | chore: version 0.2.0 and install pin | +| vue-journal-layer | 1e05efc | test: the playground's HTML sitemap paths per locale | +| vue-journal-layer | 839db51 | fix: post pages keep og:type article and their article times | +| vue-journal-layer | d64fe47 | fix: path-form listing routes use the SEO module's resolved pagination | +| vue-journal-layer | d4ba3e2 | feat: check-git-install --check-links refuses local dependencies | +| vue-journal-layer | 761c7fc | chore: version 0.8.0 and install pin | +| vue-menu-layer | a7602c9 | chore: version 0.5.0 and install pin | +| vue-faq-layer | 5e5950a | chore: version 0.5.0 and install pin | +| summercms.go | c2e56ef | docs(33-16): release-note drafts for the layer fixes | + +All on each repo's default branch (layers `main`, summercms.go `master`). Nothing was pushed or tagged, and no `link:../vue-seo-layer` dependency was swapped. `git diff ..HEAD -- pnpm-lock.yaml` is empty in all four layers, and package.json changed only in its `version` field. + +## Finding to test + +| Finding | Fix | Proving test | +|---------|-----|--------------| +| G1 layer side (D-23, D-24, D-33) | `noindexStaticRoutes`, `#seo-routes` `noindexPaths`, `routeReportBody(paths, noindex)` with `ROUTE_REPORT_MAX_BODY` = 4,113,024 | tests/routes.test.ts: `noindexStaticRoutes: the static paths the build serves noindex`, `…client-only noindex pages kept, patterns never sent`, `…the i18n copies of a noindex page`, `routeReportBody: within the plugin limits, both lists`, `…the JSON body stays within the older plugin limit, noindex dropped first`, `…routes alone over the limit are cut from the end`; smoke expect-seo.txt `routereport 1 … noindex=/konto noindex=/szkic noindex=/prywatne lacksnoindex=/ lacksnoindex=/mapa-strony` | +| L4 WR-01 page-1 self-redirect | `seoFirstPageRedirect`, locale segment in SeoHtmlSitemapPage | tests/pageHref.test.ts: `seoFirstPageRedirect: page 1 goes to the unnumbered URL with the locale segment`, `…never a redirect to the URL itself`; smoke `301 /mapa-strony/posts?page=1` still passes | +| L4 WR-04 sitemap pages over a catch-all | `seoSitemapPages` catch-all rule, `htmlSitemap.overCatchAll`, build log line | tests/sitemapPages.test.ts: `a root catch-all skips every locale unless overCatchAll`, `a prefixed catch-all skips only the paths it covers`, `an app page at the path keeps it (taken)`; smoke expect-seo.txt /mapa-strony lines pass with the playground's `overCatchAll: true` | +| L4 WR-05 i18n cross-locale copies | pages added after i18n, names `___`; SitemapLink route check | tests/sitemapPages.test.ts `with i18n each route is named for its own locale, once`; built journal playground route names (below); smoke footer link line | +| L4 WR-08 root files while unknown | `rootFileTargets` targets the fixed names in state unknown | tests/rootFiles.test.ts `a backend never reached since start (unknown) still owns the fixed root files` and `an older backend (absent) passes every path through`; expect-seo-down.txt `503 /robots.txt`, `503 /sitemap.xml`, `503 /llms.txt` with `Retry-After: 60`; expect.txt (`--no-site`) keeps `404 /robots.txt` | +| L4 WR-02 post og:type and article times | `journalArticleMeta`, always set in PostPage | tests/seo.test.ts `journalArticleMeta: og:type article and the post times`; expect-acme.txt `/blog/heading-on count=property="og:type":1 …article:published_time…` (failed before the fix) and `/blog/uncategorised-note` | +| L4 WR-03 journal path-form routes | `journalPaginationSource`, computed in the extendPages callback | tests/routes.test.ts `journalPaginationSource: the SEO module's resolved option, else the raw seo.pagination` and `an app that set only form path gets the default pl and en segments`; a probe build with only `pagination: { form: 'path' }` had `/blog/strona/:page(\d+)`, `/blog/page/:page(\d+)` and the `/pl/` copies | +| L4 CR-01 link: dependency, versions, pins | `check-git-install.sh --check-links`, tag-mode guard, version fields and install pins | `check-git-install.sh --self-test` (planted link:, git+https, file:/workspace:/portal:, missing and mixed cases); `--check-links ../vue-seo-layer ../vue-journal-layer ../vue-menu-layer ../vue-faq-layer` exits 1 with the three link: lines and none for vue-seo-layer; version/pin loop prints seo 0.2.0 #v0.2.0, journal 0.8.0 #v0.8.0, menu 0.5.0 #v0.5.0, faq 0.5.0 #v0.5.0 | + +Prohibitions: the route report stays compatible both ways. The fixture API ignores `noindex`, and the smoke with the routereport line passes. `--no-site` (404, not retried) passes in expect.txt. The guard and the noindex headers are unchanged: guard:check passes (16 checks), and the expect-seo.txt `/konto`, `/szkic`, `/prywatne` noindex lines pass. + +## Journal playground route names (WR-05 evidence) + +`grep -rhoE '"seo-sitemap-[a-z-]+(___[a-z]+)*"' .playground/.output/server | sort -u` after the fix: + +``` +"seo-sitemap-en___en" /sitemap +"seo-sitemap-group-en___en" /sitemap/:group +"seo-sitemap-group-pl___pl" /pl/mapa-strony/:group +"seo-sitemap-pl___pl" /pl/mapa-strony +``` + +Before the fix the build held seo-sitemap-en___pl and seo-sitemap-pl___en. With the first `modules:done` attempt it held `___en___en` / `___en___pl` doubles (see deviation 1). + +## The 33-09 "app without pages" item + +It is gone. I built the menu playground (only `app.vue`) once without `seo: { htmlSitemap: false }`: it registered no `seo-sitemap-` route and its smoke passed (17 checks). The config was restored afterwards, and both the menu and FAQ playgrounds keep `htmlSitemap: false` as the plan asked. The vue-seo-layer README and the menu and FAQ upgrade notes now say that such an app gets no sitemap pages and needs no setting. + +## Status for deferred-items.md (owned by 33-15, not edited here) + +- **G1, layer side:** closed. Each server start sends `noindex` with the static paths the build serves noindex, including the i18n copies, under the gap-2 contract. The CMS side (the filter and the stored set) belongs to 33-15. +- **33-09 check-git-install item (no guard against link:):** closed on the script side. `--check-links` and the tag-mode check are in vue-journal-layer d4ba3e2, and `--self-test` covers them. The swap itself is still open and belongs to 33-14 Task 4. +- **33-09 "app without pages":** closed (see above). + +## For 33-14 Task 4 + +- **Link swap:** not done. `link:../vue-seo-layer` is still in vue-journal-layer, vue-menu-layer and vue-faq-layer. Step order (also in the journal README, Releasing): tag vue-seo-layer v0.2.0, then in each content layer replace the link with `git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0` and run `pnpm install`. Then run `bash ../vue-journal-layer/scripts/check-git-install.sh --check-links ../vue-seo-layer ../vue-journal-layer ../vue-menu-layer ../vue-faq-layer`, which must exit 0, then tag and push. +- **check-git-install ``:** now also fails when the installed tag's package.json has a local dependency, before building. Its `--self-test` is part of the gate's layers stage. +- **Versions:** already bumped (seo 0.2.0, journal 0.8.0, menu 0.5.0, faq 0.5.0), so Task 4 does not bump them again. +- **Release notes:** the four 33-14-notes-vue-*.md drafts changed (c2e56ef): + - seo: the route report's noindex list, the sitemap pages per locale and not over a catch-all, `overCatchAll`, the upgrade line, and the Fixed line about the root-file 503s; + - journal: path-form listing routes, plus an upgrade line on the catch-all and the article meta; + - menu and FAQ: the catch-all upgrade note replaces the `htmlSitemap: false` advice. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] The sitemap page hook is registered on `ready`, not in `modules:done`** +- **Found during:** Task 2 B (journal playground build). +- **Issue:** vue-seo-layer's module is set up before @nuxtjs/i18n, so its `modules:done` handler runs first. A `pages:resolved` hook added there ran before the one i18n adds in its own `modules:done`. i18n then localised the layer's routes again (`seo-sitemap-en___en___pl`), which is the WR-05 bug once more. +- **Fix:** the hook is added from `ready`, which Nuxt calls after every `modules:done` handler and before the build resolves the pages. The plan's acceptance grep `grep -n "modules:done" lib/seo-module.ts` now matches two comment lines that explain the choice, not the hook. +- **Files:** ../vue-seo-layer/lib/seo-module.ts. **Commit:** a651de4. + +**2. [Rule 1 - Bug] routeReportBody: routes too large means noindex is empty** +- **Found during:** Task 1 (the unit test for routes alone over the limit). +- **Issue:** my first version cut routes to fit and then filled the slack with noindex entries. That breaks contract point 6, which drops noindex first. +- **Fix:** when routes alone exceed the limit, noindex is `[]`. **Commit:** 7a26692. + +**3. [Rule 1 - Docs] README notes that were wrong after the fix** +- The vue-seo-layer README `htmlSitemap` row and the menu and FAQ "Upgrading from 0.4" bullets said that an app with only `app.vue` must set `htmlSitemap: false`. They now describe the new behaviour. +- The journal README "Fallback meta" bullet no longer lists the article times, which are now always set. +- **Commits:** a651de4, 839db51, a7602c9, 5e5950a. + +**4. Minor wording/placement choices** +- **`--check-links` line:** it reads `check-git-install: : depends on through (
)` instead of `: depends on `, one wording for both modes. +- **SeoHtmlSitemapPage form:** the redirect form comes from the route (path when `:page` is set, else query) instead of from the site answer. A path-form route under a query-form site answer would otherwise self-redirect too. +- **Journal "Upgrading from 0.7":** besides the catch-all and og:type notes, it also mentions the path-form segments. + +## Known Stubs + +None. + +## Threat Flags + +None. No new endpoint or auth path. The route report carries one more list on the existing request, as recorded in T-33-77. + +## Gate + +`bash scripts/check-phase33.sh --self-test --layers` from summercms.go, exit 0. Log: /tmp/claude-1000/-media-nvme-dev-golem15-summercms-io-summercms-summercms-go/cc746d7e-76ce-49f4-ae43-0e1debb2bbea/scratchpad/gate-33-16.log + +- `PASS self-test` (45 checks, `phase33 self-test passed`) +- `PASS layers` (`phase33 layers passed`): + - unit tests: vue-seo-layer 152/152, vue-journal-layer 202/202, vue-menu-layer 24/24, vue-faq-layer 19/19 (tap, `# fail 0`); + - the four playground builds; + - vue-seo-layer `smoke:ssr` (7 runs), `guard:check`, `purge:check`; + - the five journal smoke runs, its purge-check and purge-token-check, and `check-git-install: self-test ok`; + - the menu and FAQ `smoke:ssr`. + +The go, plugins, docker and consumers stages were not run. The orchestrator runs `--all` after both SUMMARYs exist. + +Task verifies run separately along the way: +- vue-seo-layer: `smoke:ssr` all seven runs ok (expect-seo.txt 64 checks, expect-seo-down.txt 9), guard:check 16, purge:check 18, purge-token-check 53. +- vue-journal-layer: `smoke:ssr` five runs ok (expect-acme.txt 89 checks), plus the built route-name check above. +- `--check-links` over the four layers: exit 1 with three link: lines (journal, menu, FAQ), as required until 33-14 Task 4. + +## Self-Check: PASSED + +Both created files exist; all 13 commits exist in their repos; the plan's acceptance greps match (noindexStaticRoutes, ROUTE_REPORT_MAX_BODY in code and tests, noindexPaths in plugin and types, the README contract JSON, `^503 /robots.txt`, seoFirstPageRedirect, overCatchAll in README and playground, og:type lines in expect-acme.txt, --check-links in script and README); the `modules:done` grep matches comments only (deviation 1).