Files
vue-seo-layer/lib/seo-module.ts
Jakub Zych 160b53b2f5 feat: CMS head and one JSON-LD graph on every server-rendered page (tracer)
- Nitro middleware resolves page requests once and hands the answer to the
  render (event.context.seo); the route middleware reuses it
- useCmsSeo: html lang, hreflang links, og:locale and exactly one JSON-LD
  script with one @graph (CMS nodes plus useSchemaNode nodes, merged by @id)
- local module (configKey seo): cms (alias of public.seo.enabled),
  breadcrumbs, links.newTabHint, rootFiles, resolveFiles
- GET /api/__seo__/resolve for client navigation through the Nuxt server
- smoke harness: count=, exact header=, noheader=, reqheader=, repeated
  options, resolves/apicalls/noreport lines, --no-site; v0.1.1 suite runs
  unchanged against a backend without /site
2026-11-04 09:15:26 +01:00

66 lines
2.6 KiB
TypeScript

// The layer's local Nuxt module (configKey `seo`). It copies the module
// options into runtimeConfig.public.seo, where the layer's app and server
// code read them. It lives in lib/ rather than modules/ so Nuxt's
// local-module scan cannot register it a second time. Layer source never
// uses the ~ or @ aliases: they resolve to the consuming project.
//
// The config key `seo` is shared with nuxt-seo-utils (@nuxtjs/seo) where an
// app still uses it: each module reads only its own keys, so the layer's
// options never use a name nuxt-seo-utils has (enabled, fallbackTitle …).
import { defineNuxtModule } from 'nuxt/kit'
export interface SeoModuleOptions {
/**
* false turns every CMS call off (no resolve, no head from the CMS, no
* redirects, no root files, no 404 reports), for a site without the SEO
* plugin. An alias of public.seo.enabled (NUXT_PUBLIC_SEO_ENABLED), which
* keeps working; either one false switches the layer off.
*/
cms: boolean
/** Breadcrumbs from the CMS (components in 33-08). */
breadcrumbs: boolean
links: {
/**
* A visually hidden "(opens in a new tab)" hint inside external links
* while the CMS's links_new_tab is on. Copied to
* runtimeConfig.public.seo.links.newTabHint for the body-link pipeline.
*/
newTabHint: boolean
}
/** Serve robots.txt, the sitemaps, llms.txt and the CMS's root files from the backend. */
rootFiles: boolean
/** File extensions the Nitro middleware still resolves, so an old file address can redirect. */
resolveFiles: string[]
}
export const SEO_MODULE_DEFAULTS: SeoModuleOptions = {
cms: true,
breadcrumbs: true,
links: { newTabHint: true },
rootFiles: true,
resolveFiles: ['pdf'],
}
export default defineNuxtModule<SeoModuleOptions>({
meta: {
name: 'vue-seo-layer',
configKey: 'seo',
},
defaults: SEO_MODULE_DEFAULTS,
setup(options, nuxt) {
const runtime = nuxt.options.runtimeConfig as { public: Record<string, unknown> }
const current = (runtime.public.seo ?? {}) as Record<string, unknown>
// Only the layer's own keys: the shared config key may carry
// nuxt-seo-utils options, which stay out of the public runtime config.
runtime.public.seo = {
...current,
cms: options.cms !== false,
breadcrumbs: options.breadcrumbs !== false,
links: { ...((current.links as Record<string, unknown>) ?? {}), newTabHint: options.links?.newTabHint !== false },
rootFiles: options.rootFiles !== false,
resolveFiles: Array.isArray(options.resolveFiles) ? [...options.resolveFiles] : [...SEO_MODULE_DEFAULTS.resolveFiles],
}
},
})