- 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
81 lines
3.4 KiB
TypeScript
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,
|
|
}
|
|
}
|