- the Nitro middleware answers redirect (301/302, Location from the answer only, same-site targets as paths as in v0.1.1) and gone (410) before the render and the route cache, sets X-Robots-Tag, and answers 404 for a pagination.invalid answer; other not_found answers stay with Nuxt - resolve answers cached in an LRU (seo.resolveCacheTtl, default 60 s, 1000 entries) with in-flight de-duplication; a clear detaches loads in flight so no answer fetched before a purge is stored after it - the playground route-caches /blog/** (swr 300)
vue-seo-layer
The Nuxt half of the SummerCMS SEO plugin (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.
// nuxt.config.ts of the app
export default defineNuxtConfig({
extends: ['vue-seo-layer'],
})
"vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.1.1"
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 <apiBase>/resolve?url=<path and query>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.
redirectanswers 301 or 302 withLocation. A target on the site's own origin (the origin of the answer'surl) 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.
goneanswers 410 with the app's error page, also on a route that would render. - Head. An
okanswer sets the title (final; the plugin applies its templates, so the app'stitleTemplateis replaced by%s),description,canonical,robots,og:title,og:description,og:image,og:url,twitter:card,twitter:siteand the verification meta tags. The entry hastagPriority: '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:urlorog:imagethe plugin answers as a path (a backend withoutgolem15.seo.site_urlorapp.url) is made absolute on the request's origin; set the site URL on the backend so it names the public origin.not_foundanswers (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
staginganswer (the backend outsideSUMMER_ENV=production) makes every pagenoindex, nofollow, known or not. TheX-Robots-Tagresponse 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 withPOST <apiBase>/notfound,{"url", "referer"}andAuthorization: Bearer <notfoundToken>, after the response. Without a token nothing is reported. - Verification files. A request for a single-segment
.html,.txtor.xmlfile (/google0123.html,/BingSiteAuth.xml) is looked up underGET <apiBase>/verification/<name>and served withX-Content-Type-Options: nosniff, a sandboxing CSP andX-Robots-Tag: noindexwhen the plugin has it; otherwise it falls through to the app's routes.robots.txt,sitemap.xml,sitemap.xsl,rss.xml,feed.xml,atom.xmlandllms.txtare never looked up. - Visitor address. The server calls forward the visitor's
X-Forwarded-Forplus 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'shttp.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 <base>/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;nullon any failure.seoHeadInput,seoAbsolute,seoRedirectTarget,seoRobots,seoXRobotsTag,seoResolvableUrl,seoAnswer,seoVerificationName: the pure helpers behind them (app/utils/seo.ts), auto-imported.
Development
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.