Files
vue-seo-layer/app/composables/useCmsSeo.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

32 lines
1.5 KiB
TypeScript

// The page head from the CMS resolve answer: title, description, robots,
// canonical, Open Graph (with og:locale and its alternates), X/Twitter,
// verification meta, <html lang>, hreflang links and exactly one JSON-LD
// script holding one @graph (the CMS nodes plus those added through
// useSchemaNode, merged by @id). One entry with tagPriority 'critical', so
// its tags win over the same tags a page, a layer or a module sets; a value
// the answer lacks adds no tag and the page's own stays. The layer's app
// plugin (seo-head) is the only caller: pages and other layers never set CMS
// values themselves.
import type { Ref } from 'vue'
import { mergeGraph } from '../utils/graph.ts'
import { seoHeadInput } from '../utils/seo.ts'
import type { SeoResolveAnswer } from '../utils/types.ts'
export function useCmsSeo(answer: Ref<SeoResolveAnswer | null>): void {
const origin = useRequestURL().origin
const nodes = useSchemaNodes()
useHead(
computed(() => {
const head = seoHeadInput(answer.value, origin)
const cmsGraph = answer.value?.action === 'ok' ? answer.value.jsonld : null
const graph = mergeGraph(cmsGraph, nodes())
// unhead serializes the object and escapes "<" inside it, so a value
// such as "</script>" cannot end the script element.
const script = graph ? [{ type: 'application/ld+json', key: 'seo-jsonld', innerHTML: graph }] : []
return { ...head, script }
}),
{ tagPriority: 'critical' },
)
}