- 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
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
// The route report (D-33): at server start the layer posts its static
|
|
// 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, 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
|
|
|
|
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 }
|
|
}
|
|
|
|
/**
|
|
* What an answer means: sent (2xx), absent (404: a backend without the
|
|
* page registry, or with both tokens empty; never retried), refused (401 or
|
|
* 403: the token is wrong; never retried) or retry (no answer, or any other
|
|
* status: tried once more after ROUTE_REPORT_RETRY_MS).
|
|
*/
|
|
export function routeReportOutcome(status: number | null): 'sent' | 'absent' | 'refused' | 'retry' {
|
|
if (status !== null && status >= 200 && status < 300) return 'sent'
|
|
if (status === 404) return 'absent'
|
|
if (status === 401 || status === 403) return 'refused'
|
|
return 'retry'
|
|
}
|