From c2357cd7f9a83911f0c54da422fdaee1f0383cce Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 09:29:02 +0100 Subject: [PATCH] docs: README - options (cms alias of public.seo.enabled, breadcrumbs, links.newTabHint, rootFiles, resolveFiles), the new runtime keys and the moved purge keys - the resolve and site answer contract, one JSON-LD graph, root files, the purge route, 404 report, labels and tokens, the smoke runs --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 79bf098..daf3ded 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # vue-seo-layer -The Nuxt half of the SummerCMS SEO plugin ([sm-seo-plugin](https://git.golem15.com/golem15/sm-seo-plugin), plugin ID `golem15.seo`). For every page it asks the plugin for the redirect decision and the effective SEO, enforces the decision, and sets the head from the answer. It also reports 404s to the plugin's 404 log and serves the verification files at the site root. +The Nuxt half of the SummerCMS SEO plugin ([sm-seo-plugin](https://git.golem15.com/golem15/sm-seo-plugin), plugin ID `golem15.seo`). For every page it asks the plugin for the redirect decision and the effective SEO, enforces the decision, and sets the head from the answer, including hreflang and one JSON-LD `@graph`. It also serves the site's root files from the backend (verification files, and with SEO v0.2.0 robots.txt, the sitemaps, llms.txt and the IndexNow key), reports 404s to the plugin's 404 log, and owns the shared site cache purge route. + +## Install + +```json +"vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0" +``` ```ts // nuxt.config.ts of the app @@ -9,48 +15,86 @@ export default defineNuxtConfig({ }) ``` -```json -"vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.1.1" -``` +Pin a tag; the features marked "v0.2.0" below need that tag and, where they read new answer keys, the SEO plugin's v0.2.0 backend. With an older backend the layer behaves as v0.1.1. -The layer declares no Nuxt modules and works next to `@nuxtjs/seo`, the Journal layer or an app's own `useSeoMeta`. +The layer's only module is its own local one (`lib/seo-module.ts`); it pulls in no package and no `@nuxtjs/i18n`. It works next to `@nuxtjs/seo`, the Journal layer or an app's own `useSeoMeta`; its options share the `seo` config key with nuxt-seo-utils and use none of its names. ## What it does -- **Resolve.** A global route middleware calls `GET /resolve?url=` before the page runs, on the server for the first request and in the browser on each client-side navigation (the server's answer travels in the payload, so hydration fetches nothing). Nuxt's own paths (`/_…`, `/api/…`) are never resolved. A call that fails, times out (`timeoutMs`) or answers anything unexpected is ignored: the page renders with its own head. -- **Redirects.** `redirect` answers 301 or 302 with `Location`. A target on the site's own origin (the origin of the answer's `url`) becomes a path, so a staging or preview host keeps the visitor even when the backend's site URL names production; a target on another host stays absolute. The redirect wins over the route, so an old address no page matches still redirects. +- **Resolve.** Every page request is resolved with `GET /resolve?url=` before Nuxt renders it, in a Nitro middleware, so the render uses the same answer and makes no second call. Answers are cached (v0.2.0): path and query as the key, `seo.resolveCacheTtl` seconds (default 60), at most 1000 entries, and concurrent requests for one path share one call; the site cache purge empties the cache. In the browser a global route middleware resolves each client-side navigation with the public base (the server's answer travels in the payload, so hydration fetches nothing). Nuxt's own paths (`/_…`, `/api/…`) and asset files are never resolved by the middleware (files with an extension in `resolveFiles`, default `pdf`, are, so an old PDF address can redirect); the route middleware still resolves a page under a dotted path. A call that fails, times out (`timeoutMs`) or answers anything unexpected is ignored: the page renders with its own head (fail open). +- **Redirects.** `redirect` answers 301 or 302 with `Location`, before the page and before Nitro's route cache, so a redirect added for an `swr`-cached page applies on the next request after the resolve cache entry expires or the site cache is purged. A target on the site's own origin (the origin of the answer's `url`) becomes a path, so a staging or preview host keeps the visitor even when the backend's site URL names production; a target on another host stays absolute. Nothing from the request (Host, X-Forwarded-Host, path, query) goes into the Location. The redirect wins over the route, so an old address no page matches still redirects. - **410.** `gone` answers 410 with the app's error page, also on a route that would render. -- **Head.** An `ok` answer sets the title (final; the plugin applies its templates, so the app's `titleTemplate` is replaced by `%s`), `description`, `canonical`, `robots`, `og:title`, `og:description`, `og:image`, `og:url`, `twitter:card`, `twitter:site` and the verification meta tags. The entry has `tagPriority: 'critical'`, so its tags win over the same tags set by a page, a layer or a module; a value the answer leaves empty adds no tag, and the page's own stays. A canonical, `og:url` or `og:image` the plugin answers as a path (a backend without `golem15.seo.site_url` or `app.url`) is made absolute on the request's origin; set the site URL on the backend so it names the public origin. `not_found` answers (pages the plugin does not know: listings, search, author pages, static pages without a URL rule) leave the head to the page. -- **Robots and staging.** A `staging` answer (the backend outside `SUMMER_ENV=production`) makes every page `noindex, nofollow`, known or not. The `X-Robots-Tag` response header follows the same value. -- **404 log.** Each page view answered 404 (a GET that accepts `text/html`, from an unknown route or a page that throws a 404) is reported with `POST /notfound`, `{"url", "referer"}` and `Authorization: Bearer `, after the response. Without a token nothing is reported. -- **Verification files.** A request for a single-segment `.html`, `.txt` or `.xml` file (`/google0123.html`, `/BingSiteAuth.xml`) is looked up under `GET /verification/` and served with `X-Content-Type-Options: nosniff`, a sandboxing CSP and `X-Robots-Tag: noindex` when the plugin has it; otherwise it falls through to the app's routes. `robots.txt`, `sitemap.xml`, `sitemap.xsl`, `rss.xml`, `feed.xml`, `atom.xml` and `llms.txt` are never looked up. +- **404 for a bad page number (v0.2.0).** A `not_found` answer with `pagination.invalid` (`?page=abc`) is a 404. Every other `not_found` is left to Nuxt's routing and the pages. +- **Head.** An `ok` answer sets the title (final; the plugin applies its templates, so the app's `titleTemplate` is replaced by `%s`), `description`, `canonical`, `robots`, `og:title`, `og:description`, `og:image`, `og:url`, `twitter:card`, `twitter:site` and the verification meta tags, and from v0.2.0 answers `` (`html_lang`), `og:locale`, one `og:locale:alternate` per value, one `` per entry of `alternates`, and exactly one `