- 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
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
// Request-scoped schema.org nodes a page or another layer adds to the page's
|
|
// one JSON-LD @graph (an ItemList on a listing, a FAQPage under a FAQ block).
|
|
// useCmsSeo merges them with the CMS's nodes by @id (mergeGraph), so a page
|
|
// never carries a second JSON-LD script. The list lives on the Nuxt app
|
|
// instance, not in useState: it is rebuilt by the components' setup on the
|
|
// server and again on the client, so it never travels in the payload.
|
|
|
|
import type { MaybeRefOrGetter, ShallowRef } from 'vue'
|
|
import type { SeoSchemaNode } from '../utils/types.ts'
|
|
|
|
interface SchemaEntry {
|
|
id: number
|
|
node: MaybeRefOrGetter<SeoSchemaNode | null | undefined>
|
|
}
|
|
|
|
type AppWithNodes = { _seoSchemaNodes?: ShallowRef<SchemaEntry[]>; _seoSchemaNodeSeq?: number }
|
|
|
|
function store(): { list: ShallowRef<SchemaEntry[]>; app: AppWithNodes } {
|
|
const app = useNuxtApp() as unknown as AppWithNodes
|
|
app._seoSchemaNodes ??= shallowRef<SchemaEntry[]>([])
|
|
return { list: app._seoSchemaNodes, app }
|
|
}
|
|
|
|
/** The nodes registered for the current page, resolved (refs and getters read). */
|
|
export function useSchemaNodes(): () => SeoSchemaNode[] {
|
|
const { list } = store()
|
|
return () => list.value.map((e) => toValue(e.node)).filter((n): n is SeoSchemaNode => !!n && typeof n === 'object')
|
|
}
|
|
|
|
/**
|
|
* Adds `node` (a plain object, a ref or a getter; null adds nothing) to the
|
|
* page's @graph for as long as the calling component lives. A node with the
|
|
* @id of a CMS node is merged into it, the CMS values first.
|
|
*/
|
|
export function useSchemaNode(node: MaybeRefOrGetter<SeoSchemaNode | null | undefined>): void {
|
|
const { list, app } = store()
|
|
const id = (app._seoSchemaNodeSeq = (app._seoSchemaNodeSeq ?? 0) + 1)
|
|
list.value = [...list.value, { id, node }]
|
|
if (import.meta.client) {
|
|
onScopeDispose(() => {
|
|
list.value = list.value.filter((e) => e.id !== id)
|
|
})
|
|
}
|
|
}
|