diff --git a/.planning/phases/33-seo-output-audit-and-content/33-07-SUMMARY.md b/.planning/phases/33-seo-output-audit-and-content/33-07-SUMMARY.md new file mode 100644 index 00000000..8eb9d1db --- /dev/null +++ b/.planning/phases/33-seo-output-audit-and-content/33-07-SUMMARY.md @@ -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-.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-.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 '

Ścieżka nawigacji

'; 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 ``, 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`; `` 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. diff --git a/.planning/phases/33-seo-output-audit-and-content/deferred-items.md b/.planning/phases/33-seo-output-audit-and-content/deferred-items.md new file mode 100644 index 00000000..0a563fe6 --- /dev/null +++ b/.planning/phases/33-seo-output-audit-and-content/deferred-items.md @@ -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.