Files
vue-seo-layer/server/lib/config.ts
Jakub Zych 3abc986f42 feat: route report and drift check at server start
- the module writes #seo-routes (static paths and route patterns) from the
  resolved pages; options seo.routeReport, seo.pagination and
  seo.htmlSitemap reach the public runtime config
- seo-route-report: POST /registry/routes once per process with the
  route-report token, one retry after 60 s on failure, nothing without a
  token, with the layer off or with routeReport false
- seo-drift: one "[vue-seo-layer] drift:" warning per mismatch between
  the CMS's site answer (URL patterns, pagination, HTML sitemap paths)
  and the build
- GET /api/__seo__/site and useSeoSite()
- smoke grammar routereport and log, fixture registry route, drift run
2026-11-04 11:49:21 +01:00

84 lines
3.6 KiB
TypeScript

// 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<string, unknown> {
if (config === null || typeof config !== 'object') return {}
const value = (config as Record<string, unknown>)[name]
return value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : {}
}
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,
}
}