// The resolve call and the shared answer of the current page. On the server // it takes the answer the Nitro middleware (server/middleware/seo.ts) already // resolved for this request through its cache (event.context.seo), so a page // costs one resolve call; for a request the middleware did not resolve (a // path it skips, such as a dotted author slug) it calls the internal base (runtimeConfig.seo.apiBase) and forwards the // visitor's x-forwarded-for plus the Nuxt peer, so the backend's per-IP // throttle (seo-resolve, 600 a minute) keeps one bucket per visitor, also on // route-cached renders. In the browser it calls the same-origin public base. // Any failure (timeout, 4xx, 5xx, an unexpected body) answers null: the page // then renders with its own head. import { seoAnswer } from '../utils/seo.ts' import type { SeoResolveAnswer } from '../utils/types.ts' /** The resolve answer of the page being rendered, null when there is none. */ export function useSeoAnswer() { return useState('seo-layer:answer', () => null) } export async function resolveSeo(url: string): Promise { const config = useRuntimeConfig() const timeout = Number((config.seo as { timeoutMs?: number } | undefined)?.timeoutMs) || 1500 try { if (import.meta.server) { const event = useRequestEvent() const resolved = event?.context.seoResolved as { url: string; answer: SeoResolveAnswer | null } | undefined if (resolved && resolved.url === url) return resolved.answer const base = (config.seo as { apiBase: string }).apiBase const recorded = event?.context.seoForwardedFor as string | undefined const headers: Record = recorded ? { 'x-forwarded-for': recorded } : {} return seoAnswer(await $fetch('/resolve', { baseURL: base, query: { url }, headers, timeout })) } const base = (config.public.seo as { apiBase: string }).apiBase return seoAnswer(await $fetch('/resolve', { baseURL: base, query: { url }, timeout })) } catch { return null } }