feat: the route report sends the noindex static paths (tracer)
- noindexStaticRoutes: requiresAuth, noindex/none seo.robots and static
pages under seo.noindexRoutes, i18n copies included, never patterns or
the layer's HTML sitemap pages (G1, gap-2 contract)
- #seo-routes exports noindexPaths; routeReportBody(paths, noindex) answers
{routes, noindex} with the same limits and keeps the JSON body within the
older plugin's 4,113,024 bytes, dropping noindex entries first
- smoke grammar noindex= and lacksnoindex=; README carries the contract
This commit is contained in:
@@ -1,22 +1,63 @@
|
||||
// The route report (D-33): at server start the layer posts its static
|
||||
// routes (#seo-routes, built from the pages) to the SEO plugin's
|
||||
// POST /_seo/api/v1/registry/routes with the route-report token, so the
|
||||
// CMS's page registry knows every frontend page. Pure helpers for the
|
||||
// routes and the static paths it serves noindex (#seo-routes, built from
|
||||
// the pages) to the SEO plugin's POST /_seo/api/v1/registry/routes with the
|
||||
// route-report token, so the CMS's page registry knows every frontend page
|
||||
// and its sitemaps leave the noindex ones out (G1). Pure helpers for the
|
||||
// Nitro plugin server/plugins/seo-route-report.ts, so `node --test` drives
|
||||
// them.
|
||||
|
||||
/** The plugin's limits: at most 2000 paths of at most 2048 bytes each. */
|
||||
/** The plugin's limits: at most 2000 paths of at most 2048 bytes each, per list. */
|
||||
export const ROUTE_REPORT_MAX_ROUTES = 2000
|
||||
export const ROUTE_REPORT_MAX_BYTES = 2048
|
||||
/**
|
||||
* The most body a plugin built before the `noindex` key reads
|
||||
* (sm-seo-plugin's maxRouteReportBody: MaxReportedRoutes*(MaxRouteBytes+8)+1024,
|
||||
* 4,113,024 bytes); the serialised JSON body never exceeds it.
|
||||
*/
|
||||
export const ROUTE_REPORT_MAX_BODY = ROUTE_REPORT_MAX_ROUTES * (ROUTE_REPORT_MAX_BYTES + 8) + 1024
|
||||
/** The one retry after a failed report waits this long. */
|
||||
export const ROUTE_REPORT_RETRY_MS = 60_000
|
||||
|
||||
/** The request body: the paths within the plugin's limits. */
|
||||
export function routeReportBody(paths: readonly string[]): { routes: string[] } {
|
||||
const routes = paths
|
||||
.filter((p) => typeof p === 'string' && p.startsWith('/') && Buffer.byteLength(p, 'utf8') <= ROUTE_REPORT_MAX_BYTES)
|
||||
.slice(0, ROUTE_REPORT_MAX_ROUTES)
|
||||
return { routes }
|
||||
export interface RouteReportBody {
|
||||
routes: string[]
|
||||
noindex: string[]
|
||||
}
|
||||
|
||||
const withinLimits = (paths: readonly string[]) =>
|
||||
paths.filter((p) => typeof p === 'string' && p.startsWith('/') && Buffer.byteLength(p, 'utf8') <= ROUTE_REPORT_MAX_BYTES).slice(0, ROUTE_REPORT_MAX_ROUTES)
|
||||
|
||||
/** The longest prefix of `paths` whose JSON array entries fit into `room` bytes (commas included). */
|
||||
function fitting(paths: readonly string[], room: number): string[] {
|
||||
let used = 0
|
||||
let n = 0
|
||||
for (const p of paths) {
|
||||
const size = Buffer.byteLength(JSON.stringify(p), 'utf8') + (n > 0 ? 1 : 0)
|
||||
if (used + size > room) break
|
||||
used += size
|
||||
n++
|
||||
}
|
||||
return paths.slice(0, n)
|
||||
}
|
||||
|
||||
/**
|
||||
* The request body: both lists filtered to paths starting with / of at most
|
||||
* 2048 bytes and cut to 2000 entries; a path in `routes` is never repeated in
|
||||
* `noindex`. `noindex` is always sent (also []), so the plugin's set follows
|
||||
* each build. The serialised body stays at or under ROUTE_REPORT_MAX_BODY:
|
||||
* `noindex` entries are dropped from the end first, then `routes` entries.
|
||||
*/
|
||||
export function routeReportBody(paths: readonly string[], noindexPaths: readonly string[] = []): RouteReportBody {
|
||||
const routes = withinLimits(paths)
|
||||
const listed = new Set(routes)
|
||||
let noindex = withinLimits(noindexPaths.filter((p) => !listed.has(p)))
|
||||
const routesSize = Buffer.byteLength(JSON.stringify({ routes, noindex: [] }), 'utf8')
|
||||
if (routesSize > ROUTE_REPORT_MAX_BODY) {
|
||||
// Every noindex entry is gone and the body is still too large: cut routes.
|
||||
const empty = Buffer.byteLength(JSON.stringify({ routes: [], noindex: [] }), 'utf8')
|
||||
return { routes: fitting(routes, ROUTE_REPORT_MAX_BODY - empty), noindex: [] }
|
||||
}
|
||||
noindex = fitting(noindex, ROUTE_REPORT_MAX_BODY - routesSize)
|
||||
return { routes, noindex }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// Reports the site's static routes to the CMS's page registry once per
|
||||
// server process (D-33): POST <apiBase>/registry/routes with
|
||||
// `Authorization: Bearer <route-report token>` and {routes: [path, ...]}.
|
||||
// The paths come from the build (#seo-routes): pages without parameters,
|
||||
// without the ones whose meta keeps them out of the index (requiresAuth, a
|
||||
// noindex seo.robots) and without seo.noindexRoutes. It never delays the
|
||||
// start: the report goes out after the plugin returns, a failure (no
|
||||
// answer, a 5xx) is tried once more after 60 s, and a 404 (a backend without
|
||||
// the registry) or a refused token is logged once and not retried. Nothing
|
||||
// is sent with the layer off (seo.cms, public.seo.enabled), with
|
||||
// seo.routeReport false, without a token (NUXT_SEO_ROUTE_REPORT_TOKEN, else
|
||||
// NUXT_SEO_NOTFOUND_TOKEN) or while prerendering. The token stays in the
|
||||
// `Authorization: Bearer <route-report token>` and
|
||||
// {routes: [path, ...], noindex: [path, ...]}. The paths come from the build
|
||||
// (#seo-routes): `routes` are the pages without parameters that may be
|
||||
// indexed; `noindex` the static paths the build serves noindex (requiresAuth,
|
||||
// a noindex or none seo.robots, a static page under seo.noindexRoutes, their
|
||||
// i18n copies), so the CMS leaves them out of its sitemaps (G1). `noindex`
|
||||
// is always sent, also empty; a plugin older than the key ignores it. It
|
||||
// never delays the start: the report goes out after the plugin returns, a
|
||||
// failure (no answer, a 5xx) is tried once more after 60 s, and a 404 (a
|
||||
// backend without the registry) or a refused token is logged once and not
|
||||
// retried. Nothing is sent with the layer off (seo.cms, public.seo.enabled),
|
||||
// with seo.routeReport false, without a token (NUXT_SEO_ROUTE_REPORT_TOKEN,
|
||||
// else NUXT_SEO_NOTFOUND_TOKEN) or while prerendering. The token stays in the
|
||||
// server-only runtime config and goes to the configured apiBase only.
|
||||
|
||||
import { staticRoutes } from '#seo-routes'
|
||||
import { noindexPaths, staticRoutes } from '#seo-routes'
|
||||
import { seoServerConfig } from '../lib/config.ts'
|
||||
import { ROUTE_REPORT_RETRY_MS, routeReportBody, routeReportOutcome } from '../lib/routeReport.ts'
|
||||
|
||||
@@ -20,7 +23,7 @@ export default defineNitroPlugin(() => {
|
||||
if (import.meta.prerender) return
|
||||
const cfg = seoServerConfig(useRuntimeConfig())
|
||||
if (!cfg.enabled || !cfg.routeReport || !cfg.routeReportToken) return
|
||||
const body = routeReportBody(staticRoutes)
|
||||
const body = routeReportBody(staticRoutes, noindexPaths)
|
||||
|
||||
const send = async (attempt: number): Promise<void> => {
|
||||
let status: number | null = null
|
||||
@@ -39,7 +42,7 @@ export default defineNitroPlugin(() => {
|
||||
reason = (err as Error)?.message ?? String(err)
|
||||
}
|
||||
const outcome = routeReportOutcome(status)
|
||||
if (outcome === 'sent') console.info(`[vue-seo-layer] route report: ${body.routes.length} routes sent`)
|
||||
if (outcome === 'sent') console.info(`[vue-seo-layer] route report: ${body.routes.length} routes and ${body.noindex.length} noindex paths sent`)
|
||||
else if (outcome === 'absent') console.info('[vue-seo-layer] route report: the CMS has no page registry (404), nothing reported')
|
||||
else if (outcome === 'refused') console.warn(`[vue-seo-layer] route report: the CMS refused the token (${status}); set NUXT_SEO_ROUTE_REPORT_TOKEN to golem15.seo.route_report_token`)
|
||||
else if (attempt === 1) {
|
||||
|
||||
2
server/seo-routes.d.ts
vendored
2
server/seo-routes.d.ts
vendored
@@ -5,4 +5,6 @@ declare module '#seo-routes' {
|
||||
export const staticRoutes: string[]
|
||||
/** Every page's route pattern (the drift check). */
|
||||
export const routePatterns: Array<{ pattern: string; catchAll: boolean }>
|
||||
/** Static paths the build serves noindex (the route report's noindex list, G1). */
|
||||
export const noindexPaths: string[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user