docs(33-07): complete vue-seo-layer runtime plan

- SUMMARY: Task 1 answer (vue-seo-layer), Jakub's extend-in-place ruling,
  v0.1.1 compatibility evidence, deviations; next layer tag v0.2.0
- deferred-items: path canonicals under swr (pre-existing in v0.1.1)
This commit is contained in:
Jakub Zych
2026-11-04 09:35:51 +01:00
parent b6180a7dac
commit 94f3699cc4
2 changed files with 358 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
---
phase: 33-seo-output-audit-and-content
plan: 07
subsystem: vue-seo-layer
tags: [nuxt-layer, nitro-middleware, json-ld, hreflang, lru-cache, root-files, site-cache-purge, labels, css-tokens]
requires:
- phase: 32-seo-foundation
provides: GET /_seo/api/v1/resolve answer (action, status, location, url, locale, staging, seo), POST /notfound, verification files
- phase: 33-01
provides: version ruling (phase targets SummerCMS v0.6.1, base v0.6.0)
provides:
- vue-seo-layer runtime on top of v0.1.1 (repository ../vue-seo-layer, branch main, 7 commits, not pushed, not tagged)
- Nitro middleware server/middleware/seo.ts (resolveCached, redirect/410/X-Robots-Tag, pagination.invalid 404, event.context.seo)
- useCmsSeo (html lang, hreflang, og:locale, one JSON-LD @graph), useSchemaNode, mergeGraph, isPagePath
- resolve LRU (seo.resolveCacheTtl, 1000 entries, in-flight de-duplication) and the site answer cache (server/lib/site.ts)
- shared POST /api/__site__/cache/purge moved in (library and plugin byte-identical, route also clears the SEO caches) plus a purge hook for the transition
- root-file proxies server/middleware/seo-root.ts (robots.txt, sitemap.xml, sitemap-<name>.xml, llms.txt, listed verification and IndexNow key files)
- local module lib/seo-module.ts (configKey seo; cms, breadcrumbs, links.newTabHint, rootFiles, resolveFiles)
- useSeoLabels / pickLabels / formatSeoLabel (L8 en, pl), seo-tokens.css, seo-base.css
- smoke harness (count=, exact header=, noheader=, reqheader=, repeatable options, resolves/apicalls/noreport lines, --no-site), fixture API (site, root files, resolve counts, reload), purge-check.mjs, purge-token-check.mjs
affects: [33-08, 33-09, 33-12, 33-13, 33-14]
actuals:
tokens: 42900
tasks: 4
commits: 7
plan_head_before: 7d52c5d2e50fb5870887751adfdcd5e78512a271
plan_head_after: 6636cde70a6d866e6d7511c24f5d3b70258455f9
tech-stack:
added: []
patterns:
- "Resolve once per request in Nitro middleware, hand the answer to the render through event.context.seoResolved; the route middleware reuses it on the server and resolves only on client navigation"
- "New behaviour gated on the backend's v0.2.0 site answer: a backend whose /site answers 404, or one never reached, keeps the v0.1.1 behaviour"
- "LRU with generation counter: a clear detaches loads in flight, so nothing fetched before a purge is stored after it"
- "Smoke runs per scenario (v0.1.1 suite against --no-site, full, off via both switches, backend down, no token)"
key-files:
created:
- ../vue-seo-layer/lib/seo-module.ts
- ../vue-seo-layer/server/middleware/seo.ts
- ../vue-seo-layer/server/middleware/seo-root.ts
- ../vue-seo-layer/server/lib/config.ts
- ../vue-seo-layer/server/lib/resolve.ts
- ../vue-seo-layer/server/lib/lru.ts
- ../vue-seo-layer/server/lib/site.ts
- ../vue-seo-layer/server/lib/siteCachePurge.ts
- ../vue-seo-layer/server/api/__seo__/resolve.get.ts
- ../vue-seo-layer/server/api/__site__/cache/purge.post.ts
- ../vue-seo-layer/server/plugins/site-cache-purge.ts
- ../vue-seo-layer/server/plugins/seo-cache-purge.ts
- ../vue-seo-layer/app/composables/useCmsSeo.ts
- ../vue-seo-layer/app/composables/useSchemaNode.ts
- ../vue-seo-layer/app/composables/useSeoLabels.ts
- ../vue-seo-layer/app/utils/graph.ts
- ../vue-seo-layer/app/utils/pagePath.ts
- ../vue-seo-layer/app/utils/labels.ts
- ../vue-seo-layer/app/app.config.ts
- ../vue-seo-layer/app/assets/css/seo-tokens.css
- ../vue-seo-layer/app/assets/css/seo-base.css
- ../vue-seo-layer/testing/nuxt-server.mjs
- ../vue-seo-layer/testing/purge-check.mjs
- ../vue-seo-layer/testing/purge-token-check.mjs
- ../vue-seo-layer/testing/expect-seo.txt
- ../vue-seo-layer/testing/expect-seo-off.txt
- ../vue-seo-layer/testing/expect-seo-down.txt
- ../vue-seo-layer/testing/expect-seo-notoken.txt
- ../vue-seo-layer/testing/fixtures/seo/resolve.json
- ../vue-seo-layer/testing/fixtures/seo/site.json
- ../vue-seo-layer/tests/graph.test.ts
- ../vue-seo-layer/tests/pagePath.test.ts
- ../vue-seo-layer/tests/lru.test.ts
- ../vue-seo-layer/tests/labels.test.ts
- ../vue-seo-layer/tests/sitePurge.test.ts
- ../vue-seo-layer/tests/rootFiles.test.ts
modified:
- ../vue-seo-layer/nuxt.config.ts
- ../vue-seo-layer/package.json
- ../vue-seo-layer/README.md
- ../vue-seo-layer/app/plugins/seo-head.ts
- ../vue-seo-layer/app/middleware/seo.global.ts
- ../vue-seo-layer/app/composables/useSeoResolve.ts
- ../vue-seo-layer/app/utils/seo.ts
- ../vue-seo-layer/app/utils/types.ts
- ../vue-seo-layer/server/middleware/seo-verification.ts
- ../vue-seo-layer/server/plugins/seo-notfound.ts
- ../vue-seo-layer/testing/smoke-ssr.mjs
- ../vue-seo-layer/testing/fixture-api.mjs
- ../vue-seo-layer/tests/seo.test.ts
- ../vue-seo-layer/.playground/nuxt.config.ts
key-decisions:
- "Task 1 (Jakub 2026-11-04): the shared layer's name is vue-seo-layer"
- "Option 1 (Jakub 2026-11-04): extend the existing v0.1.1 repository in place (no git init, no re-scaffold, remote origin kept, never pushed or tagged); backward compatibility with v0.1.1 mandatory"
- "33-14 does not create the repository with tea; the layer's next tag is v0.2.0"
- "Module option seo.cms is an alias of public.seo.enabled (NUXT_PUBLIC_SEO_ENABLED); either one false switches the layer off"
- "Redirect Location keeps the v0.1.1 rule: same-site targets become paths, other hosts stay absolute; built from the answer only"
- "Client navigation keeps public.seo.apiBase (/_seo/api/v1); GET /api/__seo__/resolve is opt-in by setting it to /api/__seo__"
- "Root files and the root_files-only verification rule apply only once the backend gave a v0.2.0 site answer; otherwise v0.1.1 behaviour"
patterns-established:
- "Compatibility suite: testing/expect.txt (v0.1.1) runs unchanged against a backend without /site on every smoke run"
requirements-completed: [SC-2, D-97, D-20, D-20a, D-20b, D-20c, D-08, D-05, D-103, D-43, D-55, D-71, D-22, D-25]
coverage:
- id: D1
description: "A Nuxt page extending the layer renders the CMS title, canonical, hreflang, html lang, og:locale and exactly one JSON-LD script with one @graph (CMS nodes plus layer nodes merged by @id; '<' escaped)"
requirement: "D-43"
verification:
- kind: integration
ref: "npm run smoke:ssr (testing/expect-seo.txt tracer lines)"
status: pass
- kind: unit
ref: "tests/graph.test.ts, tests/seo.test.ts#seoHeadInput: v0.2.0 keys"
status: pass
human_judgment: false
- id: D2
description: "Redirect (301/302, Location from the answer only, crafted Host ignored), 410, X-Robots-Tag and the pagination.invalid 404 enforced in Nitro middleware before the render and the route cache"
requirement: "D-20a"
verification:
- kind: integration
ref: "npm run smoke:ssr (expect-seo.txt redirect/410/404 lines); testing/purge-check.mjs step 8 (redirect on a route-cached page)"
status: pass
human_judgment: false
- id: D3
description: "Resolve LRU (TTL, 1000 entries, in-flight de-duplication, purge-safe) and the moved site cache purge route clearing route cache, resolve and site caches"
requirement: "D-97"
verification:
- kind: unit
ref: "tests/lru.test.ts, tests/sitePurge.test.ts"
status: pass
- kind: integration
ref: "npm run purge:check (purge-check 18 checks, purge-token-check 53 checks); smoke 'resolves /blog/lwia-grzywa 1'"
status: pass
human_judgment: false
- id: D4
description: "Root files from the backend (robots.txt, sitemap.xml, sitemap-<name>.xml, llms.txt, listed verification and IndexNow key files) with status, content type, X-Robots-Tag; 503 Retry-After 60 on backend failure; other paths pass through"
requirement: "D-05"
verification:
- kind: unit
ref: "tests/rootFiles.test.ts"
status: pass
- kind: integration
ref: "npm run smoke:ssr (expect-seo.txt root lines, expect-seo-down.txt)"
status: pass
human_judgment: false
- id: D5
description: "404 report once with the bearer token; none with the layer off or without a token"
requirement: "D-20"
verification:
- kind: integration
ref: "npm run smoke:ssr (report/noreport lines in expect-seo.txt, expect-seo-off.txt, expect-seo-notoken.txt)"
status: pass
human_judgment: false
- id: D6
description: "L8 labels (en, pl, app overrides), seo-tokens.css and seo-base.css (only .seo-* classes), no i18n dependency"
verification:
- kind: unit
ref: "tests/labels.test.ts"
status: pass
- kind: integration
ref: "smoke line '<p class=\"seo-visually-hidden\">Ścieżka nawigacji</p>'; plan acceptance greps"
status: pass
human_judgment: false
- id: D7
description: "v0.1.1 behaviour and config keys preserved for consumers"
verification:
- kind: integration
ref: "testing/expect.txt (31 v0.1.1 checks) on every smoke run; expect-seo-off.txt with NUXT_PUBLIC_SEO_ENABLED=false; scratch build of vue-grzybyfunkcjonalne-app against this layer: its smoke 52/52, smoke:ga 2/2, unit 28/28"
status: pass
human_judgment: false
duration: 25min
completed: 2026-11-04
status: complete
---
# Phase 33 Plan 07: vue-seo-layer runtime Summary
**The existing vue-seo-layer v0.1.1 now resolves each page once in Nitro middleware with a purge-safe LRU, enforces redirect, 410 and X-Robots-Tag before the route cache, renders hreflang and one merged JSON-LD @graph, serves the CMS's root files, owns the shared site-cache purge route and ships the L8 labels and tokens. Every v0.1.1 behaviour and config key still works.**
## Task 1 answer and Jakub's ruling (2026-11-04)
- **Task 1 (checkpoint:decision), answered:** the layer's name is **vue-seo-layer**.
- **The plan was stale.** It assumed `../vue-seo-layer` did not exist. In fact the repository was created and published on 2026-10-31, outside this phase: remote `git@git.golem15.com:golem15/vue-seo-layer.git`, tags v0.1.0 and v0.1.1, branch main with 2 commits (ac59f3c, 7d52c5d). It is consumed by `../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app` pinned at `#v0.1.1`.
- **Jakub's ruling (option 1): extend the existing layer in place.** So:
- no `git init`, no recreate or re-scaffold, no file deleted, work stayed on main;
- the acceptance criterion "no remote" is void (origin exists);
- nothing was pushed or tagged.
- **Backward compatibility with v0.1.1 is mandatory.** The plan's new keys were added alongside the existing ones. Where a plan key duplicates an existing one, the existing key keeps working and the new one is an alias: `seo.cms` is an alias of `public.seo.enabled`, and either one false switches the layer off.
- **Commit subjects.** The plan's subjects were used. No scaffold commit exists, so the tracer subject describes the actual change.
- **For later plans:**
- **33-14** does NOT create the repository with tea.
- **The layer's next tag is v0.2.0.** `package.json` still says 0.1.1; bump it when tagging in 33-14. The README already shows the `#v0.2.0` pin and marks new features "v0.2.0".
- **Framework context (33-01):** the phase targets SummerCMS v0.6.1 on base v0.6.0.
- **No planned change needed a checkpoint:decision.** Every planned change could be made without changing observable v0.1.1 behaviour. One regression I introduced myself was found by the consumer build and fixed (deviation 6).
### How v0.1.1 compatibility was verified
1. **The v0.1.1 suite is unchanged.** `testing/expect.txt` (31 checks: head priority, empty values keep page tags, staging noindex, redirects, 410, 404 reports, verification files, `404 /robots.txt`, relative canonicals) runs on every `npm run smoke:ssr` against a backend whose `/site` answers 404, as v0.1.1's backend does. It passes on the final commit.
2. **Both switches tested.** `testing/expect-seo-off.txt` runs once with the new `NUXT_PUBLIC_SEO_CMS=false` and once with the v0.1.1 `NUXT_PUBLIC_SEO_ENABLED=false`. Both give zero API calls, the pages' own head, no redirect, no root files and no report.
3. **Consumer build.** In a scratchpad copy of `vue-grzybyfunkcjonalne-app`, its node_modules copy of vue-seo-layer was replaced with `git archive HEAD` of this layer. The real repository was not touched. I ran `nuxt build`, then its `smoke:ssr` (52/52), `smoke:ga` (2/2) and `npm test` (28/28). This found deviation 6, which is fixed. The consumer build also runs next to `@nuxtjs/seo`, which shares the `seo` config key, without errors.
4. **Config keys kept with v0.1.1 semantics:** `seo.apiBase`, `seo.notfoundToken`, `seo.timeoutMs`, `public.seo.apiBase` (still the browser's resolve base) and `public.seo.enabled`. The exported helpers `useSeoAnswer`, `resolveSeo`, `seoHeadInput` and the rest of `app/utils/seo.ts` keep their signatures. The new head keys are only added when the answer carries them, so an old backend gives the same tags.
## Performance
- **Duration:** 25 min
- **Started:** 2026-11-04T08:07:26Z
- **Completed:** 2026-11-04T08:32:38Z
- **Tasks:** 4 (Task 1 answered, Tasks 2 to 4 executed)
- **Files modified:** 57 in ../vue-seo-layer
## Accomplishments
- **Nitro middleware** resolves each page request once through the LRU, forwarding X-Forwarded-For. It handles redirect, 410, X-Robots-Tag and the pagination.invalid 404 before the render and the route cache. The render reuses the answer, so a page costs one resolve call; the smoke checks this with `resolves PATH 1`.
- **One head owner** (seo-head plugin → `useCmsSeo`). It sets the v0.1.1 tags plus `<html lang>`, og:locale and og:locale:alternate, hreflang links, and exactly one JSON-LD script with one @graph. The graph merges CMS nodes and `useSchemaNode` nodes by @id, with CMS values winning.
- **Shared purge route** moved in with the same contract. It also empties the resolve and site caches. A purge hook does the same when the journal, menu or FAQ layer's copy of the route answers instead (Pitfall 6 during the transition to 33-09).
- **Root-file proxies** with status, content type and X-Robots-Tag passed on, and 503 with Retry-After 60 on backend failure. They are gated on the v0.2.0 site answer.
- **L8 labels, tokens and base styles**, a README covering everything, and 67 unit tests.
- **Six smoke runs** (93 checks) plus the purge checks (18 + 53).
## Task Commits
All in /media/nvme/dev/golem15/summercms.io/summercms/vue-seo-layer (branch main, not pushed):
1. **Task 1: name the layer.** No commit; answered: vue-seo-layer.
2. **Task 2: tracer.** `160b53b` feat: CMS head and one JSON-LD graph on every server-rendered page (tracer)
3. **Task 3:**
- A `e3baa70` feat: redirect and 410 enforcement, X-Robots-Tag and the resolve cache
- B `691fa09` feat: shared site cache purge route clears the SEO caches
- C `d162943` feat: root files from the CMS and the 404 report
4. **Task 4:**
- A `aa7480e` feat: SEO labels, tokens and base styles
- B `c2357cd` docs: README
5. **Deviation fix:** `6636cde` fix: root files fall through until the backend has given a site answer
## Decisions Made
See key-decisions in the frontmatter. In short:
- **seo.cms** is an alias of public.seo.enabled.
- **Redirect Location** keeps the v0.1.1 same-origin-to-path rule.
- **The browser** keeps public.seo.apiBase; the new `/api/__seo__/resolve` is opt-in.
- **Root files** depend on a confirmed v0.2.0 site answer.
## Deviations from Plan
### Ruling-driven (Jakub, option 1)
**1. The repository exists.**
- **What changed:** no `git init`, no scaffold, remote kept, "no remote" criterion void.
- **Fixture layout:** the v0.1.1 fixtures (`testing/fixtures/resolve.json`, `verification/`) and `testing/expect.txt` were kept as they are. The new answers live in `testing/fixtures/seo/`, and the fixture API merges both.
### Compatibility-driven (Rule 2, to keep v0.1.1 behaviour)
**2. Redirect Location.**
- **Plan:** "forward the resolver's absolute location" (`/stary` → `https://example.test/nowy`).
- **What I did:** kept v0.1.1's `seoRedirectTarget`. A same-site target becomes a path (`/nowy`); another host stays absolute.
- **Prohibition still holds:** the Location never uses request input. The smoke sends a crafted Host and X-Forwarded-Host, and `server/middleware/seo.ts` has no `getRequestHost` or `x-forwarded-host`.
**3. Client-navigation resolve.**
- **Plan:** fetch through `/api/__seo__/resolve`.
- **What I did:** the browser still uses `public.seo.apiBase`, whose v0.1.1 meaning is the same-origin resolve base. The `/api/__seo__/resolve` route exists, goes through the server LRU, has `cache: false` and noindex, and is used when the app sets `public.seo.apiBase` to `/api/__seo__`.
**4. Head application.**
- **Plan:** `seo-head.ts` takes the answer from `useRequestEvent().context.seo`.
- **What I did:**
- The v0.1.1 global route middleware stays. On the server it reuses `event.context.seoResolved` (no second call). It still resolves paths the Nitro middleware skips (dotted page paths) and client navigations.
- The canonical link has no explicit `key`. unhead's own canonical de-duplication then keeps one canonical even when a page sets its own, which v0.1.1's `/about` check relies on.
**5. Verification files.**
- **Plan:** only names listed in `root_files` are proxied; others pass through.
- **What I did:** that rule applies when the backend gives a site answer. With a backend whose `/site` answers 404 (v0.1.1 era) or one never reached, `seo-verification.ts` keeps the v0.1.1 name lookup. robots.txt and the sitemaps are not proxied then, matching v0.1.1's `404 /robots.txt`.
- **Backend base:** `seo.backendBase` defaults to empty, which means `apiBase` without `/_seo/api/v1`. That is effectively `http://127.0.0.1:8096`, and an app that set only `NUXT_SEO_API_BASE` needs nothing new.
**6. [Rule 1 - Bug] Root files answered 503 while the backend was unreachable since start.**
- **Found during:** the post-plan consumer build. vue-grzybyfunkcjonalne-app's smoke expects `404 /robots.txt` and `404 /sitemap.xml` with the SEO API down.
- **Fix:** root proxies need a site answer (state ok, or the last one seen while the backend is down), so a cold start with the backend down falls through as in v0.1.1. The plan's 503 still applies once a site answer was seen: `503 /sitemap-fail-1.xml` in expect-seo.txt, and expect-seo-down.txt now expects the v0.1.1 404s.
- **Files:** server/lib/site.ts, server/middleware/seo-root.ts, tests/rootFiles.test.ts, testing/expect-seo-down.txt, README.md
- **Commit:** 6636cde
### Other adjustments
**7. Purge route copy and its transition hook.**
- **Byte-identical:** the purge library and the site-config plugin match the journal, menu and FAQ copies.
- **Not byte-identical:** the route adds `clearSeoCaches()`; its pinned hash in `tests/sitePurge.test.ts` is updated.
- **Added `server/plugins/seo-cache-purge.ts`** (an afterResponse hook on a 200 POST to `/api/__site__/cache/purge` or `/api/__journal__/cache/purge`). In an app that also extends journal, menu or FAQ, Nitro may run their copy, which does not clear the SEO caches (Pitfall 6). 33-09 removes those copies; the hook can stay, since clearing twice is harmless.
**8. purge-check.mjs.**
- **Changed:** it changes the fixture resolve title (as planned). It drops the journal-only checks: browser `max-age=0` comes from vue-journal-layer's journal-revalidate plugin, which was not moved, and `_payload.json` carries no SEO data.
- **Added:** step 8, where a redirect added for a route-cached page applies at once with `NUXT_SEO_RESOLVE_CACHE_TTL=0`.
- **Unchanged:** all other checks of both scripts.
**9. Playground route cache.**
- **Plan:** `'/**': { swr: 300 }`. **Used:** `'/blog/**'`.
- **Why:** a route-cached render has no Host header, so v0.1.1's `/relative` check (a path canonical made absolute on the request origin) needs uncached pages. Pre-existing behaviour, see Deferred.
**10. Smoke harness grammar.**
- **header=:** now means "equals", per P33-29. All v0.1.1 lines still pass.
- **Added:** `noheader=`, `reqheader=`, `noreport`, `resolves PATH N`, `apicalls N` and `--no-site`.
- **Runs:** `smoke:ssr` runs six scenarios instead of one; `purge:check` script added.
- **Boot probe:** `testing/nuxt-server.mjs` probes `/_nuxt/` so the boot never resolves a page.
**11. TDD gate.**
- **Not done:** no separate RED commits.
- **Why:** the plan requires `npm run test` green at every commit.
- **What was done:** tests were written and run failing before each implementation (`lru.test.ts`, `labels.test.ts`: ERR_MODULE_NOT_FOUND, plus a deliberate run with `clearSeoCaches()` removed failing three sitePurge tests). Each was then committed with its implementation in the plan's commit groups.
## TDD Gate Compliance
- RED observed locally (not committed) for `tests/lru.test.ts`, `tests/labels.test.ts` and the sitePurge cache-clear tests.
- GREEN in commits e3baa70, 691fa09 and aa7480e.
- No refactor commits.
- A separate `test(...)` commit was not made because the plan requires a green test suite at every commit (deviation 11).
## Known Stubs
Intentional forward options, wired in later plans and not blocking this plan's goal:
- **`lib/seo-module.ts` `breadcrumbs`** (default true): reaches `runtimeConfig.public.seo.breadcrumbs`; `<SeoBreadcrumbs>` arrives in 33-08.
- **`lib/seo-module.ts` `links.newTabHint`** (default true): reaches `runtimeConfig.public.seo.links.newTabHint`; the body-link pipeline is 33-09.
- **`nuxt.config.ts` `seo.routeReportToken`:** read by `seoServerConfig`; the route report is 33-08.
- **`app/assets/css/seo-base.css`:** only `.seo-visually-hidden` and the focus ring so far. Breadcrumb, pagination and sitemap rules land with their components in 33-08.
## Deferred Issues
- **Pre-existing in v0.1.1, out of scope:** on a route-cached (swr) render the request has no Host, so a canonical, og:url or og:image that the backend answers as a path becomes absolute on `http://localhost`. It does not affect backends with `golem15.seo.site_url` or `app.url` set, which the README already requires. Logged in deferred-items.md.
## Threat Flags
| Flag | File | Description |
|------|------|-------------|
| threat_flag: new-endpoint | ../vue-seo-layer/server/api/__seo__/resolve.get.ts | Public GET /api/__seo__/resolve?url= returns the same public resolve answer the backend's /_seo/api/v1/resolve gives. Only path URLs up to 2048 characters; goes through the LRU, never route-cached, noindex, 404 when the layer is off. |
| threat_flag: proxy | ../vue-seo-layer/server/middleware/seo-root.ts | Root-file proxy (T-33-32): targets are the server-only bases plus an allow-listed name (exact names, `sitemap-[a-z0-9-]+.xml`, names listed in root_files); covered by tests/rootFiles.test.ts including traversal inputs. |
## Self-Check: PASSED
- All key files exist in ../vue-seo-layer (checked after writing).
- Commits 160b53b, e3baa70, 691fa09, d162943, aa7480e, c2357cd and 6636cde are present on main in ../vue-seo-layer.
- `npm run test`: 67 pass, 0 fail.
- `npm run smoke:ssr`: six runs ok (31, 36, 9, 9, 6, 2 checks).
- `npm run purge:check`: purge-check ok (18), purge-token-check ok (53).
- Plan acceptance greps:
- `sendRedirect` appears once in seo.ts; no `getRequestHost` or `x-forwarded-host`.
- expect-seo.txt has more than 5 `header=` or `count=` lines.
- `application/ld+json` appears once in useCmsSeo.ts.
- package.json has no `"dependencies"` and no i18n.
- Both the `--seo-target-size` and `--seo-space-5` tokens are present.
- No bare-element rules in seo-base.css.

View File

@@ -0,0 +1,9 @@
# Phase 33 deferred items
Out-of-scope discoveries logged during execution; not fixed in the plan that found them.
## From 33-07 (vue-seo-layer)
- **Path canonicals under swr become `http://localhost/...` (pre-existing since v0.1.1).** A route-cached (swr) render runs on a copy of the request without the Host header. `seoAbsolute(value, useRequestURL().origin)` then makes a canonical, og:url or og:image that the backend answers as a path (a backend without `golem15.seo.site_url` or `app.url`) absolute on `http://localhost`. Sites with the site URL set on the backend are not affected; the layer README already requires it.
- **Possible fix:** record the request origin in `event.context` in the `request` hook, as `seo-client-address.ts` does for the visitor address, and use it in `seoHeadInput`.
- **Why deferred:** it changes v0.1.1 output, so it needs a decision. The 33-07 playground keeps `'/blog/**'` swr instead of `'/**'` so the v0.1.1 `/relative` check stays meaningful.