// The SEO layer's settings as the server code reads them from runtime // config. Pure TypeScript outside Nitro's auto-import directories, so // `node --test`, the middleware and the routes import it directly. import { seoEnabled } from '../../app/utils/seo.ts' export interface SeoServerConfig { /** False when public.seo.enabled or its alias public.seo.cms is false: no CMS call at all. */ enabled: boolean /** Server-only base of the plugin's API (NUXT_SEO_API_BASE), without a trailing slash. */ apiBase: string /** Server-only origin of the Go binary for the root files (NUXT_SEO_BACKEND_BASE). */ backendBase: string /** How long a call may take before the page renders without it (NUXT_SEO_TIMEOUT_MS). */ timeoutMs: number /** Seconds a resolve answer stays cached (NUXT_SEO_RESOLVE_CACHE_TTL); 0 turns the cache off. */ resolveCacheTtl: number /** Bearer token of POST /notfound (NUXT_SEO_NOTFOUND_TOKEN); empty: no reports. */ notfoundToken: string /** Bearer token of the route report; falls back to notfoundToken. */ routeReportToken: string /** Module option seo.routeReport (NUXT_PUBLIC_SEO_ROUTE_REPORT): report the static routes at start. */ routeReport: boolean /** Module option seo.rootFiles: serve robots.txt, the sitemaps, llms.txt and the listed root files. */ rootFiles: boolean /** Module option seo.resolveFiles: file extensions the middleware still resolves (['pdf']). */ resolveFiles: string[] } export const DEFAULT_SEO_API_BASE = 'http://127.0.0.1:8096/_seo/api/v1' export const DEFAULT_RESOLVE_CACHE_TTL = 60 const API_SUFFIX = /\/_seo\/api\/v1\/?$/ function section(config: unknown, name: string): Record { if (config === null || typeof config !== 'object') return {} const value = (config as Record)[name] return value !== null && typeof value === 'object' ? (value as Record) : {} } const str = (v: unknown) => (typeof v === 'string' ? v.trim() : typeof v === 'number' ? String(v) : '') function num(v: unknown, fallback: number): number { if (v === '' || v === null || v === undefined) return fallback const n = Number(v) return Number.isFinite(n) && n >= 0 ? n : fallback } /** * The backend origin for root files: backendBase when set, else apiBase * without its /_seo/api/v1 suffix (so an app that only sets * NUXT_SEO_API_BASE needs nothing else), else apiBase's origin. */ export function seoBackendBase(backendBase: string, apiBase: string): string { if (backendBase) return backendBase.replace(/\/+$/, '') if (API_SUFFIX.test(apiBase)) return apiBase.replace(API_SUFFIX, '') try { return new URL(apiBase).origin } catch { return '' } } /** Reads the layer's settings from a runtime config object (useRuntimeConfig(event)). */ export function seoServerConfig(config: unknown): SeoServerConfig { const seo = section(config, 'seo') const pub = section(section(config, 'public'), 'seo') const apiBase = (str(seo.apiBase) || DEFAULT_SEO_API_BASE).replace(/\/+$/, '') const notfoundToken = str(seo.notfoundToken) const resolveFiles = Array.isArray(pub.resolveFiles) ? pub.resolveFiles.filter((e): e is string => typeof e === 'string') : ['pdf'] return { enabled: seoEnabled(pub), apiBase, backendBase: seoBackendBase(str(seo.backendBase), apiBase), timeoutMs: num(seo.timeoutMs, 1500) || 1500, resolveCacheTtl: num(seo.resolveCacheTtl, DEFAULT_RESOLVE_CACHE_TTL), notfoundToken, routeReportToken: str(seo.routeReportToken) || notfoundToken, routeReport: pub.routeReport !== false && pub.routeReport !== 'false', rootFiles: pub.rootFiles !== false && pub.rootFiles !== 'false', resolveFiles, } }