# 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. ```ts // nuxt.config.ts of the app export default defineNuxtConfig({ extends: ['vue-seo-layer'], }) ``` ```json "vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.1.0" ``` The layer declares no Nuxt modules and works next to `@nuxtjs/seo`, the Journal layer or an app's own `useSeoMeta`. ## 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. - **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. `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. - **Visitor address.** The server calls forward the visitor's `X-Forwarded-For` plus the address of the peer that reached Nuxt, also on route-cached (`swr`) renders, so the plugin's per-IP throttles (`seo-resolve`, `seo-notfound`, 600 a minute) keep one bucket per visitor. List the Nuxt server's address in the backend's `http.trusted_proxies`. ## Configuration | Runtime config | Env | Default | Meaning | |---|---|---|---| | `seo.apiBase` | `NUXT_SEO_API_BASE` | `http://127.0.0.1:8096/_seo/api/v1` | Server-only base of the plugin's API for SSR calls and 404 reports. | | `seo.notfoundToken` | `NUXT_SEO_NOTFOUND_TOKEN` | `""` | The backend's `golem15.seo.notfound_token`. Empty: 404s are not reported. Server-only; not under `NUXT_SITE_*`, which nuxt-site-config publishes in every page's payload. | | `seo.timeoutMs` | `NUXT_SEO_TIMEOUT_MS` | `1500` | How long a call may take before the page renders without it. | | `public.seo.apiBase` | `NUXT_PUBLIC_SEO_API_BASE` | `/_seo/api/v1` | Same-origin base for client-side navigations. The site's proxy must route `GET /resolve` to the backend. | | `public.seo.enabled` | `NUXT_PUBLIC_SEO_ENABLED` | `true` | `false` turns the layer off: no calls, no head, no redirects, no reports. | The plugin serves `/robots.txt` itself; route that path to the backend (and turn off another robots.txt route, such as the one of `@nuxtjs/robots`). ## Composables and utils - `useSeoAnswer()`: the resolve answer of the current page (`SeoResolveAnswer | null`), shared state. - `resolveSeo(url)`: one resolve call with the server or public base; `null` on any failure. - `seoHeadInput`, `seoRedirectTarget`, `seoRobots`, `seoXRobotsTag`, `seoResolvableUrl`, `seoAnswer`, `seoVerificationName`: the pure helpers behind them (`app/utils/seo.ts`), auto-imported. ## Development ```sh pnpm install pnpm test # node --test on the helpers pnpm build:playground pnpm smoke:ssr # the playground against testing/fixture-api.mjs and testing/expect.txt ``` `testing/fixture-api.mjs` stands in for the plugin's public API (resolve answers from `testing/fixtures/resolve.json`, the 404 report with `FIXTURE_NOTFOUND_TOKEN`, verification files from `testing/fixtures/verification/`). `testing/smoke-ssr.mjs` checks status, body, content type and headers (`header=Name: text`) per line, and `report PATH` lines require a 404 report for `PATH`.