docs(33-16): complete the Nuxt layers gap-closure plan

This commit is contained in:
Jakub Zych
2026-11-04 22:58:07 +01:00
parent c2e56ef339
commit a0ac2a1ee7

View File

@@ -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 <name><routesNameSeparator><locale>"
- "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: <dir>: depends on <name> through <spec> (<section>)`, 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 <base>..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 `<name>___<locale>`; 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 `<tag>`:** 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: <dir>: depends on <name> through <spec> (<section>)` instead of `<dir>: <name> depends on <spec>`, 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).