- the Nitro middleware answers redirect (301/302, Location from the answer only, same-site targets as paths as in v0.1.1) and gone (410) before the render and the route cache, sets X-Robots-Tag, and answers 404 for a pagination.invalid answer; other not_found answers stay with Nuxt - resolve answers cached in an LRU (seo.resolveCacheTtl, default 60 s, 1000 entries) with in-flight de-duplication; a clear detaches loads in flight so no answer fetched before a purge is stored after it - the playground route-caches /blog/** (swr 300)
63 lines
2.7 KiB
TypeScript
63 lines
2.7 KiB
TypeScript
// The server side of the resolve call: GET <apiBase>/resolve?url=<path and
|
|
// query> through an LRU (path and query as key, seo.resolveCacheTtl seconds,
|
|
// default 60, at most 1000 entries, concurrent requests for one key share
|
|
// one call), forwarding the visitor's address so the backend's per-IP throttle
|
|
// (seo-resolve) keeps one bucket per visitor, also on route-cached renders
|
|
// (server/plugins/seo-client-address.ts records it). Any failure (timeout,
|
|
// 4xx, 5xx, an unexpected body) answers null and the page renders without
|
|
// CMS head tags (fail open) and is not cached; with the layer switched off
|
|
// nothing is called. The site cache purge clears the cache (clearSeoCaches).
|
|
//
|
|
// Nitro auto-imports (useRuntimeConfig, $fetch) are used inside the
|
|
// functions only, so `node --test` can import this file.
|
|
|
|
import type { H3Event } from 'h3'
|
|
import { seoAnswer } from '../../app/utils/seo.ts'
|
|
import type { SeoResolveAnswer } from '../../app/utils/types.ts'
|
|
import { seoServerConfig, type SeoServerConfig } from './config.ts'
|
|
import { createLru, type Lru } from './lru.ts'
|
|
|
|
/** Most resolve answers kept. */
|
|
export const RESOLVE_CACHE_MAX = 1000
|
|
|
|
let cache: Lru<SeoResolveAnswer | null> | null = null
|
|
let cacheTtl = -1
|
|
|
|
function resolveCache(ttlSeconds: number): Lru<SeoResolveAnswer | null> {
|
|
if (!cache || cacheTtl !== ttlSeconds) {
|
|
cache = createLru<SeoResolveAnswer | null>({ max: RESOLVE_CACHE_MAX, ttlMs: ttlSeconds * 1000 })
|
|
cacheTtl = ttlSeconds
|
|
}
|
|
return cache
|
|
}
|
|
|
|
/** Empties the layer's SEO caches (the resolve answers); the site cache purge calls it. */
|
|
export function clearSeoCaches(): void {
|
|
cache?.clear()
|
|
}
|
|
|
|
/** The x-forwarded-for value the SEO calls carry for this request. */
|
|
export function seoForwardedFor(event: H3Event): Record<string, string> {
|
|
const recorded = event.context.seoForwardedFor as string | undefined
|
|
return recorded ? { 'x-forwarded-for': recorded } : {}
|
|
}
|
|
|
|
async function fetchResolve(cfg: SeoServerConfig, url: string, headers: Record<string, string>): Promise<SeoResolveAnswer | null> {
|
|
try {
|
|
return seoAnswer(await $fetch('/resolve', { baseURL: cfg.apiBase, query: { url }, headers, timeout: cfg.timeoutMs }))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The resolve answer for `url` (path and query), or null on any failure or
|
|
* when the layer is off (public.seo.enabled or seo.cms false).
|
|
*/
|
|
export async function resolveCached(event: H3Event, url: string): Promise<SeoResolveAnswer | null> {
|
|
const cfg = seoServerConfig(useRuntimeConfig(event))
|
|
if (!cfg.enabled) return null
|
|
const headers = seoForwardedFor(event)
|
|
return resolveCache(cfg.resolveCacheTtl).load(url, () => fetchResolve(cfg, url, headers), (answer) => answer !== null)
|
|
}
|