// The server side of the resolve call: GET /resolve?url=, 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 { const recorded = event.context.seoForwardedFor as string | undefined return recorded ? { 'x-forwarded-for': recorded } : {} } async function fetchResolve(cfg: SeoServerConfig, url: string, headers: Record): Promise { 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 { const cfg = seoServerConfig(useRuntimeConfig(event)) if (!cfg.enabled) return null return fetchResolve(cfg, url, seoForwardedFor(event)) }