- 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
39 lines
1.7 KiB
TypeScript
39 lines
1.7 KiB
TypeScript
// The server side of the resolve call: GET <apiBase>/resolve?url=<path and
|
|
// query>, forwarding the visitor's address so the backend's per-IP throttle
|
|
// (seo-resolve) keeps one bucket per visitor, also on route-cached renders
|
|
// (server/plugins/seo-client-address.ts records it). Any failure (timeout,
|
|
// 4xx, 5xx, an unexpected body) answers null and the page renders without
|
|
// CMS head tags (fail open); with the layer switched off nothing is called.
|
|
//
|
|
// Nitro auto-imports (useRuntimeConfig, $fetch) are used inside the
|
|
// functions only, so `node --test` can import this file.
|
|
|
|
import type { H3Event } from 'h3'
|
|
import { seoAnswer } from '../../app/utils/seo.ts'
|
|
import type { SeoResolveAnswer } from '../../app/utils/types.ts'
|
|
import { seoServerConfig, type SeoServerConfig } from './config.ts'
|
|
|
|
/** The x-forwarded-for value the SEO calls carry for this request. */
|
|
export function seoForwardedFor(event: H3Event): Record<string, string> {
|
|
const recorded = event.context.seoForwardedFor as string | undefined
|
|
return recorded ? { 'x-forwarded-for': recorded } : {}
|
|
}
|
|
|
|
async function fetchResolve(cfg: SeoServerConfig, url: string, headers: Record<string, string>): Promise<SeoResolveAnswer | null> {
|
|
try {
|
|
return seoAnswer(await $fetch('/resolve', { baseURL: cfg.apiBase, query: { url }, headers, timeout: cfg.timeoutMs }))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The resolve answer for `url` (path and query), or null on any failure or
|
|
* when the layer is off (public.seo.enabled or seo.cms false).
|
|
*/
|
|
export async function resolveCached(event: H3Event, url: string): Promise<SeoResolveAnswer | null> {
|
|
const cfg = seoServerConfig(useRuntimeConfig(event))
|
|
if (!cfg.enabled) return null
|
|
return fetchResolve(cfg, url, seoForwardedFor(event))
|
|
}
|