Files
vue-seo-layer/server/lib/resolve.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

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))
}