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

81 lines
3.4 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 (33-08); falls back to notfoundToken. */
routeReportToken: string
/** 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,
rootFiles: pub.rootFiles !== false && pub.rootFiles !== 'false',
resolveFiles,
}
}