@contentrain/verify 0.2.0 → 0.3.1
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 +11 -2
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{load-DUyyLerx.mjs → load-L1C52508.mjs} +131 -22
- package/dist/load-L1C52508.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/load-DUyyLerx.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -50,6 +50,14 @@ import { loadSiteDirectory, verify } from '@contentrain/verify'
|
|
|
50
50
|
const report = verify(await loadSiteDirectory('dist', 'https://example.com'))
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
A build usually writes a sitemap index rather than one `sitemap.xml` —
|
|
54
|
+
`@astrojs/sitemap` writes `sitemap-index.xml` naming `sitemap-0.xml`, … — so the
|
|
55
|
+
loader follows the index and hands `verify` the pages every named sitemap lists.
|
|
56
|
+
A sitemap the index names but the build does not serve is reported as a stale
|
|
57
|
+
entry. Passed to `verify` directly, a bare index lists files rather than pages,
|
|
58
|
+
so the membership checks skip and say so instead of reporting every page
|
|
59
|
+
missing.
|
|
60
|
+
|
|
53
61
|
## The checks
|
|
54
62
|
|
|
55
63
|
Severity is decided by one rule: **error** means the built site is wrong — a
|
|
@@ -63,15 +71,16 @@ a page must not fail its own gate.
|
|
|
63
71
|
| | `identity.title-duplicate` | error | Two titles in the head — a crawler picks one, undefined which. A `<title>` inside an inline `<svg>` is an accessibility label and is not counted |
|
|
64
72
|
| | `identity.canonical-missing` | warning | No canonical link |
|
|
65
73
|
| | `identity.canonical-duplicate` | error | Two canonicals: the page says two things about itself |
|
|
66
|
-
| | `identity.canonical-mismatch` | error | **The clone failure.** A page carrying the template's canonical points the whole site at one URL and removes it from the index. Invisible in a browser, worse than having no canonical |
|
|
74
|
+
| | `identity.canonical-mismatch` | error | **The clone failure.** A page carrying the template's canonical points the whole site at one URL and removes it from the index. Invisible in a browser, worse than having no canonical. When the baseline page declared the same canonical target (a syndicated post pointing at its original), the finding is `info`: the source's decision was carried over |
|
|
67
75
|
| | `identity.description-missing` | warning | No meta description |
|
|
68
76
|
| | `identity.open-graph-incomplete` | warning | Missing `og:title` / `og:type` / `og:url` — shares render without a card |
|
|
69
77
|
| | `identity.twitter-card-missing` | info | No `twitter:card` |
|
|
70
78
|
| identity | `identity.lang-missing` | warning | No `lang` on `<html>` — what a screen reader picks a voice from |
|
|
71
|
-
| indexing | `indexing.noindex` | error | **The staging leak.** A `noindex` in the meta or `X-Robots-Tag` survives deploy and the site disappears from search over the following weeks |
|
|
79
|
+
| indexing | `indexing.noindex` | error | **The staging leak.** A `noindex` in the meta or `X-Robots-Tag` survives deploy and the site disappears from search over the following weeks. When the baseline page was noindex too, the finding is `info`: the page was deliberately kept out of search |
|
|
72
80
|
| | `indexing.sitemap-missing-entry` | warning | An indexable page is not in the sitemap (a `noindex` page is not asked to be) |
|
|
73
81
|
| | `indexing.sitemap-stale-entry` | warning | The sitemap lists a page the build does not serve |
|
|
74
82
|
| status | `status.mismatch` | error | The status changed against the baseline |
|
|
83
|
+
| | `status.baseline-page-missing` | error | A page the baseline answered with 2xx that the build does not serve and no redirect rule covers. `status.mismatch` only sees pages present on both sides, so a page the migration never produced would otherwise be reported by nothing. Only checked for a build (`input.build`) — a capture may simply not include a page; the old site's 404 page is not counted |
|
|
75
84
|
| | `status.soft-404` | error | A "not found" page answering 200 — invisible to every status monitor, indexed as content |
|
|
76
85
|
| | `status.redirect-chain` | warning | More hops than `maxRedirectHops` (default 1) |
|
|
77
86
|
| | `status.redirect-loop` | error | A chain that returns to itself |
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-
|
|
1
|
+
import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-L1C52508.mjs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
//#region src/cli.ts
|
|
4
4
|
const USAGE = `contentrain-verify <dist-dir> [options]
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/verify.ts","../src/load.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/verify.ts","../src/load.ts"],"mappings":";;;;;AAsEA;;;;;;;iBAAgB,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,YAAA;;iBA0C5B,YAAA,CAAa,MAAA,EAAQ,YAAA;;;;;AA1CrC;;;;;;iBCjCsB,iBAAA,CAAkB,GAAA,UAAa,IAAA,YAAgB,OAAA,CAAQ,IAAA,CAAK,WAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-
|
|
1
|
+
import { i as CHECK_GROUPS, n as formatReport, r as verify, t as loadSiteDirectory } from "./load-L1C52508.mjs";
|
|
2
2
|
export { CHECK_GROUPS, formatReport, loadSiteDirectory, verify };
|
|
@@ -148,6 +148,14 @@ function text(html) {
|
|
|
148
148
|
function sitemapLocations(xml) {
|
|
149
149
|
return [...xml.matchAll(/<loc>\s*([\s\S]*?)\s*<\/loc>/gi)].map((m) => decodeEntities((m[1] ?? "").trim()));
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether a sitemap document is an index — a list of other sitemaps rather
|
|
153
|
+
* than of pages. Its `<loc>` entries are sitemap files, so reading them as page
|
|
154
|
+
* addresses reports every page missing and every child sitemap stale.
|
|
155
|
+
*/
|
|
156
|
+
function isSitemapIndex(xml) {
|
|
157
|
+
return /<sitemapindex[\s>]/i.test(xml);
|
|
158
|
+
}
|
|
151
159
|
//#endregion
|
|
152
160
|
//#region src/url.ts
|
|
153
161
|
const SITE_FALLBACK = "https://contentrain.invalid";
|
|
@@ -218,6 +226,18 @@ function isNotFoundDocument(doc, ctx) {
|
|
|
218
226
|
function finding(f) {
|
|
219
227
|
return f;
|
|
220
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Where a canonical points, comparable between the old site and the new one:
|
|
231
|
+
* the path when it stays on the host of the page that declares it (the two
|
|
232
|
+
* sites live on different origins), origin and path when it leaves it.
|
|
233
|
+
*/
|
|
234
|
+
function canonicalTarget(href, docUrl, site) {
|
|
235
|
+
const declared = resolve(href, docUrl, site);
|
|
236
|
+
const own = absolute(docUrl, site) ?? resolve(docUrl, "/", site);
|
|
237
|
+
if (!declared || !own) return void 0;
|
|
238
|
+
const host = new URL(declared);
|
|
239
|
+
return host.host === new URL(own).host ? identity(declared, site) : host.origin + identity(declared, site);
|
|
240
|
+
}
|
|
221
241
|
function identityChecks(doc, ctx) {
|
|
222
242
|
const out = [];
|
|
223
243
|
const url = doc.url;
|
|
@@ -265,14 +285,20 @@ function identityChecks(doc, ctx) {
|
|
|
265
285
|
else {
|
|
266
286
|
const declared = resolve(canonicals[0].href, url, ctx.input.site);
|
|
267
287
|
const self = absolute(url, ctx.input.site) ?? resolve(url, "/", ctx.input.site);
|
|
268
|
-
if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site))
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
url,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
288
|
+
if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) {
|
|
289
|
+
const target = canonicalTarget(canonicals[0].href, url, ctx.input.site);
|
|
290
|
+
const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site));
|
|
291
|
+
const beforeCanonicals = before ? linksRel(before.html, "canonical") : [];
|
|
292
|
+
const kept = target !== void 0 && beforeCanonicals.length === 1 && canonicalTarget(beforeCanonicals[0].href, before.url, ctx.input.site) === target;
|
|
293
|
+
out.push(finding({
|
|
294
|
+
group,
|
|
295
|
+
check: "identity.canonical-mismatch",
|
|
296
|
+
severity: kept ? "info" : "error",
|
|
297
|
+
url,
|
|
298
|
+
message: kept ? "Canonical points at a different page, as it did on the baseline — kept." : "Canonical points at a different page.",
|
|
299
|
+
detail: `${canonicals[0].href} ≠ ${url}`
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
276
302
|
}
|
|
277
303
|
if (!/<html\b[^>]*\slang\s*=\s*["']?[a-zA-Z]/.test(doc.html)) out.push(finding({
|
|
278
304
|
group,
|
|
@@ -314,15 +340,19 @@ function indexingChecks(doc, ctx) {
|
|
|
314
340
|
const group = "indexing";
|
|
315
341
|
const url = doc.url;
|
|
316
342
|
const reason = noindexReason(doc);
|
|
317
|
-
if (reason)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
343
|
+
if (reason) {
|
|
344
|
+
const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site));
|
|
345
|
+
const kept = before !== void 0 && noindexReason(before) !== void 0;
|
|
346
|
+
out.push(finding({
|
|
347
|
+
group,
|
|
348
|
+
check: "indexing.noindex",
|
|
349
|
+
severity: kept ? "info" : "error",
|
|
350
|
+
url,
|
|
351
|
+
message: kept ? "Page is marked noindex, as it was on the baseline — kept." : "Page is marked noindex.",
|
|
352
|
+
detail: reason
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
if (ctx.input.sitemap !== void 0 && !isSitemapIndex(ctx.input.sitemap)) {
|
|
326
356
|
if (!new Set(sitemapLocations(ctx.input.sitemap).map((loc) => identity(loc, ctx.input.site))).has(identity(url, ctx.input.site)) && !reason && !isNotFoundDocument(doc, ctx)) out.push(finding({
|
|
327
357
|
group,
|
|
328
358
|
check: "indexing.sitemap-missing-entry",
|
|
@@ -335,7 +365,7 @@ function indexingChecks(doc, ctx) {
|
|
|
335
365
|
}
|
|
336
366
|
/** Sitemap entries pointing at pages the build does not serve. */
|
|
337
367
|
function sitemapStaleChecks(ctx) {
|
|
338
|
-
if (ctx.input.sitemap === void 0) return [];
|
|
368
|
+
if (ctx.input.sitemap === void 0 || isSitemapIndex(ctx.input.sitemap)) return [];
|
|
339
369
|
const out = [];
|
|
340
370
|
for (const loc of sitemapLocations(ctx.input.sitemap)) {
|
|
341
371
|
const id = identity(loc, ctx.input.site);
|
|
@@ -403,6 +433,43 @@ function notFoundPageChecks(ctx) {
|
|
|
403
433
|
message: "The build has no 404.html, so a wrong path lands on the host’s page instead of the site’s."
|
|
404
434
|
})];
|
|
405
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Pages the old site served that the build does not, with no redirect for them.
|
|
438
|
+
*
|
|
439
|
+
* `status.mismatch` walks the build's own documents, so it can only compare
|
|
440
|
+
* pages that exist on both sides. A page that the migration never produced is
|
|
441
|
+
* not a document here at all, and nothing would ever report it — while every
|
|
442
|
+
* link and every search result pointing at it now ends on a 404. This walks the
|
|
443
|
+
* baseline instead.
|
|
444
|
+
*
|
|
445
|
+
* Only a build is the whole site. A set of captured pages may simply not
|
|
446
|
+
* include one, so asserting absence there would report a defect the input
|
|
447
|
+
* cannot express. Only baseline pages that answered 2xx count: a redirect or an
|
|
448
|
+
* error on the old site was not a page anyone could land on. A redirect rule
|
|
449
|
+
* for the address covers it — where it leads is `redirectChecks`' question.
|
|
450
|
+
*/
|
|
451
|
+
function baselineCoverageChecks(ctx) {
|
|
452
|
+
if (!ctx.input.build || !ctx.input.baseline) return [];
|
|
453
|
+
const redirected = new Set((ctx.input.redirects ?? []).map((rule) => identity(rule.from, ctx.input.site)));
|
|
454
|
+
const out = [];
|
|
455
|
+
const reported = /* @__PURE__ */ new Set();
|
|
456
|
+
for (const before of ctx.input.baseline.documents) {
|
|
457
|
+
const status = before.status ?? 200;
|
|
458
|
+
if (status < 200 || status >= 300) continue;
|
|
459
|
+
const id = identity(before.url, ctx.input.site);
|
|
460
|
+
if (id === "/404" || id === "/404.html") continue;
|
|
461
|
+
if (ctx.served.has(id) || redirected.has(id) || reported.has(id)) continue;
|
|
462
|
+
reported.add(id);
|
|
463
|
+
out.push(finding({
|
|
464
|
+
group: "status",
|
|
465
|
+
check: "status.baseline-page-missing",
|
|
466
|
+
severity: "error",
|
|
467
|
+
url: before.url,
|
|
468
|
+
message: "The old site served this page; the build does not, and no redirect covers it."
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
406
473
|
/** Redirect rules: chains, loops, and targets that do not exist. */
|
|
407
474
|
function redirectChecks(ctx) {
|
|
408
475
|
const rules = ctx.input.redirects;
|
|
@@ -711,6 +778,10 @@ function absentInputs(input, groups) {
|
|
|
711
778
|
group: "indexing",
|
|
712
779
|
reason: "no sitemap — membership checks did not run"
|
|
713
780
|
});
|
|
781
|
+
else if (input.sitemap !== void 0 && isSitemapIndex(input.sitemap) && groups.includes("indexing")) skipped.push({
|
|
782
|
+
group: "indexing",
|
|
783
|
+
reason: "sitemap is an index without its child sitemaps — membership checks did not run"
|
|
784
|
+
});
|
|
714
785
|
if (!input.redirects?.length && groups.includes("status")) skipped.push({
|
|
715
786
|
group: "status",
|
|
716
787
|
reason: "no redirect rules — chain and target checks did not run"
|
|
@@ -719,6 +790,10 @@ function absentInputs(input, groups) {
|
|
|
719
790
|
group: "status",
|
|
720
791
|
reason: "not a build directory — the 404 page check did not run"
|
|
721
792
|
});
|
|
793
|
+
if (input.baseline && !input.build && groups.includes("status")) skipped.push({
|
|
794
|
+
group: "status",
|
|
795
|
+
reason: "not a build directory — pages the baseline has and the input lacks were not reported"
|
|
796
|
+
});
|
|
722
797
|
if (!input.options?.sourceOrigin && groups.includes("assets")) skipped.push({
|
|
723
798
|
group: "assets",
|
|
724
799
|
reason: "no sourceOrigin — the old-host reference scan did not run"
|
|
@@ -772,6 +847,7 @@ function verify(input) {
|
|
|
772
847
|
run("indexing", () => sitemapStaleChecks(ctx));
|
|
773
848
|
run("status", () => redirectChecks(ctx));
|
|
774
849
|
run("status", () => notFoundPageChecks(ctx));
|
|
850
|
+
run("status", () => baselineCoverageChecks(ctx));
|
|
775
851
|
run("international", () => internationalChecks(ctx));
|
|
776
852
|
run("assets", () => sourceOriginChecks(ctx));
|
|
777
853
|
const counts = {
|
|
@@ -835,7 +911,7 @@ async function loadSiteDirectory(dir, site) {
|
|
|
835
911
|
const documents = [];
|
|
836
912
|
const assets = [];
|
|
837
913
|
const texts = [];
|
|
838
|
-
|
|
914
|
+
const sitemaps = /* @__PURE__ */ new Map();
|
|
839
915
|
for (const file of files.toSorted()) {
|
|
840
916
|
const served = `/${file.split(sep).join("/")}`;
|
|
841
917
|
if (DOCUMENT_EXT.test(file)) {
|
|
@@ -846,7 +922,7 @@ async function loadSiteDirectory(dir, site) {
|
|
|
846
922
|
});
|
|
847
923
|
continue;
|
|
848
924
|
}
|
|
849
|
-
if (/^\/sitemap[^/]*\.xml$/i.test(served))
|
|
925
|
+
if (/^\/sitemap[^/]*\.xml$/i.test(served)) sitemaps.set(served, await readFile(join(dir, file), "utf8"));
|
|
850
926
|
if (TEXT_EXT.test(file)) texts.push({
|
|
851
927
|
path: served,
|
|
852
928
|
content: await readFile(join(dir, file), "utf8")
|
|
@@ -857,12 +933,45 @@ async function loadSiteDirectory(dir, site) {
|
|
|
857
933
|
documents,
|
|
858
934
|
assets,
|
|
859
935
|
files: texts,
|
|
860
|
-
sitemap,
|
|
936
|
+
sitemap: combineSitemaps(sitemaps, site),
|
|
861
937
|
site,
|
|
862
938
|
build: true
|
|
863
939
|
};
|
|
864
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Every page address a build's sitemap files list, as one `<urlset>`.
|
|
943
|
+
*
|
|
944
|
+
* A build rarely writes a single `sitemap.xml`. `@astrojs/sitemap` writes
|
|
945
|
+
* `sitemap-index.xml` naming `sitemap-0.xml`, `sitemap-1.xml`, …; reading any
|
|
946
|
+
* one of those files alone gives either no pages or some of them, and the
|
|
947
|
+
* membership checks then report pages missing that are listed.
|
|
948
|
+
*
|
|
949
|
+
* When there is an index, only the sitemaps it names count — that is what a
|
|
950
|
+
* crawler following it reads. A named sitemap the build does not serve is kept
|
|
951
|
+
* as an entry, so the stale-entry check reports it instead of it disappearing.
|
|
952
|
+
*/
|
|
953
|
+
function combineSitemaps(sitemaps, site) {
|
|
954
|
+
if (sitemaps.size === 0) return void 0;
|
|
955
|
+
const documents = [...sitemaps.values()];
|
|
956
|
+
const indexes = documents.filter(isSitemapIndex);
|
|
957
|
+
const locations = [];
|
|
958
|
+
for (const xml of indexes.length > 0 ? indexes : documents) {
|
|
959
|
+
if (!isSitemapIndex(xml)) {
|
|
960
|
+
locations.push(...sitemapLocations(xml));
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
for (const child of sitemapLocations(xml)) {
|
|
964
|
+
const content = sitemaps.get(identity(child, site));
|
|
965
|
+
if (content === void 0 || isSitemapIndex(content)) locations.push(child);
|
|
966
|
+
else locations.push(...sitemapLocations(content));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return `<urlset>${locations.map((loc) => `<url><loc>${escapeXml(loc)}</loc></url>`).join("")}</urlset>`;
|
|
970
|
+
}
|
|
971
|
+
function escapeXml(value) {
|
|
972
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
973
|
+
}
|
|
865
974
|
//#endregion
|
|
866
975
|
export { CHECK_GROUPS as i, formatReport as n, verify as r, loadSiteDirectory as t };
|
|
867
976
|
|
|
868
|
-
//# sourceMappingURL=load-
|
|
977
|
+
//# sourceMappingURL=load-L1C52508.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-L1C52508.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\n/** A text file that is not a document — a stylesheet, a script. */\nexport interface VerifyFile {\n /** Path as served (`/assets/app.css`). */\n path: string\n content: 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 * The origin the content was migrated away from — the old WordPress host.\n *\n * A migrated page that still points at it is not finished: the new site\n * depends on the old one staying up, and the moment it does not, images and\n * stylesheets disappear. Measured across a blind cohort it was the single\n * largest blocker, so this is checked as an error when the origin is known.\n */\n sourceOrigin?: string\n /**\n * Hosts that may legitimately still be referenced even though they are not\n * the site's own — a CDN the customer keeps. Applies to\n * `assets.source-origin-reference` only.\n */\n allowHosts?: 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 /**\n * Contents of non-document text files — stylesheets, scripts. Only the checks\n * that scan content read these; everything else works from `documents` and\n * the `assets` path list.\n *\n * Supplying them matters for `assets.source-origin-reference`: a `url()` in a\n * stylesheet pointing at the old host is exactly as fatal as one in the HTML,\n * and a scan that silently skips CSS reports a smaller number than a scan\n * that does not — which is worse than not counting at all when two tools\n * compare figures.\n */\n files?: VerifyFile[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /**\n * Whether `documents` are a static build directory rather than pages captured\n * from a running site. Build-level expectations — a `404.html` exists — are\n * only meaningful for a build, and asserting them against a capture would\n * report a defect the capture simply cannot show. `loadSiteDirectory` sets it.\n */\n build?: boolean\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\n/**\n * Whether a sitemap document is an index — a list of other sitemaps rather\n * than of pages. Its `<loc>` entries are sitemap files, so reading them as page\n * addresses reports every page missing and every child sitemap stale.\n */\nexport function isSitemapIndex(xml: string): boolean {\n return /<sitemapindex[\\s>]/i.test(xml)\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, isSitemapIndex, 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\n/**\n * The build's 404 page, which is not served at its own address.\n *\n * A static host hands this document to a visitor who asked for something else,\n * so every check that assumes \"this document is served at this URL\" is wrong\n * about it: its canonical does not describe a page, it does not belong in the\n * sitemap, and — the one that matters — it says \"not found\" and is short by\n * design, which is precisely the shape of a soft-404. Without this exemption\n * every correctly built site would fail on having done the right thing.\n */\nfunction isNotFoundDocument(doc: VerifyDocument, ctx: Context): boolean {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n}\n\nfunction finding(f: Finding): Finding {\n return f\n}\n\n// ─── identity ───\n\n/**\n * Where a canonical points, comparable between the old site and the new one:\n * the path when it stays on the host of the page that declares it (the two\n * sites live on different origins), origin and path when it leaves it.\n */\nfunction canonicalTarget(href: string, docUrl: string, site?: string): string | undefined {\n const declared = resolve(href, docUrl, site)\n const own = absolute(docUrl, site) ?? resolve(docUrl, '/', site)\n if (!declared || !own) return undefined\n const host = new URL(declared)\n return host.host === new URL(own).host ? identity(declared, site) : host.origin + identity(declared, site)\n}\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 (isNotFoundDocument(doc, ctx)) {\n // No address of its own, so nothing to be canonical about.\n } else 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 // The same decision on the old site — syndicated content canonicalising\n // to its original — is the source's intent, carried over, not a clone's\n // leftover. Kept visible, not failed.\n const target = canonicalTarget(canonicals[0]!.href, url, ctx.input.site)\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n const beforeCanonicals = before ? linksRel(before.html, 'canonical') : []\n const kept = target !== undefined && beforeCanonicals.length === 1\n && canonicalTarget(beforeCanonicals[0]!.href, before!.url, ctx.input.site) === target\n out.push(finding({\n group,\n check: 'identity.canonical-mismatch',\n severity: kept ? 'info' : 'error',\n url,\n message: kept ? 'Canonical points at a different page, as it did on the baseline — kept.' : 'Canonical points at a different page.',\n detail: `${canonicals[0]!.href} ≠ ${url}`,\n }))\n }\n }\n\n // `<html lang>` is what a screen reader picks a voice from and what a\n // translation prompt keys on. A migrated page that lost it is not visibly\n // broken, which is why it stays lost.\n if (!/<html\\b[^>]*\\slang\\s*=\\s*[\"']?[a-zA-Z]/.test(doc.html)) {\n out.push(finding({ group, check: 'identity.lang-missing', severity: 'warning', url, message: 'No lang attribute on <html>.' }))\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 // A page the old site already kept out of search — a thank-you page, an\n // internal archive — is the source's decision, carried over. Kept visible,\n // not failed: the leak is a noindex the old site did not have.\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n const kept = before !== undefined && noindexReason(before) !== undefined\n out.push(finding({\n group,\n check: 'indexing.noindex',\n severity: kept ? 'info' : 'error',\n url,\n message: kept ? 'Page is marked noindex, as it was on the baseline — kept.' : 'Page is marked noindex.',\n detail: reason,\n }))\n }\n\n if (ctx.input.sitemap !== undefined && !isSitemapIndex(ctx.input.sitemap)) {\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 && !isNotFoundDocument(doc, ctx)) {\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 || isSitemapIndex(ctx.input.sitemap)) 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 && !isNotFoundDocument(doc, ctx)) {\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/**\n * A build with no `404.html`.\n *\n * Only meaningful for a build: a set of pages captured from a running site\n * cannot show one, so asserting it there would report a defect the input cannot\n * express. Measured across a blind cohort every site lacked one — and because\n * migrated navigation is root-relative, a wrong path is not a rare event: it\n * lands the visitor on the host's generic page, with none of the site's chrome\n * and no way back.\n */\nexport function notFoundPageChecks(ctx: Context): Finding[] {\n if (!ctx.input.build) return []\n // Both spellings a static build produces: Astro writes `404.html` at the\n // root, and a directory-style build writes `404/index.html`, which the\n // address rule reduces to `/404`.\n const served = ctx.input.documents.some((doc) => {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n })\n if (served) return []\n return [finding({\n group: 'status',\n check: 'status.not-found-page-missing',\n severity: 'error',\n message: 'The build has no 404.html, so a wrong path lands on the host\\u2019s page instead of the site\\u2019s.',\n })]\n}\n\n/**\n * Pages the old site served that the build does not, with no redirect for them.\n *\n * `status.mismatch` walks the build's own documents, so it can only compare\n * pages that exist on both sides. A page that the migration never produced is\n * not a document here at all, and nothing would ever report it — while every\n * link and every search result pointing at it now ends on a 404. This walks the\n * baseline instead.\n *\n * Only a build is the whole site. A set of captured pages may simply not\n * include one, so asserting absence there would report a defect the input\n * cannot express. Only baseline pages that answered 2xx count: a redirect or an\n * error on the old site was not a page anyone could land on. A redirect rule\n * for the address covers it — where it leads is `redirectChecks`' question.\n */\nexport function baselineCoverageChecks(ctx: Context): Finding[] {\n if (!ctx.input.build || !ctx.input.baseline) return []\n const redirected = new Set((ctx.input.redirects ?? []).map(rule => identity(rule.from, ctx.input.site)))\n const out: Finding[] = []\n const reported = new Set<string>()\n for (const before of ctx.input.baseline.documents) {\n const status = before.status ?? 200\n if (status < 200 || status >= 300) continue\n const id = identity(before.url, ctx.input.site)\n if (id === '/404' || id === '/404.html') continue\n if (ctx.served.has(id) || redirected.has(id) || reported.has(id)) continue\n reported.add(id)\n out.push(finding({\n group: 'status',\n check: 'status.baseline-page-missing',\n severity: 'error',\n url: before.url,\n message: 'The old site served this page; the build does not, and no redirect covers it.',\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/**\n * References to the origin the content was migrated away from.\n *\n * A generated page that still points at the old WordPress host has not been\n * migrated, it has been mirrored: the new site works only while the old one\n * stays up, and the day it goes away the images and stylesheets go with it.\n * Across a blind cohort this was the single largest thing keeping sites from\n * being deliverable, so it is an error rather than a warning.\n *\n * The scan is over raw text, not parsed attributes, because the references hide\n * in every shape a reference can take — `src`, `srcset`, `href`, `url()` in a\n * style attribute, a string inside inline script. That over-matches slightly:\n * the origin written in a comment or in visible prose counts too. Reported that\n * way on purpose — a literal old-host URL on the page is worth looking at even\n * when it is not fetched.\n *\n * Stylesheets and scripts are scanned only when their contents are supplied\n * (`input.files`). Without them the count is HTML-only, which is a smaller\n * number than the truth rather than a wrong one — and the report says so.\n */\nexport function sourceOriginChecks(ctx: Context): Finding[] {\n const origin = ctx.options.sourceOrigin\n if (!origin) return []\n\n let host: string\n try {\n host = new URL(origin.includes('://') ? origin : `https://${origin}`).host\n } catch {\n return [finding({\n group: 'assets',\n check: 'assets.source-origin-invalid',\n severity: 'warning',\n message: `sourceOrigin \"${origin}\" is not a host or a URL; the scan did not run.`,\n })]\n }\n if ((ctx.options.allowHosts ?? []).includes(host)) return []\n\n const out: Finding[] = []\n const pattern = new RegExp(host.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`), 'gi')\n const count = (source: string) => (source.match(pattern) ?? []).length\n\n for (const doc of ctx.input.documents) {\n const hits = count(doc.html)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: doc.url,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host} — this page still depends on the site it replaced.`,\n }))\n }\n }\n\n for (const file of ctx.input.files ?? []) {\n const hits = count(file.content)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: file.path,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host}.`,\n }))\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, baselineCoverageChecks, identityChecks, indexingChecks, internationalChecks, navigationChecks,\n notFoundPageChecks, redirectChecks, sitemapStaleChecks, sourceOriginChecks,\n statusChecks, structuredChecks,\n} from './checks.js'\nimport { isSitemapIndex } from './html.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 } else if (input.sitemap !== undefined && isSitemapIndex(input.sitemap) && groups.includes('indexing')) {\n // An index lists sitemap files, not pages; its children are not here to read.\n skipped.push({ group: 'indexing', reason: 'sitemap is an index without its child sitemaps — 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 if (!input.build && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'not a build directory — the 404 page check did not run' })\n }\n if (input.baseline && !input.build && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'not a build directory — pages the baseline has and the input lacks were not reported' })\n }\n if (!input.options?.sourceOrigin && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no sourceOrigin — the old-host reference scan did not run' })\n } else if (!input.files?.length && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no stylesheet or script contents — the old-host scan covered HTML only' })\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('status', () => notFoundPageChecks(ctx))\n run('status', () => baselineCoverageChecks(ctx))\n run('international', () => internationalChecks(ctx))\n run('assets', () => sourceOriginChecks(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 { isSitemapIndex, sitemapLocations } from './html.js'\nimport type { VerifyDocument, VerifyFile, VerifyInput } from './types.js'\nimport { identity } from './url.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\nconst TEXT_EXT = /\\.(css|m?js|cjs|json|xml|txt|svg)$/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' | 'files' | 'sitemap' | 'site' | 'build'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n const texts: VerifyFile[] = []\n const sitemaps = new Map<string, string>()\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)) sitemaps.set(served, await readFile(join(dir, file), 'utf8'))\n // Stylesheets and scripts are read as text so the content scans can see\n // them. A `url()` pointing at the old host is exactly as fatal as one in\n // the HTML, and a scan that skips CSS reports a number smaller than the\n // truth — which is worse than not counting when two tools compare figures.\n if (TEXT_EXT.test(file)) {\n texts.push({ path: served, content: await readFile(join(dir, file), 'utf8') })\n }\n assets.push(served)\n }\n\n return { documents, assets, files: texts, sitemap: combineSitemaps(sitemaps, site), site, build: true }\n}\n\n/**\n * Every page address a build's sitemap files list, as one `<urlset>`.\n *\n * A build rarely writes a single `sitemap.xml`. `@astrojs/sitemap` writes\n * `sitemap-index.xml` naming `sitemap-0.xml`, `sitemap-1.xml`, …; reading any\n * one of those files alone gives either no pages or some of them, and the\n * membership checks then report pages missing that are listed.\n *\n * When there is an index, only the sitemaps it names count — that is what a\n * crawler following it reads. A named sitemap the build does not serve is kept\n * as an entry, so the stale-entry check reports it instead of it disappearing.\n */\nfunction combineSitemaps(sitemaps: Map<string, string>, site?: string): string | undefined {\n if (sitemaps.size === 0) return undefined\n const documents = [...sitemaps.values()]\n const indexes = documents.filter(isSitemapIndex)\n const locations: string[] = []\n for (const xml of indexes.length > 0 ? indexes : documents) {\n if (!isSitemapIndex(xml)) {\n locations.push(...sitemapLocations(xml))\n continue\n }\n for (const child of sitemapLocations(xml)) {\n const content = sitemaps.get(identity(child, site))\n if (content === undefined || isSitemapIndex(content)) locations.push(child)\n else locations.push(...sitemapLocations(content))\n }\n }\n const entries = locations.map(loc => `<url><loc>${escapeXml(loc)}</loc></url>`)\n return `<urlset>${entries.join('')}</urlset>`\n}\n\nfunction escapeXml(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"load-DUyyLerx.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\n/** A text file that is not a document — a stylesheet, a script. */\nexport interface VerifyFile {\n /** Path as served (`/assets/app.css`). */\n path: string\n content: 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 * The origin the content was migrated away from — the old WordPress host.\n *\n * A migrated page that still points at it is not finished: the new site\n * depends on the old one staying up, and the moment it does not, images and\n * stylesheets disappear. Measured across a blind cohort it was the single\n * largest blocker, so this is checked as an error when the origin is known.\n */\n sourceOrigin?: string\n /**\n * Hosts that may legitimately still be referenced even though they are not\n * the site's own — a CDN the customer keeps. Applies to\n * `assets.source-origin-reference` only.\n */\n allowHosts?: 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 /**\n * Contents of non-document text files — stylesheets, scripts. Only the checks\n * that scan content read these; everything else works from `documents` and\n * the `assets` path list.\n *\n * Supplying them matters for `assets.source-origin-reference`: a `url()` in a\n * stylesheet pointing at the old host is exactly as fatal as one in the HTML,\n * and a scan that silently skips CSS reports a smaller number than a scan\n * that does not — which is worse than not counting at all when two tools\n * compare figures.\n */\n files?: VerifyFile[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /**\n * Whether `documents` are a static build directory rather than pages captured\n * from a running site. Build-level expectations — a `404.html` exists — are\n * only meaningful for a build, and asserting them against a capture would\n * report a defect the capture simply cannot show. `loadSiteDirectory` sets it.\n */\n build?: boolean\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\n/**\n * The build's 404 page, which is not served at its own address.\n *\n * A static host hands this document to a visitor who asked for something else,\n * so every check that assumes \"this document is served at this URL\" is wrong\n * about it: its canonical does not describe a page, it does not belong in the\n * sitemap, and — the one that matters — it says \"not found\" and is short by\n * design, which is precisely the shape of a soft-404. Without this exemption\n * every correctly built site would fail on having done the right thing.\n */\nfunction isNotFoundDocument(doc: VerifyDocument, ctx: Context): boolean {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n}\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 (isNotFoundDocument(doc, ctx)) {\n // No address of its own, so nothing to be canonical about.\n } else 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 // `<html lang>` is what a screen reader picks a voice from and what a\n // translation prompt keys on. A migrated page that lost it is not visibly\n // broken, which is why it stays lost.\n if (!/<html\\b[^>]*\\slang\\s*=\\s*[\"']?[a-zA-Z]/.test(doc.html)) {\n out.push(finding({ group, check: 'identity.lang-missing', severity: 'warning', url, message: 'No lang attribute on <html>.' }))\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 && !isNotFoundDocument(doc, ctx)) {\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 && !isNotFoundDocument(doc, ctx)) {\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/**\n * A build with no `404.html`.\n *\n * Only meaningful for a build: a set of pages captured from a running site\n * cannot show one, so asserting it there would report a defect the input cannot\n * express. Measured across a blind cohort every site lacked one — and because\n * migrated navigation is root-relative, a wrong path is not a rare event: it\n * lands the visitor on the host's generic page, with none of the site's chrome\n * and no way back.\n */\nexport function notFoundPageChecks(ctx: Context): Finding[] {\n if (!ctx.input.build) return []\n // Both spellings a static build produces: Astro writes `404.html` at the\n // root, and a directory-style build writes `404/index.html`, which the\n // address rule reduces to `/404`.\n const served = ctx.input.documents.some((doc) => {\n const id = identity(doc.url, ctx.input.site)\n return id === '/404' || id === '/404.html'\n })\n if (served) return []\n return [finding({\n group: 'status',\n check: 'status.not-found-page-missing',\n severity: 'error',\n message: 'The build has no 404.html, so a wrong path lands on the host\\u2019s page instead of the site\\u2019s.',\n })]\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/**\n * References to the origin the content was migrated away from.\n *\n * A generated page that still points at the old WordPress host has not been\n * migrated, it has been mirrored: the new site works only while the old one\n * stays up, and the day it goes away the images and stylesheets go with it.\n * Across a blind cohort this was the single largest thing keeping sites from\n * being deliverable, so it is an error rather than a warning.\n *\n * The scan is over raw text, not parsed attributes, because the references hide\n * in every shape a reference can take — `src`, `srcset`, `href`, `url()` in a\n * style attribute, a string inside inline script. That over-matches slightly:\n * the origin written in a comment or in visible prose counts too. Reported that\n * way on purpose — a literal old-host URL on the page is worth looking at even\n * when it is not fetched.\n *\n * Stylesheets and scripts are scanned only when their contents are supplied\n * (`input.files`). Without them the count is HTML-only, which is a smaller\n * number than the truth rather than a wrong one — and the report says so.\n */\nexport function sourceOriginChecks(ctx: Context): Finding[] {\n const origin = ctx.options.sourceOrigin\n if (!origin) return []\n\n let host: string\n try {\n host = new URL(origin.includes('://') ? origin : `https://${origin}`).host\n } catch {\n return [finding({\n group: 'assets',\n check: 'assets.source-origin-invalid',\n severity: 'warning',\n message: `sourceOrigin \"${origin}\" is not a host or a URL; the scan did not run.`,\n })]\n }\n if ((ctx.options.allowHosts ?? []).includes(host)) return []\n\n const out: Finding[] = []\n const pattern = new RegExp(host.replace(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`), 'gi')\n const count = (source: string) => (source.match(pattern) ?? []).length\n\n for (const doc of ctx.input.documents) {\n const hits = count(doc.html)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: doc.url,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host} — this page still depends on the site it replaced.`,\n }))\n }\n }\n\n for (const file of ctx.input.files ?? []) {\n const hits = count(file.content)\n if (hits) {\n out.push(finding({\n group: 'assets',\n check: 'assets.source-origin-reference',\n severity: 'error',\n url: file.path,\n message: `${hits} reference${hits === 1 ? '' : 's'} to the source origin ${host}.`,\n }))\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 notFoundPageChecks, redirectChecks, sitemapStaleChecks, sourceOriginChecks,\n 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 if (!input.build && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'not a build directory — the 404 page check did not run' })\n }\n if (!input.options?.sourceOrigin && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no sourceOrigin — the old-host reference scan did not run' })\n } else if (!input.files?.length && groups.includes('assets')) {\n skipped.push({ group: 'assets', reason: 'no stylesheet or script contents — the old-host scan covered HTML only' })\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('status', () => notFoundPageChecks(ctx))\n run('international', () => internationalChecks(ctx))\n run('assets', () => sourceOriginChecks(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, VerifyFile, VerifyInput } from './types.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\nconst TEXT_EXT = /\\.(css|m?js|cjs|json|xml|txt|svg)$/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' | 'files' | 'sitemap' | 'site' | 'build'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n const texts: VerifyFile[] = []\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 // Stylesheets and scripts are read as text so the content scans can see\n // them. A `url()` pointing at the old host is exactly as fatal as one in\n // the HTML, and a scan that skips CSS reports a number smaller than the\n // truth — which is worse than not counting when two tools compare figures.\n if (TEXT_EXT.test(file)) {\n texts.push({ path: served, content: await readFile(join(dir, file), 'utf8') })\n }\n assets.push(served)\n }\n\n return { documents, assets, files: texts, sitemap, site, build: true }\n}\n"],"mappings":"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"}
|