# 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, 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. From v0.2.0 it also fails a build whose indexable routes would render only in the browser, marks declared and signed-in routes noindex, reports the site's routes to the plugin's page registry, and ships server-rendered breadcrumbs, pagination and an HTML sitemap. ## 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 export default defineNuxtConfig({ extends: ['vue-seo-layer'], }) ``` 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'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.** 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. - **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 `