@contentrain/verify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +13 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/cli.d.mts +24 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +68 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/load-KUG-8Ug4.mjs +742 -0
- package/dist/load-KUG-8Ug4.mjs.map +1 -0
- package/dist/types-C_hmH1n3.d.mts +87 -0
- package/dist/types-C_hmH1n3.d.mts.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-KUG-8Ug4.mjs","names":[],"sources":["../src/types.ts","../src/html.ts","../src/url.ts","../src/checks.ts","../src/verify.ts","../src/load.ts"],"sourcesContent":["// ─── What a verification run is given, and what it answers with ───\n//\n// Everything here is data the caller already has. The package never fetches:\n// a hardened crawler is a security surface of its own (SSRF, DNS rebinding,\n// redirect and byte limits) and belongs where that hardening lives, not in the\n// MIT library every consumer embeds. Whoever holds a safe fetcher — or a\n// directory of built files — supplies the documents.\n//\n// The consequence is that this runs identically over a `dist/` directory, over\n// a captured baseline, and inside another tool's pipeline, with no network and\n// no mocking.\n\n/** One page as served: the address it answers at and what it answered with. */\nexport interface VerifyDocument {\n /** Absolute URL, or a site-relative path (`/about/`). */\n url: string\n html: string\n /** Defaults to 200 when the caller did not record one. */\n status?: number\n /** Response headers, lowercased keys. `x-robots-tag` is read here. */\n headers?: Record<string, string>\n}\n\nexport interface VerifyRedirect {\n from: string\n to: string\n status: number\n}\n\nexport const CHECK_GROUPS = [\n /** canonical, `<title>`, description, Open Graph, Twitter card */\n 'identity',\n /** robots meta, X-Robots-Tag, noindex leaks, sitemap membership */\n 'indexing',\n /** status parity, soft-404, redirect chains */\n 'status',\n /** hreflang pairs and reciprocity */\n 'international',\n /** JSON-LD validity and type parity */\n 'structured',\n /** internal links, pagination, feeds */\n 'navigation',\n /** image resolution and alt preservation */\n 'assets',\n] as const\n\nexport type CheckGroup = (typeof CHECK_GROUPS)[number]\n\nexport type Severity = 'error' | 'warning' | 'info'\n\n/**\n * One thing that is wrong, or worth knowing.\n *\n * `check` is a stable id (`identity.canonical-mismatch`) so a consumer can\n * suppress, threshold or track one finding without matching on prose.\n */\nexport interface Finding {\n group: CheckGroup\n check: string\n severity: Severity\n /** The document the finding is about, when it is about one. */\n url?: string\n message: string\n detail?: string\n}\n\nexport interface VerifyReport {\n /** No `error` findings. This is the gate. */\n passed: boolean\n findings: Finding[]\n counts: Record<Severity, number>\n /** How many documents were examined. */\n documents: number\n /** Which groups actually ran. */\n groups: CheckGroup[]\n /** Groups that were asked for but could not run, and why. */\n skipped: { group: CheckGroup, reason: string }[]\n}\n\nexport interface VerifyOptions {\n /**\n * A redirect chain longer than this is reported. One hop is normal; two is a\n * link-equity leak and a latency cost, and chains grow silently as redirect\n * rules accumulate over migrations.\n */\n maxRedirectHops?: number\n /**\n * A 200 response shorter than this many characters of text, whose title or\n * body says \"not found\", is treated as a soft-404. Kept configurable because\n * a legitimately short page exists; the default is deliberately low.\n */\n soft404MaxTextLength?: number\n /** Hosts to treat as internal besides `site`'s own. */\n internalHosts?: string[]\n}\n\nexport interface VerifyInput {\n /** The built site. */\n documents: VerifyDocument[]\n /** The site's canonical origin (`https://example.com`). Several checks need it. */\n site?: string\n /** `sitemap.xml` as produced by the build. */\n sitemap?: string\n /** Redirect rules the host will serve. */\n redirects?: VerifyRedirect[]\n /** Non-document files the build produced, as served (`/images/a.png`). */\n assets?: string[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /** Defaults to every group. */\n groups?: readonly CheckGroup[]\n options?: VerifyOptions\n}\n","// ─── Reading the few things a document has to say ───\n//\n// Targeted regexes, not a parser. The same choice `@contentrain/emitter-astro`\n// makes, for the same reason: these documents are machine-generated HTML whose\n// head we already control, the extracted set is small and fixed, and a parser\n// would put a dependency into the one package a migration is supposed to be\n// able to run anywhere.\n//\n// The limits are real and bounded. `head()` splits on the first `</head>`, so a\n// document without one yields nothing rather than guessing; attribute reads are\n// quote-aware but do not resolve entities beyond the five that matter. Where a\n// check would be wrong under those limits, it is not written.\n\n/** Everything before the first `</head>`, or '' when there is none. */\nexport function head(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? '' : html.slice(0, end)\n}\n\n/** Everything after the first `</head>`, or the whole document when there is none. */\nexport function body(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? html : html.slice(end)\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nexport function decodeEntities(value: string): string {\n return value.replace(/&(#39|amp|lt|gt|quot|apos);/g, (_m, name: string) => ENTITIES[name] ?? _m)\n}\n\n/**\n * Read one attribute off a single tag's source. Quote-aware; unquoted allowed.\n *\n * The name must be preceded by whitespace, not merely a word boundary: `-` is\n * not a word character, so `\\bhref` matches inside `data-href` — and lazy-\n * loading WordPress themes put the real URL in `data-src` and a placeholder in\n * `src` on almost every image.\n */\nexport function attr(tag: string, name: string): string | undefined {\n const re = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s\"'>]+))`, 'i')\n const match = re.exec(tag)\n if (!match) return undefined\n return decodeEntities(match[1] ?? match[2] ?? match[3] ?? '')\n}\n\n/** Every `<tag …>` opening tag in the source, as raw strings. */\nexport function tags(source: string, name: string): string[] {\n return [...source.matchAll(new RegExp(`<${name}\\\\b[^>]*>`, 'gi'))].map(m => m[0])\n}\n\n/**\n * `<title>` texts from the head. `<svg>` blocks are removed first: a `<title>`\n * inside one is an accessibility label, valid markup, and not the page title —\n * counting it would report a duplicate on every page with an inline icon.\n */\nexport function titles(html: string): string[] {\n const source = head(html).replace(/<svg\\b[\\s\\S]*?<\\/svg\\s*>/gi, '')\n return [...source.matchAll(/<title\\b[^>]*>([\\s\\S]*?)<\\/title\\s*>/gi)]\n .map(m => decodeEntities(m[1] ?? '').trim())\n}\n\n/** `<meta name=\"…\">` / `<meta property=\"…\">` content, keyed by lowercased key. */\nexport function metaMap(html: string): Map<string, string[]> {\n const found = new Map<string, string[]>()\n for (const tag of tags(head(html), 'meta')) {\n const key = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (!key) continue\n const content = attr(tag, 'content') ?? ''\n found.set(key, [...(found.get(key) ?? []), content])\n }\n return found\n}\n\nexport interface LinkTag {\n rel: string\n href: string\n hreflang?: string\n type?: string\n}\n\n/** `<link>` tags from the head, with `rel` lowercased. */\nexport function links(html: string): LinkTag[] {\n const found: LinkTag[] = []\n for (const tag of tags(head(html), 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase()\n const href = attr(tag, 'href')\n if (!rel || href === undefined) continue\n found.push({ rel, href, hreflang: attr(tag, 'hreflang'), type: attr(tag, 'type') })\n }\n return found\n}\n\n/** Links whose `rel` list contains the given token. */\nexport function linksRel(html: string, token: string): LinkTag[] {\n return links(html).filter(link => link.rel.split(/\\s+/).includes(token))\n}\n\nexport interface JsonLdBlock {\n raw: string\n /** Parsed value, or undefined when the block is not valid JSON. */\n value?: unknown\n}\n\n/** Every `application/ld+json` block in the document, parsed where possible. */\nexport function jsonLd(html: string): JsonLdBlock[] {\n const blocks: JsonLdBlock[] = []\n const re = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n for (const match of html.matchAll(re)) {\n const raw = (match[1] ?? '').trim()\n try {\n blocks.push({ raw, value: JSON.parse(raw) as unknown })\n } catch {\n blocks.push({ raw })\n }\n }\n return blocks\n}\n\n/**\n * `@type` values in a JSON-LD block, including those nested in `@graph` —\n * themes commonly ship one graph holding Organization, WebSite and WebPage,\n * and reading only the root would see none of them.\n */\nexport function jsonLdTypes(value: unknown): string[] {\n const found: string[] = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const item of node) walk(item)\n return\n }\n if (!node || typeof node !== 'object') return\n const record = node as Record<string, unknown>\n const type = record['@type']\n if (typeof type === 'string') found.push(type)\n else if (Array.isArray(type)) for (const t of type) if (typeof t === 'string') found.push(t)\n if ('@graph' in record) walk(record['@graph'])\n }\n walk(value)\n return found\n}\n\n/** `href` of every `<a>` in the document body. */\nexport function anchors(html: string): string[] {\n return tags(body(html), 'a')\n .map(tag => attr(tag, 'href'))\n .filter((href): href is string => href !== undefined)\n}\n\nexport interface ImageRef {\n src: string\n /** Absent means no `alt` attribute at all — different from `alt=\"\"`. */\n alt?: string\n}\n\n/** Every `<img>` in the document body. */\nexport function images(html: string): ImageRef[] {\n return tags(body(html), 'img')\n .map(tag => ({ src: attr(tag, 'src') ?? '', alt: attr(tag, 'alt') }))\n .filter(image => image.src !== '')\n}\n\n/** Visible text, roughly: script/style stripped, tags removed, runs collapsed. */\nexport function text(html: string): string {\n return body(html)\n .replace(/<(script|style)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** `<loc>` entries of a sitemap or sitemap index. */\nexport function sitemapLocations(xml: string): string[] {\n return [...xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi)].map(m => decodeEntities((m[1] ?? '').trim()))\n}\n","// ─── Addresses ───\n//\n// Every check that asks \"does this link go somewhere that exists\" is really\n// asking a normalisation question, and getting it wrong in either direction is\n// worse than not asking: too strict and a correct site fails its gate, too\n// loose and a broken one passes.\n//\n// The rules here are deliberately conservative. A trailing slash and an\n// `index.html` are the same address, because static hosts serve them that way.\n// A query string and a fragment are not part of a document's identity. Case is\n// preserved in the path, because static hosts are case-sensitive even when the\n// author assumed otherwise.\n\nconst SITE_FALLBACK = 'https://contentrain.invalid'\n\n/** Absolute URL for `href` as seen from `base`, or undefined when unresolvable. */\nexport function resolve(href: string, base: string, site?: string): string | undefined {\n try {\n const origin = site ?? SITE_FALLBACK\n const absoluteBase = base.startsWith('http') ? base : new URL(base, origin).href\n return new URL(href, absoluteBase).href\n } catch {\n return undefined\n }\n}\n\n/** Whether a resolved URL belongs to the site under test. */\nexport function isInternal(url: string, site?: string, extraHosts: readonly string[] = []): boolean {\n try {\n const parsed = new URL(url)\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false\n if (!site) return parsed.origin === SITE_FALLBACK || extraHosts.includes(parsed.host)\n return parsed.host === new URL(site).host || extraHosts.includes(parsed.host)\n } catch {\n return false\n }\n}\n\n/**\n * The identity of a document: path only, with `index.html` and a trailing\n * slash removed, query and fragment dropped. `/` stays `/`.\n */\nexport function identity(url: string, site?: string): string {\n const full = url.startsWith('http') ? url : resolve(url, '/', site) ?? url\n let path: string\n try {\n path = new URL(full).pathname\n } catch {\n path = full.split(/[?#]/)[0] ?? full\n }\n path = path.replace(/\\/index\\.html?$/i, '/')\n if (path.length > 1) path = path.replace(/\\/+$/, '')\n return path === '' ? '/' : path\n}\n\n/** Absolute form of a document's own address, when the site origin is known. */\nexport function absolute(url: string, site?: string): string | undefined {\n if (url.startsWith('http')) return url\n if (!site) return undefined\n try {\n return new URL(url, site).href\n } catch {\n return undefined\n }\n}\n","// ─── The gates ───\n//\n// Seven groups, from the migration contract's verification scope. Each check\n// has a stable id and answers one question; none of them fetches anything.\n//\n// Two rules shaped every check below. A finding is an `error` only when the\n// built site is *wrong* — a visitor or a crawler gets something it should not.\n// Everything that is merely worse than the old site is a `warning`, because a\n// migration that improves a page should not fail its own gate.\n\nimport type { Finding, VerifyDocument, VerifyInput, VerifyOptions } from './types.js'\nimport {\n anchors, images, jsonLd, jsonLdTypes, linksRel, metaMap, sitemapLocations, text, titles,\n} from './html.js'\nimport { absolute, identity, isInternal, resolve } from './url.js'\n\nexport interface Context {\n input: VerifyInput\n options: Required<Pick<VerifyOptions, 'maxRedirectHops' | 'soft404MaxTextLength'>> & VerifyOptions\n /** Document identity → document, for the built site. */\n byIdentity: Map<string, VerifyDocument>\n /** Baseline document identity → document. */\n baselineByIdentity: Map<string, VerifyDocument>\n /** Identities of everything the build serves: documents plus assets. */\n served: Set<string>\n}\n\nconst NOT_FOUND_TEXT = /\\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i\n\nfunction finding(f: Finding): Finding {\n return f\n}\n\n// ─── identity ───\n\nexport function identityChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const url = doc.url\n const group = 'identity' as const\n const found = titles(doc.html)\n\n if (found.every(t => t === '')) {\n out.push(finding({ group, check: 'identity.title-missing', severity: 'error', url, message: 'No non-empty <title> in the head.' }))\n } else if (found.length > 1) {\n out.push(finding({\n group,\n check: 'identity.title-duplicate',\n severity: 'error',\n url,\n message: `${found.length} <title> elements in the head — a crawler takes one and it is not defined which.`,\n detail: found.join(' | '),\n }))\n }\n\n const meta = metaMap(doc.html)\n const description = meta.get('description')?.[0]?.trim()\n if (!description) {\n out.push(finding({ group, check: 'identity.description-missing', severity: 'warning', url, message: 'No meta description.' }))\n }\n\n const canonicals = linksRel(doc.html, 'canonical')\n if (canonicals.length === 0) {\n out.push(finding({ group, check: 'identity.canonical-missing', severity: 'warning', url, message: 'No canonical link.' }))\n } else if (canonicals.length > 1) {\n out.push(finding({\n group,\n check: 'identity.canonical-duplicate',\n severity: 'error',\n url,\n message: `${canonicals.length} canonical links — the page tells crawlers two different things about itself.`,\n detail: canonicals.map(c => c.href).join(' | '),\n }))\n } else {\n // The failure this catches is a template's canonical surviving a clone:\n // every page canonicalises to one URL and the site removes itself from the\n // index. It is worse than having no canonical at all.\n const declared = resolve(canonicals[0]!.href, url, ctx.input.site)\n const self = absolute(url, ctx.input.site) ?? resolve(url, '/', ctx.input.site)\n if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) {\n out.push(finding({\n group,\n check: 'identity.canonical-mismatch',\n severity: 'error',\n url,\n message: 'Canonical points at a different page.',\n detail: `${canonicals[0]!.href} ≠ ${url}`,\n }))\n }\n }\n\n const missingOg = ['og:title', 'og:type', 'og:url'].filter(key => !meta.get(key)?.[0])\n if (missingOg.length) {\n out.push(finding({\n group,\n check: 'identity.open-graph-incomplete',\n severity: 'warning',\n url,\n message: `Open Graph incomplete — missing ${missingOg.join(', ')}. Shares render without a card.`,\n }))\n }\n if (!meta.get('twitter:card')?.[0]) {\n out.push(finding({ group, check: 'identity.twitter-card-missing', severity: 'info', url, message: 'No twitter:card.' }))\n }\n\n return out\n}\n\n// ─── indexing ───\n\n/** `noindex` as declared by a robots meta or an X-Robots-Tag header. */\nfunction noindexReason(doc: VerifyDocument): string | undefined {\n const robots = metaMap(doc.html).get('robots')?.join(',') ?? ''\n if (/\\bnoindex\\b/i.test(robots)) return `<meta name=\"robots\" content=\"${robots}\">`\n const header = doc.headers?.['x-robots-tag'] ?? ''\n if (/\\bnoindex\\b/i.test(header)) return `X-Robots-Tag: ${header}`\n return undefined\n}\n\nexport function indexingChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'indexing' as const\n const url = doc.url\n\n // A staging `noindex` carried into production is silent: the site builds,\n // deploys, serves — and disappears from search over the following weeks.\n const reason = noindexReason(doc)\n if (reason) {\n out.push(finding({ group, check: 'indexing.noindex', severity: 'error', url, message: 'Page is marked noindex.', detail: reason }))\n }\n\n if (ctx.input.sitemap !== undefined) {\n const listed = new Set(sitemapLocations(ctx.input.sitemap).map(loc => identity(loc, ctx.input.site)))\n if (!listed.has(identity(url, ctx.input.site)) && !reason) {\n out.push(finding({ group, check: 'indexing.sitemap-missing-entry', severity: 'warning', url, message: 'Indexable page is not in the sitemap.' }))\n }\n }\n\n return out\n}\n\n/** Sitemap entries pointing at pages the build does not serve. */\nexport function sitemapStaleChecks(ctx: Context): Finding[] {\n if (ctx.input.sitemap === undefined) return []\n const out: Finding[] = []\n for (const loc of sitemapLocations(ctx.input.sitemap)) {\n const id = identity(loc, ctx.input.site)\n if (ctx.byIdentity.has(id)) continue\n out.push(finding({\n group: 'indexing',\n check: 'indexing.sitemap-stale-entry',\n severity: 'warning',\n url: loc,\n message: 'Sitemap lists a page the build does not serve.',\n }))\n }\n return out\n}\n\n// ─── status ───\n\nexport function statusChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'status' as const\n const url = doc.url\n const status = doc.status ?? 200\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const was = before.status ?? 200\n if (was !== status) {\n out.push(finding({\n group,\n check: 'status.mismatch',\n severity: 'error',\n url,\n message: `Status changed: ${was} → ${status}.`,\n }))\n }\n }\n\n // A \"not found\" page answering 200 is invisible to every monitor that looks\n // at status codes, and a crawler indexes it as content.\n if (status === 200) {\n const bodyText = text(doc.html)\n const title = titles(doc.html)[0] ?? ''\n const saysNotFound = NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))\n if (saysNotFound && bodyText.length <= ctx.options.soft404MaxTextLength) {\n out.push(finding({\n group,\n check: 'status.soft-404',\n severity: 'error',\n url,\n message: 'Page says it was not found but answers 200.',\n detail: title || bodyText.slice(0, 120),\n }))\n }\n }\n\n return out\n}\n\n/** Redirect rules: chains, loops, and targets that do not exist. */\nexport function redirectChecks(ctx: Context): Finding[] {\n const rules = ctx.input.redirects\n if (!rules?.length) return []\n const out: Finding[] = []\n const group = 'status' as const\n const byFrom = new Map(rules.map(rule => [identity(rule.from, ctx.input.site), rule]))\n\n for (const rule of rules) {\n const seen = new Set<string>([identity(rule.from, ctx.input.site)])\n let hops = 1\n let target = identity(rule.to, ctx.input.site)\n\n while (byFrom.has(target)) {\n if (seen.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-loop',\n severity: 'error',\n url: rule.from,\n message: 'Redirect chain loops.',\n detail: [...seen, target].join(' → '),\n }))\n break\n }\n seen.add(target)\n target = identity(byFrom.get(target)!.to, ctx.input.site)\n hops += 1\n }\n\n if (hops > ctx.options.maxRedirectHops) {\n out.push(finding({\n group,\n check: 'status.redirect-chain',\n severity: 'warning',\n url: rule.from,\n message: `Redirect chain is ${hops} hops.`,\n detail: [...seen, target].join(' → '),\n }))\n }\n\n const external = !isInternal(resolve(rule.to, rule.from, ctx.input.site) ?? '', ctx.input.site, ctx.options.internalHosts)\n if (!external && !ctx.served.has(target) && !byFrom.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-target-missing',\n severity: 'error',\n url: rule.from,\n message: 'Redirect points at a page the build does not serve.',\n detail: rule.to,\n }))\n }\n }\n\n return out\n}\n\n// ─── international ───\n\n/**\n * hreflang has to be symmetric: if A offers B as its Turkish alternate, B must\n * offer A back. A one-sided declaration is ignored by search engines entirely,\n * so a half-migrated set of alternates is not a partial win — it is nothing.\n */\nexport function internationalChecks(ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'international' as const\n const declared = new Map<string, Map<string, string>>()\n\n for (const doc of ctx.input.documents) {\n const self = identity(doc.url, ctx.input.site)\n const alternates = new Map<string, string>()\n for (const link of linksRel(doc.html, 'alternate')) {\n if (!link.hreflang) continue\n const target = resolve(link.href, doc.url, ctx.input.site)\n if (!target) continue\n alternates.set(link.hreflang.toLowerCase(), identity(target, ctx.input.site))\n }\n if (alternates.size) declared.set(self, alternates)\n }\n\n for (const [self, alternates] of declared) {\n const listsSelf = [...alternates.values()].includes(self)\n if (!listsSelf) {\n out.push(finding({\n group,\n check: 'international.hreflang-no-self',\n severity: 'warning',\n url: self,\n message: 'Page declares hreflang alternates but does not list itself.',\n }))\n }\n for (const [lang, target] of alternates) {\n if (target === self) continue\n if (!ctx.byIdentity.has(target)) {\n out.push(finding({\n group,\n check: 'international.hreflang-unknown-target',\n severity: 'warning',\n url: self,\n message: `hreflang=\"${lang}\" points at a page the build does not serve.`,\n detail: target,\n }))\n continue\n }\n const back = declared.get(target)\n if (!back || ![...back.values()].includes(self)) {\n out.push(finding({\n group,\n check: 'international.hreflang-not-reciprocal',\n severity: 'error',\n url: self,\n message: `hreflang=\"${lang}\" names ${target}, which does not name it back — search engines ignore one-sided alternates.`,\n }))\n }\n }\n }\n\n return out\n}\n\n// ─── structured data ───\n\nexport function structuredChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'structured' as const\n const url = doc.url\n const blocks = jsonLd(doc.html)\n\n for (const block of blocks) {\n if (block.value === undefined) {\n out.push(finding({\n group,\n check: 'structured.jsonld-invalid',\n severity: 'error',\n url,\n message: 'A JSON-LD block does not parse — the whole block is discarded by consumers.',\n detail: block.raw.slice(0, 120),\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (!before) return out\n\n const had = new Set(jsonLd(before.html).flatMap(block => jsonLdTypes(block.value)))\n const has = new Set(blocks.flatMap(block => jsonLdTypes(block.value)))\n const lost = [...had].filter(type => !has.has(type))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'structured.type-lost',\n severity: 'warning',\n url,\n message: `Structured data types the old page had are gone: ${lost.join(', ')}.`,\n }))\n }\n\n return out\n}\n\n// ─── navigation ───\n\nexport function navigationChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'navigation' as const\n const url = doc.url\n const redirected = new Set((ctx.input.redirects ?? []).map(rule => identity(rule.from, ctx.input.site)))\n\n const broken = new Set<string>()\n for (const href of anchors(doc.html)) {\n if (/^(#|mailto:|tel:|javascript:|data:)/i.test(href)) continue\n const target = resolve(href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n const id = identity(target, ctx.input.site)\n if (ctx.served.has(id) || redirected.has(id)) continue\n broken.add(href)\n }\n for (const href of broken) {\n out.push(finding({ group, check: 'navigation.broken-internal-link', severity: 'error', url, message: 'Internal link goes nowhere.', detail: href }))\n }\n\n for (const token of ['next', 'prev'] as const) {\n for (const link of linksRel(doc.html, token)) {\n const target = resolve(link.href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n out.push(finding({\n group,\n check: 'navigation.pagination-broken',\n severity: 'warning',\n url,\n message: `rel=\"${token}\" points at a page the build does not serve.`,\n detail: link.href,\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const feedRel = (html: string) => linksRel(html, 'alternate').some(link => /rss|atom|feed/i.test(link.type ?? ''))\n if (feedRel(before.html) && !feedRel(doc.html)) {\n out.push(finding({ group, check: 'navigation.feed-lost', severity: 'warning', url, message: 'The old page advertised a feed; this one does not.' }))\n }\n }\n\n return out\n}\n\n// ─── assets ───\n\nexport function assetChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'assets' as const\n const url = doc.url\n const refs = images(doc.html)\n\n const broken = new Set<string>()\n for (const image of refs) {\n if (/^data:/i.test(image.src)) continue\n const target = resolve(image.src, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n broken.add(image.src)\n }\n for (const src of broken) {\n out.push(finding({ group, check: 'assets.broken-image', severity: 'error', url, message: 'Image does not resolve to anything the build serves.', detail: src }))\n }\n\n // `alt=\"\"` is a decision (decorative); no `alt` attribute at all is an\n // omission. Only the second is reported.\n const noAlt = refs.filter(image => image.alt === undefined)\n if (noAlt.length) {\n out.push(finding({\n group,\n check: 'assets.missing-alt',\n severity: 'warning',\n url,\n message: `${noAlt.length} image${noAlt.length === 1 ? '' : 's'} without an alt attribute.`,\n detail: noAlt.slice(0, 5).map(image => image.src).join(', '),\n }))\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const had = new Map(images(before.html).filter(i => i.alt).map(i => [identity(resolve(i.src, before.url, ctx.input.site) ?? i.src, ctx.input.site), i.alt!]))\n const now = new Map(refs.map(i => [identity(resolve(i.src, url, ctx.input.site) ?? i.src, ctx.input.site), i.alt]))\n const lost = [...had.keys()].filter(src => now.has(src) && !now.get(src))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'assets.alt-lost',\n severity: 'warning',\n url,\n message: `${lost.length} image${lost.length === 1 ? '' : 's'} lost alt text the old page had.`,\n detail: lost.slice(0, 5).join(', '),\n }))\n }\n }\n\n return out\n}\n","// ─── Running the gates ───\n\nimport type { CheckGroup, Finding, Severity, VerifyInput, VerifyReport } from './types.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { Context } from './checks.js'\nimport {\n assetChecks, identityChecks, indexingChecks, internationalChecks, navigationChecks,\n redirectChecks, sitemapStaleChecks, statusChecks, structuredChecks,\n} from './checks.js'\nimport { identity } from './url.js'\n\nconst DEFAULTS = { maxRedirectHops: 1, soft404MaxTextLength: 600 }\n\n/**\n * Checks that need an input the caller did not supply. Reported rather than\n * silently omitted: a green report over a run that never compared anything is\n * the most dangerous output this package could produce.\n */\nfunction absentInputs(input: VerifyInput, groups: readonly CheckGroup[]): { group: CheckGroup, reason: string }[] {\n const skipped: { group: CheckGroup, reason: string }[] = []\n if (!input.baseline) {\n for (const group of ['status', 'structured', 'navigation', 'assets'] as const) {\n if (groups.includes(group)) skipped.push({ group, reason: 'no baseline — parity checks did not run' })\n }\n }\n if (input.sitemap === undefined && groups.includes('indexing')) {\n skipped.push({ group: 'indexing', reason: 'no sitemap — membership checks did not run' })\n }\n if (!input.redirects?.length && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'no redirect rules — chain and target checks did not run' })\n }\n return skipped\n}\n\nfunction buildContext(input: VerifyInput): Context {\n const options = { ...DEFAULTS, ...input.options }\n const byIdentity = new Map(input.documents.map(doc => [identity(doc.url, input.site), doc]))\n const baselineByIdentity = new Map((input.baseline?.documents ?? []).map(doc => [identity(doc.url, input.site), doc]))\n const served = new Set<string>([\n ...byIdentity.keys(),\n ...(input.assets ?? []).map(asset => identity(asset, input.site)),\n ])\n return { input, options, byIdentity, baselineByIdentity, served }\n}\n\n/**\n * Run the gates over a built site.\n *\n * Nothing is fetched. `input.documents` is the built site as served, and\n * `input.baseline` — when the caller has one — is the site it replaced. Without\n * a baseline every self-check still runs; only the parity checks (status\n * change, lost structured data, lost alt text, lost feed) go quiet, because\n * there is nothing to compare against.\n */\nexport function verify(input: VerifyInput): VerifyReport {\n const groups = [...(input.groups ?? CHECK_GROUPS)]\n const ctx = buildContext(input)\n const findings: Finding[] = []\n const skipped = absentInputs(input, groups)\n\n const run = (group: CheckGroup, fn: () => Finding[]) => {\n if (!groups.includes(group)) return\n findings.push(...fn())\n }\n\n for (const doc of input.documents) {\n run('identity', () => identityChecks(doc, ctx))\n run('indexing', () => indexingChecks(doc, ctx))\n run('status', () => statusChecks(doc, ctx))\n run('structured', () => structuredChecks(doc, ctx))\n run('navigation', () => navigationChecks(doc, ctx))\n run('assets', () => assetChecks(doc, ctx))\n }\n\n // Site-wide checks: these read the whole set, not one document.\n run('indexing', () => sitemapStaleChecks(ctx))\n run('status', () => redirectChecks(ctx))\n run('international', () => internationalChecks(ctx))\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, info: 0 }\n for (const item of findings) counts[item.severity] += 1\n\n return {\n passed: counts.error === 0,\n findings,\n counts,\n documents: input.documents.length,\n groups,\n skipped,\n }\n}\n\n/** A report as lines, most severe first. Used by the CLI and by consumers. */\nexport function formatReport(report: VerifyReport): string {\n const order: Severity[] = ['error', 'warning', 'info']\n const lines: string[] = []\n for (const severity of order) {\n for (const item of report.findings.filter(f => f.severity === severity)) {\n const where = item.url ? ` ${item.url}` : ''\n const detail = item.detail ? `\\n ${item.detail}` : ''\n lines.push(`${severity.toUpperCase().padEnd(7)} ${item.check}${where}\\n ${item.message}${detail}`)\n }\n }\n for (const { group, reason } of report.skipped) {\n lines.push(`SKIPPED ${group} — ${reason}`)\n }\n lines.push(\n `\\n${report.documents} document${report.documents === 1 ? '' : 's'} · `\n + `${report.counts.error} error · ${report.counts.warning} warning · ${report.counts.info} info · `\n + (report.passed ? 'PASS' : 'FAIL'),\n )\n return lines.join('\\n')\n}\n","// ─── Reading a built directory ───\n//\n// The one place this package touches a filesystem. Kept apart from the checks\n// so the engine stays pure: a consumer that already holds the documents — a\n// migration runner, a CI step with an artefact — imports `verify` and never\n// loads this module.\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\nimport type { VerifyDocument, VerifyInput } from './types.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\n\nasync function walk(dir: string, root: string, files: string[] = []): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(full, root, files)\n continue\n }\n files.push(relative(root, full))\n }\n return files\n}\n\n/**\n * Load a static build as a verification input: every `.html` becomes a\n * document served at its path, everything else becomes an asset.\n *\n * `dist/about/index.html` is served at `/about/`, which `identity()` then\n * reduces to `/about` — the same address a link to `/about/` or\n * `/about/index.html` resolves to.\n */\nexport async function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n let sitemap: string | undefined\n\n for (const file of files.toSorted()) {\n const served = `/${file.split(sep).join('/')}`\n if (DOCUMENT_EXT.test(file)) {\n documents.push({ url: served, html: await readFile(join(dir, file), 'utf8'), status: 200 })\n continue\n }\n if (/^\\/sitemap[^/]*\\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), 'utf8')\n assets.push(served)\n }\n\n return { documents, assets, sitemap, site }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/** One page as served: the address it answers at and what it answered with. */
|
|
3
|
+
interface VerifyDocument {
|
|
4
|
+
/** Absolute URL, or a site-relative path (`/about/`). */
|
|
5
|
+
url: string;
|
|
6
|
+
html: string;
|
|
7
|
+
/** Defaults to 200 when the caller did not record one. */
|
|
8
|
+
status?: number;
|
|
9
|
+
/** Response headers, lowercased keys. `x-robots-tag` is read here. */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface VerifyRedirect {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
status: number;
|
|
16
|
+
}
|
|
17
|
+
declare const CHECK_GROUPS: readonly ["identity", "indexing", "status", "international", "structured", "navigation", "assets"];
|
|
18
|
+
type CheckGroup = (typeof CHECK_GROUPS)[number];
|
|
19
|
+
type Severity = 'error' | 'warning' | 'info';
|
|
20
|
+
/**
|
|
21
|
+
* One thing that is wrong, or worth knowing.
|
|
22
|
+
*
|
|
23
|
+
* `check` is a stable id (`identity.canonical-mismatch`) so a consumer can
|
|
24
|
+
* suppress, threshold or track one finding without matching on prose.
|
|
25
|
+
*/
|
|
26
|
+
interface Finding {
|
|
27
|
+
group: CheckGroup;
|
|
28
|
+
check: string;
|
|
29
|
+
severity: Severity;
|
|
30
|
+
/** The document the finding is about, when it is about one. */
|
|
31
|
+
url?: string;
|
|
32
|
+
message: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
}
|
|
35
|
+
interface VerifyReport {
|
|
36
|
+
/** No `error` findings. This is the gate. */
|
|
37
|
+
passed: boolean;
|
|
38
|
+
findings: Finding[];
|
|
39
|
+
counts: Record<Severity, number>;
|
|
40
|
+
/** How many documents were examined. */
|
|
41
|
+
documents: number;
|
|
42
|
+
/** Which groups actually ran. */
|
|
43
|
+
groups: CheckGroup[];
|
|
44
|
+
/** Groups that were asked for but could not run, and why. */
|
|
45
|
+
skipped: {
|
|
46
|
+
group: CheckGroup;
|
|
47
|
+
reason: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
interface VerifyOptions {
|
|
51
|
+
/**
|
|
52
|
+
* A redirect chain longer than this is reported. One hop is normal; two is a
|
|
53
|
+
* link-equity leak and a latency cost, and chains grow silently as redirect
|
|
54
|
+
* rules accumulate over migrations.
|
|
55
|
+
*/
|
|
56
|
+
maxRedirectHops?: number;
|
|
57
|
+
/**
|
|
58
|
+
* A 200 response shorter than this many characters of text, whose title or
|
|
59
|
+
* body says "not found", is treated as a soft-404. Kept configurable because
|
|
60
|
+
* a legitimately short page exists; the default is deliberately low.
|
|
61
|
+
*/
|
|
62
|
+
soft404MaxTextLength?: number;
|
|
63
|
+
/** Hosts to treat as internal besides `site`'s own. */
|
|
64
|
+
internalHosts?: string[];
|
|
65
|
+
}
|
|
66
|
+
interface VerifyInput {
|
|
67
|
+
/** The built site. */
|
|
68
|
+
documents: VerifyDocument[];
|
|
69
|
+
/** The site's canonical origin (`https://example.com`). Several checks need it. */
|
|
70
|
+
site?: string;
|
|
71
|
+
/** `sitemap.xml` as produced by the build. */
|
|
72
|
+
sitemap?: string;
|
|
73
|
+
/** Redirect rules the host will serve. */
|
|
74
|
+
redirects?: VerifyRedirect[];
|
|
75
|
+
/** Non-document files the build produced, as served (`/images/a.png`). */
|
|
76
|
+
assets?: string[];
|
|
77
|
+
/** The old site, for parity checks. Absent means only self-checks run. */
|
|
78
|
+
baseline?: {
|
|
79
|
+
documents: VerifyDocument[];
|
|
80
|
+
};
|
|
81
|
+
/** Defaults to every group. */
|
|
82
|
+
groups?: readonly CheckGroup[];
|
|
83
|
+
options?: VerifyOptions;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { VerifyDocument as a, VerifyRedirect as c, Severity as i, VerifyReport as l, CheckGroup as n, VerifyInput as o, Finding as r, VerifyOptions as s, CHECK_GROUPS as t };
|
|
87
|
+
//# sourceMappingURL=types-C_hmH1n3.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-C_hmH1n3.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;UAaiB,cAAA;EAAc;EAE7B,GAAA;EACA,IAAA;EADA;EAGA,MAAA;EAAA;EAEA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,cAAA;EACf,IAAA;EACA,EAAA;EACA,MAAA;AAAA;AAAA,cAGW,YAAA;AAAA,KAiBD,UAAA,WAAqB,YAAA;AAAA,KAErB,QAAA;;;;AAnBZ;;;UA2BiB,OAAA;EACf,KAAA,EAAO,UAAA;EACP,KAAA;EACA,QAAA,EAAU,QAAA;;EAEV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EAlBG;EAoBlB,MAAA;EACA,QAAA,EAAU,OAAA;EACV,MAAA,EAAQ,MAAA,CAAO,QAAA;;EAEf,SAAA;EAfA;EAiBA,MAAA,EAAQ,UAAA;EAhBR;EAkBA,OAAA;IAAW,KAAA,EAAO,UAAA;IAAY,MAAA;EAAA;AAAA;AAAA,UAGf,aAAA;EAhBT;AAGR;;;;EAmBE,eAAA;EAfQ;;;;;EAqBR,oBAAA;EAtBA;EAwBA,aAAA;AAAA;AAAA,UAGe,WAAA;EA1BA;EA4Bf,SAAA,EAAW,cAAA;EAxBX;EA0BA,IAAA;EAxBA;EA0BA,OAAA;EA1BkB;EA4BlB,SAAA,GAAY,cAAA;EA5BwB;EA8BpC,MAAA;EA3Be;EA6Bf,QAAA;IAAa,SAAA,EAAW,cAAA;EAAA;EAvBxB;EAyBA,MAAA,YAAkB,UAAA;EAClB,OAAA,GAAU,aAAA;AAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contentrain/verify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Portable SEO, indexing, link and asset gates for a migrated site — offline, zero-dependency",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Contentrain/ai.git",
|
|
10
|
+
"directory": "packages/verify"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://ai.contentrain.io",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Contentrain/ai/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"contentrain",
|
|
18
|
+
"seo",
|
|
19
|
+
"verify",
|
|
20
|
+
"migration",
|
|
21
|
+
"parity",
|
|
22
|
+
"hreflang",
|
|
23
|
+
"sitemap"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"import": "./dist/index.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/index.mjs",
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"bin": {
|
|
38
|
+
"contentrain-verify": "./dist/bin.mjs"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts",
|
|
45
|
+
"dev": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts --watch",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"clean": "rm -rf dist"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.10.0",
|
|
52
|
+
"tsdown": "^0.21.0",
|
|
53
|
+
"typescript": "^5.7.0"
|
|
54
|
+
}
|
|
55
|
+
}
|