@contentrain/verify 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Contentrain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,18 +71,21 @@ 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
- | 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 |
78
+ | identity | `identity.lang-missing` | warning | No `lang` on `<html>` — what a screen reader picks a voice from |
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 |
71
80
  | | `indexing.sitemap-missing-entry` | warning | An indexable page is not in the sitemap (a `noindex` page is not asked to be) |
72
81
  | | `indexing.sitemap-stale-entry` | warning | The sitemap lists a page the build does not serve |
73
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 |
74
84
  | | `status.soft-404` | error | A "not found" page answering 200 — invisible to every status monitor, indexed as content |
75
85
  | | `status.redirect-chain` | warning | More hops than `maxRedirectHops` (default 1) |
76
86
  | | `status.redirect-loop` | error | A chain that returns to itself |
77
87
  | | `status.redirect-target-missing` | error | An internal redirect pointing at a page the build does not serve |
88
+ | | `status.not-found-page-missing` | error | The build has no `404.html`. Only checked for a build (`input.build`); migrated navigation is root-relative, so a wrong path is not rare, and without one the visitor lands on the host's page with none of the site's chrome |
78
89
  | international | `international.hreflang-not-reciprocal` | error | A names B but B does not name A. Search engines ignore one-sided alternates entirely, so half the work is worth nothing |
79
90
  | | `international.hreflang-no-self` | warning | A page with alternates that does not list itself |
80
91
  | | `international.hreflang-unknown-target` | warning | An alternate pointing at a page that does not exist |
@@ -86,6 +97,19 @@ a page must not fail its own gate.
86
97
  | assets | `assets.broken-image` | error | An internal image that resolves to nothing the build serves. `data:` URIs and third-party hosts are skipped |
87
98
  | | `assets.missing-alt` | warning | An `<img>` with no `alt` attribute. `alt=""` is a decision (decorative) and is not reported |
88
99
  | | `assets.alt-lost` | warning | Alt text the old page had for the same image is gone |
100
+ | | `assets.source-origin-reference` | error | A reference to `options.sourceOrigin` — the host the content was migrated away from. The new site then works only while the old one stays up. Scans documents, and stylesheets and scripts when `input.files` carries their contents; `options.allowHosts` exempts a CDN the customer kept |
101
+
102
+ ## The 404 page
103
+
104
+ A static host hands `404.html` to a visitor who asked for something else, so it
105
+ is not served at its own address and the checks that assume otherwise do not
106
+ apply to it: canonical, sitemap membership, and — the one that matters —
107
+ soft-404. A correct 404 page says "not found" and is short, which is exactly the
108
+ shape the soft-404 check looks for, so without this exemption every site that
109
+ did the right thing would fail on it.
110
+
111
+ Recognised at either spelling a build produces: `404.html` at the root, or
112
+ `404/index.html`.
89
113
 
90
114
  ## Address identity
91
115
 
@@ -117,6 +141,8 @@ contentrain-verify <dist-dir> [options]
117
141
  --site <url> Canonical origin
118
142
  --baseline <dir> The old site, as a directory of captured pages
119
143
  --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
144
+ --source-origin <host> The old host content was migrated away from
145
+ --allow-host <host> A host that may still be referenced (repeatable)
120
146
  --groups <list> identity,indexing,status,international,structured,navigation,assets
121
147
  --max-redirect-hops <n>
122
148
  --json
package/dist/cli.d.mts CHANGED
@@ -1,10 +1,12 @@
1
- import { n as CheckGroup } from "./types-C_hmH1n3.mjs";
1
+ import { n as CheckGroup } from "./types-bYfzrRnM.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare const USAGE: string;
5
5
  interface Args {
6
6
  dir?: string;
7
7
  site?: string;
8
+ sourceOrigin?: string;
9
+ allowHosts?: string[];
8
10
  baseline?: string;
9
11
  redirects?: string;
10
12
  groups?: CheckGroup[];
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAca,KAAA;AAAA,UAWI,IAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA,GAAS,UAAA;EACT,eAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,IAAA,sBAA0B,IAAA;AAAA,UAyBnC,QAAA;EACf,GAAA,GAAM,IAAA;EACN,KAAA,GAAQ,IAAA;AAAA;;iBAIY,IAAA,CAAK,IAAA,qBAAyB,EAAA,GAAI,QAAA,GAAqB,OAAA"}
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAca,KAAA;AAAA,UAaI,IAAA;EACf,GAAA;EACA,IAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA;EACA,MAAA,GAAS,UAAA;EACT,eAAA;EACA,IAAA;EACA,IAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,IAAA,sBAA0B,IAAA;AAAA,UA2BnC,QAAA;EACf,GAAA,GAAM,IAAA;EACN,KAAA,GAAQ,IAAA;AAAA;;iBAIY,IAAA,CAAK,IAAA,qBAAyB,EAAA,GAAI,QAAA,GAAqB,OAAA"}
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-KUG-8Ug4.mjs";
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]
@@ -6,6 +6,8 @@ const USAGE = `contentrain-verify <dist-dir> [options]
6
6
  --site <url> Canonical origin, e.g. https://example.com
7
7
  --baseline <dir> The old site, as a directory of captured pages
8
8
  --redirects <file> JSON: [{ "from": "/a", "to": "/b", "status": 301 }]
9
+ --source-origin <host> The old host content was migrated away from
10
+ --allow-host <host> A host that may still be referenced (repeatable)
9
11
  --groups <list> Comma-separated: ${CHECK_GROUPS.join(",")}
10
12
  --max-redirect-hops <n>
11
13
  --json Print the report as JSON instead of text
@@ -22,6 +24,8 @@ function parseArgs(argv) {
22
24
  if (arg === "--help" || arg === "-h") args.help = true;
23
25
  else if (arg === "--json") args.json = true;
24
26
  else if (arg === "--site") args.site = next();
27
+ else if (arg === "--source-origin") args.sourceOrigin = next();
28
+ else if (arg === "--allow-host") (args.allowHosts ??= []).push(next() ?? "");
25
29
  else if (arg === "--baseline") args.baseline = next();
26
30
  else if (arg === "--redirects") args.redirects = next();
27
31
  else if (arg === "--max-redirect-hops") {
@@ -51,10 +55,15 @@ async function main(argv, io = console) {
51
55
  io.log(USAGE);
52
56
  return args.help ? 0 : 2;
53
57
  }
58
+ const loaded = await loadSiteDirectory(args.dir, args.site);
59
+ const options = {};
60
+ if (args.maxRedirectHops !== void 0) options.maxRedirectHops = args.maxRedirectHops;
61
+ if (args.sourceOrigin !== void 0) options.sourceOrigin = args.sourceOrigin;
62
+ if (args.allowHosts?.length) options.allowHosts = args.allowHosts;
54
63
  const input = {
55
- ...await loadSiteDirectory(args.dir, args.site),
64
+ ...loaded,
56
65
  groups: args.groups,
57
- options: args.maxRedirectHops === void 0 ? void 0 : { maxRedirectHops: args.maxRedirectHops }
66
+ options: Object.keys(options).length ? options : void 0
58
67
  };
59
68
  if (args.baseline) input.baseline = { documents: (await loadSiteDirectory(args.baseline, args.site)).documents };
60
69
  if (args.redirects) input.redirects = JSON.parse(await readFile(args.redirects, "utf8"));
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["// ─── contentrain-verify ───\n//\n// A thin shell over `verify()`: read a built directory, run the gates, print\n// the report, return an exit code. Everything decisional lives in the library —\n// this adds argument parsing and a number, nothing else. `bin.ts` is the\n// executable; keeping the shell importable is what makes it testable without\n// spawning a process.\n\nimport { readFile } from 'node:fs/promises'\nimport { verify, formatReport } from './verify.js'\nimport { loadSiteDirectory } from './load.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { CheckGroup, VerifyInput } from './types.js'\n\nexport const USAGE = `contentrain-verify <dist-dir> [options]\n\n --site <url> Canonical origin, e.g. https://example.com\n --baseline <dir> The old site, as a directory of captured pages\n --redirects <file> JSON: [{ \"from\": \"/a\", \"to\": \"/b\", \"status\": 301 }]\n --groups <list> Comma-separated: ${CHECK_GROUPS.join(',')}\n --max-redirect-hops <n>\n --json Print the report as JSON instead of text\n\nExits 1 when any finding is an error, 2 on a usage error.`\n\nexport interface Args {\n dir?: string\n site?: string\n baseline?: string\n redirects?: string\n groups?: CheckGroup[]\n maxRedirectHops?: number\n json: boolean\n help: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): Args {\n const args: Args = { json: false, help: false }\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i]!\n const next = (): string | undefined => argv[++i]\n if (arg === '--help' || arg === '-h') args.help = true\n else if (arg === '--json') args.json = true\n else if (arg === '--site') args.site = next()\n else if (arg === '--baseline') args.baseline = next()\n else if (arg === '--redirects') args.redirects = next()\n else if (arg === '--max-redirect-hops') {\n const value = Number(next())\n if (!Number.isInteger(value) || value < 0) throw new Error('--max-redirect-hops expects a non-negative integer')\n args.maxRedirectHops = value\n } else if (arg === '--groups') {\n const list = (next() ?? '').split(',').map(g => g.trim()).filter(Boolean)\n const unknown = list.filter(g => !(CHECK_GROUPS as readonly string[]).includes(g))\n if (unknown.length) throw new Error(`unknown group(s): ${unknown.join(', ')}`)\n args.groups = list as CheckGroup[]\n } else if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)\n else args.dir ??= arg\n }\n return args\n}\n\nexport interface Console2 {\n log: (line: string) => void\n error: (line: string) => void\n}\n\n/** Returns the process exit code. `io` is injectable so tests read the output. */\nexport async function main(argv: readonly string[], io: Console2 = console): Promise<number> {\n let args: Args\n try {\n args = parseArgs(argv)\n } catch (error) {\n io.error(`${(error as Error).message}\\n\\n${USAGE}`)\n return 2\n }\n\n if (args.help || !args.dir) {\n io.log(USAGE)\n return args.help ? 0 : 2\n }\n\n const loaded = await loadSiteDirectory(args.dir, args.site)\n const input: VerifyInput = {\n ...loaded,\n groups: args.groups,\n options: args.maxRedirectHops === undefined ? undefined : { maxRedirectHops: args.maxRedirectHops },\n }\n\n if (args.baseline) {\n const before = await loadSiteDirectory(args.baseline, args.site)\n input.baseline = { documents: before.documents }\n }\n if (args.redirects) {\n input.redirects = JSON.parse(await readFile(args.redirects, 'utf8')) as VerifyInput['redirects']\n }\n\n const report = verify(input)\n io.log(args.json ? JSON.stringify(report, null, 2) : formatReport(report))\n return report.passed ? 0 : 1\n}\n"],"mappings":";;;AAcA,MAAa,QAAQ;;;;;2CAKsB,aAAa,KAAK,IAAI,CAAC;;;;;AAiBlE,SAAgB,UAAU,MAA+B;CACvD,MAAM,OAAa;EAAE,MAAM;EAAO,MAAM;EAAO;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;EACvC,MAAM,MAAM,KAAK;EACjB,MAAM,aAAiC,KAAK,EAAE;AAC9C,MAAI,QAAQ,YAAY,QAAQ,KAAM,MAAK,OAAO;WACzC,QAAQ,SAAU,MAAK,OAAO;WAC9B,QAAQ,SAAU,MAAK,OAAO,MAAM;WACpC,QAAQ,aAAc,MAAK,WAAW,MAAM;WAC5C,QAAQ,cAAe,MAAK,YAAY,MAAM;WAC9C,QAAQ,uBAAuB;GACtC,MAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,OAAI,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,MAAM,qDAAqD;AAChH,QAAK,kBAAkB;aACd,QAAQ,YAAY;GAC7B,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,OAAO,QAAQ;GACzE,MAAM,UAAU,KAAK,QAAO,MAAK,CAAE,aAAmC,SAAS,EAAE,CAAC;AAClF,OAAI,QAAQ,OAAQ,OAAM,IAAI,MAAM,qBAAqB,QAAQ,KAAK,KAAK,GAAG;AAC9E,QAAK,SAAS;aACL,IAAI,WAAW,IAAI,CAAE,OAAM,IAAI,MAAM,mBAAmB,MAAM;MACpE,MAAK,QAAQ;;AAEpB,QAAO;;;AAST,eAAsB,KAAK,MAAyB,KAAe,SAA0B;CAC3F,IAAI;AACJ,KAAI;AACF,SAAO,UAAU,KAAK;UACf,OAAO;AACd,KAAG,MAAM,GAAI,MAAgB,QAAQ,MAAM,QAAQ;AACnD,SAAO;;AAGT,KAAI,KAAK,QAAQ,CAAC,KAAK,KAAK;AAC1B,KAAG,IAAI,MAAM;AACb,SAAO,KAAK,OAAO,IAAI;;CAIzB,MAAM,QAAqB;EACzB,GAFa,MAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;EAGzD,QAAQ,KAAK;EACb,SAAS,KAAK,oBAAoB,KAAA,IAAY,KAAA,IAAY,EAAE,iBAAiB,KAAK,iBAAiB;EACpG;AAED,KAAI,KAAK,SAEP,OAAM,WAAW,EAAE,YADJ,MAAM,kBAAkB,KAAK,UAAU,KAAK,KAAK,EAC3B,WAAW;AAElD,KAAI,KAAK,UACP,OAAM,YAAY,KAAK,MAAM,MAAM,SAAS,KAAK,WAAW,OAAO,CAAC;CAGtE,MAAM,SAAS,OAAO,MAAM;AAC5B,IAAG,IAAI,KAAK,OAAO,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,aAAa,OAAO,CAAC;AAC1E,QAAO,OAAO,SAAS,IAAI"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["// ─── contentrain-verify ───\n//\n// A thin shell over `verify()`: read a built directory, run the gates, print\n// the report, return an exit code. Everything decisional lives in the library —\n// this adds argument parsing and a number, nothing else. `bin.ts` is the\n// executable; keeping the shell importable is what makes it testable without\n// spawning a process.\n\nimport { readFile } from 'node:fs/promises'\nimport { verify, formatReport } from './verify.js'\nimport { loadSiteDirectory } from './load.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { CheckGroup, VerifyInput } from './types.js'\n\nexport const USAGE = `contentrain-verify <dist-dir> [options]\n\n --site <url> Canonical origin, e.g. https://example.com\n --baseline <dir> The old site, as a directory of captured pages\n --redirects <file> JSON: [{ \"from\": \"/a\", \"to\": \"/b\", \"status\": 301 }]\n --source-origin <host> The old host content was migrated away from\n --allow-host <host> A host that may still be referenced (repeatable)\n --groups <list> Comma-separated: ${CHECK_GROUPS.join(',')}\n --max-redirect-hops <n>\n --json Print the report as JSON instead of text\n\nExits 1 when any finding is an error, 2 on a usage error.`\n\nexport interface Args {\n dir?: string\n site?: string\n sourceOrigin?: string\n allowHosts?: string[]\n baseline?: string\n redirects?: string\n groups?: CheckGroup[]\n maxRedirectHops?: number\n json: boolean\n help: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): Args {\n const args: Args = { json: false, help: false }\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i]!\n const next = (): string | undefined => argv[++i]\n if (arg === '--help' || arg === '-h') args.help = true\n else if (arg === '--json') args.json = true\n else if (arg === '--site') args.site = next()\n else if (arg === '--source-origin') args.sourceOrigin = next()\n else if (arg === '--allow-host') (args.allowHosts ??= []).push(next() ?? '')\n else if (arg === '--baseline') args.baseline = next()\n else if (arg === '--redirects') args.redirects = next()\n else if (arg === '--max-redirect-hops') {\n const value = Number(next())\n if (!Number.isInteger(value) || value < 0) throw new Error('--max-redirect-hops expects a non-negative integer')\n args.maxRedirectHops = value\n } else if (arg === '--groups') {\n const list = (next() ?? '').split(',').map(g => g.trim()).filter(Boolean)\n const unknown = list.filter(g => !(CHECK_GROUPS as readonly string[]).includes(g))\n if (unknown.length) throw new Error(`unknown group(s): ${unknown.join(', ')}`)\n args.groups = list as CheckGroup[]\n } else if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)\n else args.dir ??= arg\n }\n return args\n}\n\nexport interface Console2 {\n log: (line: string) => void\n error: (line: string) => void\n}\n\n/** Returns the process exit code. `io` is injectable so tests read the output. */\nexport async function main(argv: readonly string[], io: Console2 = console): Promise<number> {\n let args: Args\n try {\n args = parseArgs(argv)\n } catch (error) {\n io.error(`${(error as Error).message}\\n\\n${USAGE}`)\n return 2\n }\n\n if (args.help || !args.dir) {\n io.log(USAGE)\n return args.help ? 0 : 2\n }\n\n const loaded = await loadSiteDirectory(args.dir, args.site)\n const options: VerifyInput['options'] = {}\n if (args.maxRedirectHops !== undefined) options.maxRedirectHops = args.maxRedirectHops\n if (args.sourceOrigin !== undefined) options.sourceOrigin = args.sourceOrigin\n if (args.allowHosts?.length) options.allowHosts = args.allowHosts\n\n const input: VerifyInput = {\n ...loaded,\n groups: args.groups,\n options: Object.keys(options).length ? options : undefined,\n }\n\n if (args.baseline) {\n const before = await loadSiteDirectory(args.baseline, args.site)\n input.baseline = { documents: before.documents }\n }\n if (args.redirects) {\n input.redirects = JSON.parse(await readFile(args.redirects, 'utf8')) as VerifyInput['redirects']\n }\n\n const report = verify(input)\n io.log(args.json ? JSON.stringify(report, null, 2) : formatReport(report))\n return report.passed ? 0 : 1\n}\n"],"mappings":";;;AAcA,MAAa,QAAQ;;;;;;;2CAOsB,aAAa,KAAK,IAAI,CAAC;;;;;AAmBlE,SAAgB,UAAU,MAA+B;CACvD,MAAM,OAAa;EAAE,MAAM;EAAO,MAAM;EAAO;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;EACvC,MAAM,MAAM,KAAK;EACjB,MAAM,aAAiC,KAAK,EAAE;AAC9C,MAAI,QAAQ,YAAY,QAAQ,KAAM,MAAK,OAAO;WACzC,QAAQ,SAAU,MAAK,OAAO;WAC9B,QAAQ,SAAU,MAAK,OAAO,MAAM;WACpC,QAAQ,kBAAmB,MAAK,eAAe,MAAM;WACrD,QAAQ,eAAgB,EAAC,KAAK,eAAe,EAAE,EAAE,KAAK,MAAM,IAAI,GAAG;WACnE,QAAQ,aAAc,MAAK,WAAW,MAAM;WAC5C,QAAQ,cAAe,MAAK,YAAY,MAAM;WAC9C,QAAQ,uBAAuB;GACtC,MAAM,QAAQ,OAAO,MAAM,CAAC;AAC5B,OAAI,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,MAAM,qDAAqD;AAChH,QAAK,kBAAkB;aACd,QAAQ,YAAY;GAC7B,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,KAAI,MAAK,EAAE,MAAM,CAAC,CAAC,OAAO,QAAQ;GACzE,MAAM,UAAU,KAAK,QAAO,MAAK,CAAE,aAAmC,SAAS,EAAE,CAAC;AAClF,OAAI,QAAQ,OAAQ,OAAM,IAAI,MAAM,qBAAqB,QAAQ,KAAK,KAAK,GAAG;AAC9E,QAAK,SAAS;aACL,IAAI,WAAW,IAAI,CAAE,OAAM,IAAI,MAAM,mBAAmB,MAAM;MACpE,MAAK,QAAQ;;AAEpB,QAAO;;;AAST,eAAsB,KAAK,MAAyB,KAAe,SAA0B;CAC3F,IAAI;AACJ,KAAI;AACF,SAAO,UAAU,KAAK;UACf,OAAO;AACd,KAAG,MAAM,GAAI,MAAgB,QAAQ,MAAM,QAAQ;AACnD,SAAO;;AAGT,KAAI,KAAK,QAAQ,CAAC,KAAK,KAAK;AAC1B,KAAG,IAAI,MAAM;AACb,SAAO,KAAK,OAAO,IAAI;;CAGzB,MAAM,SAAS,MAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;CAC3D,MAAM,UAAkC,EAAE;AAC1C,KAAI,KAAK,oBAAoB,KAAA,EAAW,SAAQ,kBAAkB,KAAK;AACvE,KAAI,KAAK,iBAAiB,KAAA,EAAW,SAAQ,eAAe,KAAK;AACjE,KAAI,KAAK,YAAY,OAAQ,SAAQ,aAAa,KAAK;CAEvD,MAAM,QAAqB;EACzB,GAAG;EACH,QAAQ,KAAK;EACb,SAAS,OAAO,KAAK,QAAQ,CAAC,SAAS,UAAU,KAAA;EAClD;AAED,KAAI,KAAK,SAEP,OAAM,WAAW,EAAE,YADJ,MAAM,kBAAkB,KAAK,UAAU,KAAK,KAAK,EAC3B,WAAW;AAElD,KAAI,KAAK,UACP,OAAM,YAAY,KAAK,MAAM,MAAM,SAAS,KAAK,WAAW,OAAO,CAAC;CAGtE,MAAM,SAAS,OAAO,MAAM;AAC5B,IAAG,IAAI,KAAK,OAAO,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,aAAa,OAAO,CAAC;AAC1E,QAAO,OAAO,SAAS,IAAI"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as VerifyDocument, c as VerifyRedirect, i as Severity, l as VerifyReport, n as CheckGroup, o as VerifyInput, r as Finding, s as VerifyOptions, t as CHECK_GROUPS } from "./types-C_hmH1n3.mjs";
1
+ import { a as VerifyDocument, c as VerifyRedirect, i as Severity, l as VerifyReport, n as CheckGroup, o as VerifyInput, r as Finding, s as VerifyOptions, t as CHECK_GROUPS } from "./types-bYfzrRnM.mjs";
2
2
 
3
3
  //#region src/verify.d.ts
4
4
  /**
@@ -23,7 +23,7 @@ declare function formatReport(report: VerifyReport): string;
23
23
  * reduces to `/about` — the same address a link to `/about/` or
24
24
  * `/about/index.html` resolves to.
25
25
  */
26
- declare function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>>;
26
+ declare function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'files' | 'sitemap' | 'site' | 'build'>>;
27
27
  //#endregion
28
28
  export { CHECK_GROUPS, type CheckGroup, type Finding, type Severity, type VerifyDocument, type VerifyInput, type VerifyOptions, type VerifyRedirect, type VerifyReport, formatReport, loadSiteDirectory, verify };
29
29
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/verify.ts","../src/load.ts"],"mappings":";;;;;AAsDA;;;;;;;iBAAgB,MAAA,CAAO,KAAA,EAAO,WAAA,GAAc,YAAA;;iBAuC5B,YAAA,CAAa,MAAA,EAAQ,YAAA;;;;;AAvCrC;;;;;;iBCpBsB,iBAAA,CAAkB,GAAA,UAAa,IAAA,YAAgB,OAAA,CAAQ,IAAA,CAAK,WAAA"}
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-KUG-8Ug4.mjs";
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";
@@ -201,9 +209,35 @@ function absolute(url, site) {
201
209
  //#endregion
202
210
  //#region src/checks.ts
203
211
  const NOT_FOUND_TEXT = /\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i;
212
+ /**
213
+ * The build's 404 page, which is not served at its own address.
214
+ *
215
+ * A static host hands this document to a visitor who asked for something else,
216
+ * so every check that assumes "this document is served at this URL" is wrong
217
+ * about it: its canonical does not describe a page, it does not belong in the
218
+ * sitemap, and — the one that matters — it says "not found" and is short by
219
+ * design, which is precisely the shape of a soft-404. Without this exemption
220
+ * every correctly built site would fail on having done the right thing.
221
+ */
222
+ function isNotFoundDocument(doc, ctx) {
223
+ const id = identity(doc.url, ctx.input.site);
224
+ return id === "/404" || id === "/404.html";
225
+ }
204
226
  function finding(f) {
205
227
  return f;
206
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
+ }
207
241
  function identityChecks(doc, ctx) {
208
242
  const out = [];
209
243
  const url = doc.url;
@@ -233,7 +267,7 @@ function identityChecks(doc, ctx) {
233
267
  message: "No meta description."
234
268
  }));
235
269
  const canonicals = linksRel(doc.html, "canonical");
236
- if (canonicals.length === 0) out.push(finding({
270
+ if (isNotFoundDocument(doc, ctx)) {} else if (canonicals.length === 0) out.push(finding({
237
271
  group,
238
272
  check: "identity.canonical-missing",
239
273
  severity: "warning",
@@ -251,15 +285,28 @@ function identityChecks(doc, ctx) {
251
285
  else {
252
286
  const declared = resolve(canonicals[0].href, url, ctx.input.site);
253
287
  const self = absolute(url, ctx.input.site) ?? resolve(url, "/", ctx.input.site);
254
- if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) out.push(finding({
255
- group,
256
- check: "identity.canonical-mismatch",
257
- severity: "error",
258
- url,
259
- message: "Canonical points at a different page.",
260
- detail: `${canonicals[0].href} ≠ ${url}`
261
- }));
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
+ }
262
302
  }
303
+ if (!/<html\b[^>]*\slang\s*=\s*["']?[a-zA-Z]/.test(doc.html)) out.push(finding({
304
+ group,
305
+ check: "identity.lang-missing",
306
+ severity: "warning",
307
+ url,
308
+ message: "No lang attribute on <html>."
309
+ }));
263
310
  const missingOg = [
264
311
  "og:title",
265
312
  "og:type",
@@ -293,16 +340,20 @@ function indexingChecks(doc, ctx) {
293
340
  const group = "indexing";
294
341
  const url = doc.url;
295
342
  const reason = noindexReason(doc);
296
- if (reason) out.push(finding({
297
- group,
298
- check: "indexing.noindex",
299
- severity: "error",
300
- url,
301
- message: "Page is marked noindex.",
302
- detail: reason
303
- }));
304
- if (ctx.input.sitemap !== void 0) {
305
- if (!new Set(sitemapLocations(ctx.input.sitemap).map((loc) => identity(loc, ctx.input.site))).has(identity(url, ctx.input.site)) && !reason) out.push(finding({
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)) {
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({
306
357
  group,
307
358
  check: "indexing.sitemap-missing-entry",
308
359
  severity: "warning",
@@ -314,7 +365,7 @@ function indexingChecks(doc, ctx) {
314
365
  }
315
366
  /** Sitemap entries pointing at pages the build does not serve. */
316
367
  function sitemapStaleChecks(ctx) {
317
- if (ctx.input.sitemap === void 0) return [];
368
+ if (ctx.input.sitemap === void 0 || isSitemapIndex(ctx.input.sitemap)) return [];
318
369
  const out = [];
319
370
  for (const loc of sitemapLocations(ctx.input.sitemap)) {
320
371
  const id = identity(loc, ctx.input.site);
@@ -348,7 +399,7 @@ function statusChecks(doc, ctx) {
348
399
  if (status === 200) {
349
400
  const bodyText = text(doc.html);
350
401
  const title = titles(doc.html)[0] ?? "";
351
- if ((NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))) && bodyText.length <= ctx.options.soft404MaxTextLength) out.push(finding({
402
+ if ((NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))) && bodyText.length <= ctx.options.soft404MaxTextLength && !isNotFoundDocument(doc, ctx)) out.push(finding({
352
403
  group,
353
404
  check: "status.soft-404",
354
405
  severity: "error",
@@ -359,6 +410,66 @@ function statusChecks(doc, ctx) {
359
410
  }
360
411
  return out;
361
412
  }
413
+ /**
414
+ * A build with no `404.html`.
415
+ *
416
+ * Only meaningful for a build: a set of pages captured from a running site
417
+ * cannot show one, so asserting it there would report a defect the input cannot
418
+ * express. Measured across a blind cohort every site lacked one — and because
419
+ * migrated navigation is root-relative, a wrong path is not a rare event: it
420
+ * lands the visitor on the host's generic page, with none of the site's chrome
421
+ * and no way back.
422
+ */
423
+ function notFoundPageChecks(ctx) {
424
+ if (!ctx.input.build) return [];
425
+ if (ctx.input.documents.some((doc) => {
426
+ const id = identity(doc.url, ctx.input.site);
427
+ return id === "/404" || id === "/404.html";
428
+ })) return [];
429
+ return [finding({
430
+ group: "status",
431
+ check: "status.not-found-page-missing",
432
+ severity: "error",
433
+ message: "The build has no 404.html, so a wrong path lands on the host’s page instead of the site’s."
434
+ })];
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
+ }
362
473
  /** Redirect rules: chains, loops, and targets that do not exist. */
363
474
  function redirectChecks(ctx) {
364
475
  const rules = ctx.input.redirects;
@@ -533,6 +644,66 @@ function navigationChecks(doc, ctx) {
533
644
  }
534
645
  return out;
535
646
  }
647
+ /**
648
+ * References to the origin the content was migrated away from.
649
+ *
650
+ * A generated page that still points at the old WordPress host has not been
651
+ * migrated, it has been mirrored: the new site works only while the old one
652
+ * stays up, and the day it goes away the images and stylesheets go with it.
653
+ * Across a blind cohort this was the single largest thing keeping sites from
654
+ * being deliverable, so it is an error rather than a warning.
655
+ *
656
+ * The scan is over raw text, not parsed attributes, because the references hide
657
+ * in every shape a reference can take — `src`, `srcset`, `href`, `url()` in a
658
+ * style attribute, a string inside inline script. That over-matches slightly:
659
+ * the origin written in a comment or in visible prose counts too. Reported that
660
+ * way on purpose — a literal old-host URL on the page is worth looking at even
661
+ * when it is not fetched.
662
+ *
663
+ * Stylesheets and scripts are scanned only when their contents are supplied
664
+ * (`input.files`). Without them the count is HTML-only, which is a smaller
665
+ * number than the truth rather than a wrong one — and the report says so.
666
+ */
667
+ function sourceOriginChecks(ctx) {
668
+ const origin = ctx.options.sourceOrigin;
669
+ if (!origin) return [];
670
+ let host;
671
+ try {
672
+ host = new URL(origin.includes("://") ? origin : `https://${origin}`).host;
673
+ } catch {
674
+ return [finding({
675
+ group: "assets",
676
+ check: "assets.source-origin-invalid",
677
+ severity: "warning",
678
+ message: `sourceOrigin "${origin}" is not a host or a URL; the scan did not run.`
679
+ })];
680
+ }
681
+ if ((ctx.options.allowHosts ?? []).includes(host)) return [];
682
+ const out = [];
683
+ const pattern = new RegExp(host.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`), "gi");
684
+ const count = (source) => (source.match(pattern) ?? []).length;
685
+ for (const doc of ctx.input.documents) {
686
+ const hits = count(doc.html);
687
+ if (hits) out.push(finding({
688
+ group: "assets",
689
+ check: "assets.source-origin-reference",
690
+ severity: "error",
691
+ url: doc.url,
692
+ message: `${hits} reference${hits === 1 ? "" : "s"} to the source origin ${host} — this page still depends on the site it replaced.`
693
+ }));
694
+ }
695
+ for (const file of ctx.input.files ?? []) {
696
+ const hits = count(file.content);
697
+ if (hits) out.push(finding({
698
+ group: "assets",
699
+ check: "assets.source-origin-reference",
700
+ severity: "error",
701
+ url: file.path,
702
+ message: `${hits} reference${hits === 1 ? "" : "s"} to the source origin ${host}.`
703
+ }));
704
+ }
705
+ return out;
706
+ }
536
707
  function assetChecks(doc, ctx) {
537
708
  const out = [];
538
709
  const group = "assets";
@@ -607,10 +778,30 @@ function absentInputs(input, groups) {
607
778
  group: "indexing",
608
779
  reason: "no sitemap — membership checks did not run"
609
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
+ });
610
785
  if (!input.redirects?.length && groups.includes("status")) skipped.push({
611
786
  group: "status",
612
787
  reason: "no redirect rules — chain and target checks did not run"
613
788
  });
789
+ if (!input.build && groups.includes("status")) skipped.push({
790
+ group: "status",
791
+ reason: "not a build directory — the 404 page check did not run"
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
+ });
797
+ if (!input.options?.sourceOrigin && groups.includes("assets")) skipped.push({
798
+ group: "assets",
799
+ reason: "no sourceOrigin — the old-host reference scan did not run"
800
+ });
801
+ else if (!input.files?.length && groups.includes("assets")) skipped.push({
802
+ group: "assets",
803
+ reason: "no stylesheet or script contents — the old-host scan covered HTML only"
804
+ });
614
805
  return skipped;
615
806
  }
616
807
  function buildContext(input) {
@@ -655,7 +846,10 @@ function verify(input) {
655
846
  }
656
847
  run("indexing", () => sitemapStaleChecks(ctx));
657
848
  run("status", () => redirectChecks(ctx));
849
+ run("status", () => notFoundPageChecks(ctx));
850
+ run("status", () => baselineCoverageChecks(ctx));
658
851
  run("international", () => internationalChecks(ctx));
852
+ run("assets", () => sourceOriginChecks(ctx));
659
853
  const counts = {
660
854
  error: 0,
661
855
  warning: 0,
@@ -691,6 +885,7 @@ function formatReport(report) {
691
885
  //#endregion
692
886
  //#region src/load.ts
693
887
  const DOCUMENT_EXT = /\.html?$/i;
888
+ const TEXT_EXT = /\.(css|m?js|cjs|json|xml|txt|svg)$/i;
694
889
  async function walk(dir, root, files = []) {
695
890
  const entries = await readdir(dir, { withFileTypes: true });
696
891
  for (const entry of entries) {
@@ -715,7 +910,8 @@ async function loadSiteDirectory(dir, site) {
715
910
  const files = await walk(dir, dir);
716
911
  const documents = [];
717
912
  const assets = [];
718
- let sitemap;
913
+ const texts = [];
914
+ const sitemaps = /* @__PURE__ */ new Map();
719
915
  for (const file of files.toSorted()) {
720
916
  const served = `/${file.split(sep).join("/")}`;
721
917
  if (DOCUMENT_EXT.test(file)) {
@@ -726,17 +922,56 @@ async function loadSiteDirectory(dir, site) {
726
922
  });
727
923
  continue;
728
924
  }
729
- if (/^\/sitemap[^/]*\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), "utf8");
925
+ if (/^\/sitemap[^/]*\.xml$/i.test(served)) sitemaps.set(served, await readFile(join(dir, file), "utf8"));
926
+ if (TEXT_EXT.test(file)) texts.push({
927
+ path: served,
928
+ content: await readFile(join(dir, file), "utf8")
929
+ });
730
930
  assets.push(served);
731
931
  }
732
932
  return {
733
933
  documents,
734
934
  assets,
735
- sitemap,
736
- site
935
+ files: texts,
936
+ sitemap: combineSitemaps(sitemaps, site),
937
+ site,
938
+ build: true
737
939
  };
738
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
973
+ }
739
974
  //#endregion
740
975
  export { CHECK_GROUPS as i, formatReport as n, verify as r, loadSiteDirectory as t };
741
976
 
742
- //# sourceMappingURL=load-KUG-8Ug4.mjs.map
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n"],"mappings":"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"}
@@ -47,6 +47,12 @@ interface VerifyReport {
47
47
  reason: string;
48
48
  }[];
49
49
  }
50
+ /** A text file that is not a document — a stylesheet, a script. */
51
+ interface VerifyFile {
52
+ /** Path as served (`/assets/app.css`). */
53
+ path: string;
54
+ content: string;
55
+ }
50
56
  interface VerifyOptions {
51
57
  /**
52
58
  * A redirect chain longer than this is reported. One hop is normal; two is a
@@ -62,6 +68,21 @@ interface VerifyOptions {
62
68
  soft404MaxTextLength?: number;
63
69
  /** Hosts to treat as internal besides `site`'s own. */
64
70
  internalHosts?: string[];
71
+ /**
72
+ * The origin the content was migrated away from — the old WordPress host.
73
+ *
74
+ * A migrated page that still points at it is not finished: the new site
75
+ * depends on the old one staying up, and the moment it does not, images and
76
+ * stylesheets disappear. Measured across a blind cohort it was the single
77
+ * largest blocker, so this is checked as an error when the origin is known.
78
+ */
79
+ sourceOrigin?: string;
80
+ /**
81
+ * Hosts that may legitimately still be referenced even though they are not
82
+ * the site's own — a CDN the customer keeps. Applies to
83
+ * `assets.source-origin-reference` only.
84
+ */
85
+ allowHosts?: string[];
65
86
  }
66
87
  interface VerifyInput {
67
88
  /** The built site. */
@@ -74,14 +95,33 @@ interface VerifyInput {
74
95
  redirects?: VerifyRedirect[];
75
96
  /** Non-document files the build produced, as served (`/images/a.png`). */
76
97
  assets?: string[];
98
+ /**
99
+ * Contents of non-document text files — stylesheets, scripts. Only the checks
100
+ * that scan content read these; everything else works from `documents` and
101
+ * the `assets` path list.
102
+ *
103
+ * Supplying them matters for `assets.source-origin-reference`: a `url()` in a
104
+ * stylesheet pointing at the old host is exactly as fatal as one in the HTML,
105
+ * and a scan that silently skips CSS reports a smaller number than a scan
106
+ * that does not — which is worse than not counting at all when two tools
107
+ * compare figures.
108
+ */
109
+ files?: VerifyFile[];
77
110
  /** The old site, for parity checks. Absent means only self-checks run. */
78
111
  baseline?: {
79
112
  documents: VerifyDocument[];
80
113
  };
114
+ /**
115
+ * Whether `documents` are a static build directory rather than pages captured
116
+ * from a running site. Build-level expectations — a `404.html` exists — are
117
+ * only meaningful for a build, and asserting them against a capture would
118
+ * report a defect the capture simply cannot show. `loadSiteDirectory` sets it.
119
+ */
120
+ build?: boolean;
81
121
  /** Defaults to every group. */
82
122
  groups?: readonly CheckGroup[];
83
123
  options?: VerifyOptions;
84
124
  }
85
125
  //#endregion
86
126
  export { VerifyDocument as a, VerifyRedirect as c, Severity as i, VerifyReport as l, CheckGroup as n, VerifyInput as o, Finding as r, VerifyOptions as s, CHECK_GROUPS as t };
87
- //# sourceMappingURL=types-C_hmH1n3.d.mts.map
127
+ //# sourceMappingURL=types-bYfzrRnM.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-bYfzrRnM.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;UAaiB,cAAA;EAAc;EAE7B,GAAA;EACA,IAAA;EADA;EAGA,MAAA;EAAA;EAEA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,cAAA;EACf,IAAA;EACA,EAAA;EACA,MAAA;AAAA;AAAA,cAGW,YAAA;AAAA,KAiBD,UAAA,WAAqB,YAAA;AAAA,KAErB,QAAA;;;;AAnBZ;;;UA2BiB,OAAA;EACf,KAAA,EAAO,UAAA;EACP,KAAA;EACA,QAAA,EAAU,QAAA;;EAEV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EAlBG;EAoBlB,MAAA;EACA,QAAA,EAAU,OAAA;EACV,MAAA,EAAQ,MAAA,CAAO,QAAA;;EAEf,SAAA;EAfA;EAiBA,MAAA,EAAQ,UAAA;EAhBR;EAkBA,OAAA;IAAW,KAAA,EAAO,UAAA;IAAY,MAAA;EAAA;AAAA;;UAIf,UAAA;EAdA;EAgBf,IAAA;EACA,OAAA;AAAA;AAAA,UAGe,aAAA;EAhBP;;;;;EAsBR,eAAA;EAvBA;;;;;EA6BA,oBAAA;EAxBA;EA0BA,aAAA;EAxBA;;;;;;AAIF;;EA6BE,YAAA;EA3BA;;AAIF;;;EA6BE,UAAA;AAAA;AAAA,UAGe,WAAA;EAlBf;EAoBA,SAAA,EAAW,cAAA;EALX;EAOA,IAAA;EAPU;EASV,OAAA;EAN0B;EAQ1B,SAAA,GAAY,cAAA;EAND;EAQX,MAAA;EAYQ;;;;;;;;;;;EAAR,KAAA,GAAQ,UAAA;EAZR;EAcA,QAAA;IAAa,SAAA,EAAW,cAAA;EAAA;EAAX;;;;;;EAOb,KAAA;EAGuB;EADvB,MAAA,YAAkB,UAAA;EAClB,OAAA,GAAU,aAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/verify",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "Portable SEO, indexing, link and asset gates for a migrated site — offline, zero-dependency",
6
6
  "type": "module",
@@ -40,16 +40,16 @@
40
40
  "files": [
41
41
  "dist"
42
42
  ],
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "tsdown": "^0.21.0",
46
+ "typescript": "^5.7.0"
47
+ },
43
48
  "scripts": {
44
49
  "build": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts",
45
50
  "dev": "tsdown src/index.ts src/cli.ts src/bin.ts --format esm --dts --watch",
46
51
  "test": "vitest run",
47
52
  "typecheck": "tsc --noEmit",
48
53
  "clean": "rm -rf dist"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^22.10.0",
52
- "tsdown": "^0.21.0",
53
- "typescript": "^5.7.0"
54
54
  }
55
- }
55
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"load-KUG-8Ug4.mjs","names":[],"sources":["../src/types.ts","../src/html.ts","../src/url.ts","../src/checks.ts","../src/verify.ts","../src/load.ts"],"sourcesContent":["// ─── What a verification run is given, and what it answers with ───\n//\n// Everything here is data the caller already has. The package never fetches:\n// a hardened crawler is a security surface of its own (SSRF, DNS rebinding,\n// redirect and byte limits) and belongs where that hardening lives, not in the\n// MIT library every consumer embeds. Whoever holds a safe fetcher — or a\n// directory of built files — supplies the documents.\n//\n// The consequence is that this runs identically over a `dist/` directory, over\n// a captured baseline, and inside another tool's pipeline, with no network and\n// no mocking.\n\n/** One page as served: the address it answers at and what it answered with. */\nexport interface VerifyDocument {\n /** Absolute URL, or a site-relative path (`/about/`). */\n url: string\n html: string\n /** Defaults to 200 when the caller did not record one. */\n status?: number\n /** Response headers, lowercased keys. `x-robots-tag` is read here. */\n headers?: Record<string, string>\n}\n\nexport interface VerifyRedirect {\n from: string\n to: string\n status: number\n}\n\nexport const CHECK_GROUPS = [\n /** canonical, `<title>`, description, Open Graph, Twitter card */\n 'identity',\n /** robots meta, X-Robots-Tag, noindex leaks, sitemap membership */\n 'indexing',\n /** status parity, soft-404, redirect chains */\n 'status',\n /** hreflang pairs and reciprocity */\n 'international',\n /** JSON-LD validity and type parity */\n 'structured',\n /** internal links, pagination, feeds */\n 'navigation',\n /** image resolution and alt preservation */\n 'assets',\n] as const\n\nexport type CheckGroup = (typeof CHECK_GROUPS)[number]\n\nexport type Severity = 'error' | 'warning' | 'info'\n\n/**\n * One thing that is wrong, or worth knowing.\n *\n * `check` is a stable id (`identity.canonical-mismatch`) so a consumer can\n * suppress, threshold or track one finding without matching on prose.\n */\nexport interface Finding {\n group: CheckGroup\n check: string\n severity: Severity\n /** The document the finding is about, when it is about one. */\n url?: string\n message: string\n detail?: string\n}\n\nexport interface VerifyReport {\n /** No `error` findings. This is the gate. */\n passed: boolean\n findings: Finding[]\n counts: Record<Severity, number>\n /** How many documents were examined. */\n documents: number\n /** Which groups actually ran. */\n groups: CheckGroup[]\n /** Groups that were asked for but could not run, and why. */\n skipped: { group: CheckGroup, reason: string }[]\n}\n\nexport interface VerifyOptions {\n /**\n * A redirect chain longer than this is reported. One hop is normal; two is a\n * link-equity leak and a latency cost, and chains grow silently as redirect\n * rules accumulate over migrations.\n */\n maxRedirectHops?: number\n /**\n * A 200 response shorter than this many characters of text, whose title or\n * body says \"not found\", is treated as a soft-404. Kept configurable because\n * a legitimately short page exists; the default is deliberately low.\n */\n soft404MaxTextLength?: number\n /** Hosts to treat as internal besides `site`'s own. */\n internalHosts?: string[]\n}\n\nexport interface VerifyInput {\n /** The built site. */\n documents: VerifyDocument[]\n /** The site's canonical origin (`https://example.com`). Several checks need it. */\n site?: string\n /** `sitemap.xml` as produced by the build. */\n sitemap?: string\n /** Redirect rules the host will serve. */\n redirects?: VerifyRedirect[]\n /** Non-document files the build produced, as served (`/images/a.png`). */\n assets?: string[]\n /** The old site, for parity checks. Absent means only self-checks run. */\n baseline?: { documents: VerifyDocument[] }\n /** Defaults to every group. */\n groups?: readonly CheckGroup[]\n options?: VerifyOptions\n}\n","// ─── Reading the few things a document has to say ───\n//\n// Targeted regexes, not a parser. The same choice `@contentrain/emitter-astro`\n// makes, for the same reason: these documents are machine-generated HTML whose\n// head we already control, the extracted set is small and fixed, and a parser\n// would put a dependency into the one package a migration is supposed to be\n// able to run anywhere.\n//\n// The limits are real and bounded. `head()` splits on the first `</head>`, so a\n// document without one yields nothing rather than guessing; attribute reads are\n// quote-aware but do not resolve entities beyond the five that matter. Where a\n// check would be wrong under those limits, it is not written.\n\n/** Everything before the first `</head>`, or '' when there is none. */\nexport function head(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? '' : html.slice(0, end)\n}\n\n/** Everything after the first `</head>`, or the whole document when there is none. */\nexport function body(html: string): string {\n const end = html.search(/<\\/head\\s*>/i)\n return end === -1 ? html : html.slice(end)\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", '#39': \"'\" }\n\nexport function decodeEntities(value: string): string {\n return value.replace(/&(#39|amp|lt|gt|quot|apos);/g, (_m, name: string) => ENTITIES[name] ?? _m)\n}\n\n/**\n * Read one attribute off a single tag's source. Quote-aware; unquoted allowed.\n *\n * The name must be preceded by whitespace, not merely a word boundary: `-` is\n * not a word character, so `\\bhref` matches inside `data-href` — and lazy-\n * loading WordPress themes put the real URL in `data-src` and a placeholder in\n * `src` on almost every image.\n */\nexport function attr(tag: string, name: string): string | undefined {\n const re = new RegExp(`(?:^|\\\\s)${name}\\\\s*=\\\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\\\s\"'>]+))`, 'i')\n const match = re.exec(tag)\n if (!match) return undefined\n return decodeEntities(match[1] ?? match[2] ?? match[3] ?? '')\n}\n\n/** Every `<tag …>` opening tag in the source, as raw strings. */\nexport function tags(source: string, name: string): string[] {\n return [...source.matchAll(new RegExp(`<${name}\\\\b[^>]*>`, 'gi'))].map(m => m[0])\n}\n\n/**\n * `<title>` texts from the head. `<svg>` blocks are removed first: a `<title>`\n * inside one is an accessibility label, valid markup, and not the page title —\n * counting it would report a duplicate on every page with an inline icon.\n */\nexport function titles(html: string): string[] {\n const source = head(html).replace(/<svg\\b[\\s\\S]*?<\\/svg\\s*>/gi, '')\n return [...source.matchAll(/<title\\b[^>]*>([\\s\\S]*?)<\\/title\\s*>/gi)]\n .map(m => decodeEntities(m[1] ?? '').trim())\n}\n\n/** `<meta name=\"…\">` / `<meta property=\"…\">` content, keyed by lowercased key. */\nexport function metaMap(html: string): Map<string, string[]> {\n const found = new Map<string, string[]>()\n for (const tag of tags(head(html), 'meta')) {\n const key = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (!key) continue\n const content = attr(tag, 'content') ?? ''\n found.set(key, [...(found.get(key) ?? []), content])\n }\n return found\n}\n\nexport interface LinkTag {\n rel: string\n href: string\n hreflang?: string\n type?: string\n}\n\n/** `<link>` tags from the head, with `rel` lowercased. */\nexport function links(html: string): LinkTag[] {\n const found: LinkTag[] = []\n for (const tag of tags(head(html), 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase()\n const href = attr(tag, 'href')\n if (!rel || href === undefined) continue\n found.push({ rel, href, hreflang: attr(tag, 'hreflang'), type: attr(tag, 'type') })\n }\n return found\n}\n\n/** Links whose `rel` list contains the given token. */\nexport function linksRel(html: string, token: string): LinkTag[] {\n return links(html).filter(link => link.rel.split(/\\s+/).includes(token))\n}\n\nexport interface JsonLdBlock {\n raw: string\n /** Parsed value, or undefined when the block is not valid JSON. */\n value?: unknown\n}\n\n/** Every `application/ld+json` block in the document, parsed where possible. */\nexport function jsonLd(html: string): JsonLdBlock[] {\n const blocks: JsonLdBlock[] = []\n const re = /<script\\b[^>]*\\btype\\s*=\\s*[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script\\s*>/gi\n for (const match of html.matchAll(re)) {\n const raw = (match[1] ?? '').trim()\n try {\n blocks.push({ raw, value: JSON.parse(raw) as unknown })\n } catch {\n blocks.push({ raw })\n }\n }\n return blocks\n}\n\n/**\n * `@type` values in a JSON-LD block, including those nested in `@graph` —\n * themes commonly ship one graph holding Organization, WebSite and WebPage,\n * and reading only the root would see none of them.\n */\nexport function jsonLdTypes(value: unknown): string[] {\n const found: string[] = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const item of node) walk(item)\n return\n }\n if (!node || typeof node !== 'object') return\n const record = node as Record<string, unknown>\n const type = record['@type']\n if (typeof type === 'string') found.push(type)\n else if (Array.isArray(type)) for (const t of type) if (typeof t === 'string') found.push(t)\n if ('@graph' in record) walk(record['@graph'])\n }\n walk(value)\n return found\n}\n\n/** `href` of every `<a>` in the document body. */\nexport function anchors(html: string): string[] {\n return tags(body(html), 'a')\n .map(tag => attr(tag, 'href'))\n .filter((href): href is string => href !== undefined)\n}\n\nexport interface ImageRef {\n src: string\n /** Absent means no `alt` attribute at all — different from `alt=\"\"`. */\n alt?: string\n}\n\n/** Every `<img>` in the document body. */\nexport function images(html: string): ImageRef[] {\n return tags(body(html), 'img')\n .map(tag => ({ src: attr(tag, 'src') ?? '', alt: attr(tag, 'alt') }))\n .filter(image => image.src !== '')\n}\n\n/** Visible text, roughly: script/style stripped, tags removed, runs collapsed. */\nexport function text(html: string): string {\n return body(html)\n .replace(/<(script|style)\\b[\\s\\S]*?<\\/\\1\\s*>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** `<loc>` entries of a sitemap or sitemap index. */\nexport function sitemapLocations(xml: string): string[] {\n return [...xml.matchAll(/<loc>\\s*([\\s\\S]*?)\\s*<\\/loc>/gi)].map(m => decodeEntities((m[1] ?? '').trim()))\n}\n","// ─── Addresses ───\n//\n// Every check that asks \"does this link go somewhere that exists\" is really\n// asking a normalisation question, and getting it wrong in either direction is\n// worse than not asking: too strict and a correct site fails its gate, too\n// loose and a broken one passes.\n//\n// The rules here are deliberately conservative. A trailing slash and an\n// `index.html` are the same address, because static hosts serve them that way.\n// A query string and a fragment are not part of a document's identity. Case is\n// preserved in the path, because static hosts are case-sensitive even when the\n// author assumed otherwise.\n\nconst SITE_FALLBACK = 'https://contentrain.invalid'\n\n/** Absolute URL for `href` as seen from `base`, or undefined when unresolvable. */\nexport function resolve(href: string, base: string, site?: string): string | undefined {\n try {\n const origin = site ?? SITE_FALLBACK\n const absoluteBase = base.startsWith('http') ? base : new URL(base, origin).href\n return new URL(href, absoluteBase).href\n } catch {\n return undefined\n }\n}\n\n/** Whether a resolved URL belongs to the site under test. */\nexport function isInternal(url: string, site?: string, extraHosts: readonly string[] = []): boolean {\n try {\n const parsed = new URL(url)\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false\n if (!site) return parsed.origin === SITE_FALLBACK || extraHosts.includes(parsed.host)\n return parsed.host === new URL(site).host || extraHosts.includes(parsed.host)\n } catch {\n return false\n }\n}\n\n/**\n * The identity of a document: path only, with `index.html` and a trailing\n * slash removed, query and fragment dropped. `/` stays `/`.\n */\nexport function identity(url: string, site?: string): string {\n const full = url.startsWith('http') ? url : resolve(url, '/', site) ?? url\n let path: string\n try {\n path = new URL(full).pathname\n } catch {\n path = full.split(/[?#]/)[0] ?? full\n }\n path = path.replace(/\\/index\\.html?$/i, '/')\n if (path.length > 1) path = path.replace(/\\/+$/, '')\n return path === '' ? '/' : path\n}\n\n/** Absolute form of a document's own address, when the site origin is known. */\nexport function absolute(url: string, site?: string): string | undefined {\n if (url.startsWith('http')) return url\n if (!site) return undefined\n try {\n return new URL(url, site).href\n } catch {\n return undefined\n }\n}\n","// ─── The gates ───\n//\n// Seven groups, from the migration contract's verification scope. Each check\n// has a stable id and answers one question; none of them fetches anything.\n//\n// Two rules shaped every check below. A finding is an `error` only when the\n// built site is *wrong* — a visitor or a crawler gets something it should not.\n// Everything that is merely worse than the old site is a `warning`, because a\n// migration that improves a page should not fail its own gate.\n\nimport type { Finding, VerifyDocument, VerifyInput, VerifyOptions } from './types.js'\nimport {\n anchors, images, jsonLd, jsonLdTypes, linksRel, metaMap, sitemapLocations, text, titles,\n} from './html.js'\nimport { absolute, identity, isInternal, resolve } from './url.js'\n\nexport interface Context {\n input: VerifyInput\n options: Required<Pick<VerifyOptions, 'maxRedirectHops' | 'soft404MaxTextLength'>> & VerifyOptions\n /** Document identity → document, for the built site. */\n byIdentity: Map<string, VerifyDocument>\n /** Baseline document identity → document. */\n baselineByIdentity: Map<string, VerifyDocument>\n /** Identities of everything the build serves: documents plus assets. */\n served: Set<string>\n}\n\nconst NOT_FOUND_TEXT = /\\b(404|not found|page not found|sayfa bulunamad[ıi]|nicht gefunden|introuvable|no se encontr)/i\n\nfunction finding(f: Finding): Finding {\n return f\n}\n\n// ─── identity ───\n\nexport function identityChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const url = doc.url\n const group = 'identity' as const\n const found = titles(doc.html)\n\n if (found.every(t => t === '')) {\n out.push(finding({ group, check: 'identity.title-missing', severity: 'error', url, message: 'No non-empty <title> in the head.' }))\n } else if (found.length > 1) {\n out.push(finding({\n group,\n check: 'identity.title-duplicate',\n severity: 'error',\n url,\n message: `${found.length} <title> elements in the head — a crawler takes one and it is not defined which.`,\n detail: found.join(' | '),\n }))\n }\n\n const meta = metaMap(doc.html)\n const description = meta.get('description')?.[0]?.trim()\n if (!description) {\n out.push(finding({ group, check: 'identity.description-missing', severity: 'warning', url, message: 'No meta description.' }))\n }\n\n const canonicals = linksRel(doc.html, 'canonical')\n if (canonicals.length === 0) {\n out.push(finding({ group, check: 'identity.canonical-missing', severity: 'warning', url, message: 'No canonical link.' }))\n } else if (canonicals.length > 1) {\n out.push(finding({\n group,\n check: 'identity.canonical-duplicate',\n severity: 'error',\n url,\n message: `${canonicals.length} canonical links — the page tells crawlers two different things about itself.`,\n detail: canonicals.map(c => c.href).join(' | '),\n }))\n } else {\n // The failure this catches is a template's canonical surviving a clone:\n // every page canonicalises to one URL and the site removes itself from the\n // index. It is worse than having no canonical at all.\n const declared = resolve(canonicals[0]!.href, url, ctx.input.site)\n const self = absolute(url, ctx.input.site) ?? resolve(url, '/', ctx.input.site)\n if (declared && self && identity(declared, ctx.input.site) !== identity(self, ctx.input.site)) {\n out.push(finding({\n group,\n check: 'identity.canonical-mismatch',\n severity: 'error',\n url,\n message: 'Canonical points at a different page.',\n detail: `${canonicals[0]!.href} ≠ ${url}`,\n }))\n }\n }\n\n const missingOg = ['og:title', 'og:type', 'og:url'].filter(key => !meta.get(key)?.[0])\n if (missingOg.length) {\n out.push(finding({\n group,\n check: 'identity.open-graph-incomplete',\n severity: 'warning',\n url,\n message: `Open Graph incomplete — missing ${missingOg.join(', ')}. Shares render without a card.`,\n }))\n }\n if (!meta.get('twitter:card')?.[0]) {\n out.push(finding({ group, check: 'identity.twitter-card-missing', severity: 'info', url, message: 'No twitter:card.' }))\n }\n\n return out\n}\n\n// ─── indexing ───\n\n/** `noindex` as declared by a robots meta or an X-Robots-Tag header. */\nfunction noindexReason(doc: VerifyDocument): string | undefined {\n const robots = metaMap(doc.html).get('robots')?.join(',') ?? ''\n if (/\\bnoindex\\b/i.test(robots)) return `<meta name=\"robots\" content=\"${robots}\">`\n const header = doc.headers?.['x-robots-tag'] ?? ''\n if (/\\bnoindex\\b/i.test(header)) return `X-Robots-Tag: ${header}`\n return undefined\n}\n\nexport function indexingChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'indexing' as const\n const url = doc.url\n\n // A staging `noindex` carried into production is silent: the site builds,\n // deploys, serves — and disappears from search over the following weeks.\n const reason = noindexReason(doc)\n if (reason) {\n out.push(finding({ group, check: 'indexing.noindex', severity: 'error', url, message: 'Page is marked noindex.', detail: reason }))\n }\n\n if (ctx.input.sitemap !== undefined) {\n const listed = new Set(sitemapLocations(ctx.input.sitemap).map(loc => identity(loc, ctx.input.site)))\n if (!listed.has(identity(url, ctx.input.site)) && !reason) {\n out.push(finding({ group, check: 'indexing.sitemap-missing-entry', severity: 'warning', url, message: 'Indexable page is not in the sitemap.' }))\n }\n }\n\n return out\n}\n\n/** Sitemap entries pointing at pages the build does not serve. */\nexport function sitemapStaleChecks(ctx: Context): Finding[] {\n if (ctx.input.sitemap === undefined) return []\n const out: Finding[] = []\n for (const loc of sitemapLocations(ctx.input.sitemap)) {\n const id = identity(loc, ctx.input.site)\n if (ctx.byIdentity.has(id)) continue\n out.push(finding({\n group: 'indexing',\n check: 'indexing.sitemap-stale-entry',\n severity: 'warning',\n url: loc,\n message: 'Sitemap lists a page the build does not serve.',\n }))\n }\n return out\n}\n\n// ─── status ───\n\nexport function statusChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'status' as const\n const url = doc.url\n const status = doc.status ?? 200\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const was = before.status ?? 200\n if (was !== status) {\n out.push(finding({\n group,\n check: 'status.mismatch',\n severity: 'error',\n url,\n message: `Status changed: ${was} → ${status}.`,\n }))\n }\n }\n\n // A \"not found\" page answering 200 is invisible to every monitor that looks\n // at status codes, and a crawler indexes it as content.\n if (status === 200) {\n const bodyText = text(doc.html)\n const title = titles(doc.html)[0] ?? ''\n const saysNotFound = NOT_FOUND_TEXT.test(title) || NOT_FOUND_TEXT.test(bodyText.slice(0, 400))\n if (saysNotFound && bodyText.length <= ctx.options.soft404MaxTextLength) {\n out.push(finding({\n group,\n check: 'status.soft-404',\n severity: 'error',\n url,\n message: 'Page says it was not found but answers 200.',\n detail: title || bodyText.slice(0, 120),\n }))\n }\n }\n\n return out\n}\n\n/** Redirect rules: chains, loops, and targets that do not exist. */\nexport function redirectChecks(ctx: Context): Finding[] {\n const rules = ctx.input.redirects\n if (!rules?.length) return []\n const out: Finding[] = []\n const group = 'status' as const\n const byFrom = new Map(rules.map(rule => [identity(rule.from, ctx.input.site), rule]))\n\n for (const rule of rules) {\n const seen = new Set<string>([identity(rule.from, ctx.input.site)])\n let hops = 1\n let target = identity(rule.to, ctx.input.site)\n\n while (byFrom.has(target)) {\n if (seen.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-loop',\n severity: 'error',\n url: rule.from,\n message: 'Redirect chain loops.',\n detail: [...seen, target].join(' → '),\n }))\n break\n }\n seen.add(target)\n target = identity(byFrom.get(target)!.to, ctx.input.site)\n hops += 1\n }\n\n if (hops > ctx.options.maxRedirectHops) {\n out.push(finding({\n group,\n check: 'status.redirect-chain',\n severity: 'warning',\n url: rule.from,\n message: `Redirect chain is ${hops} hops.`,\n detail: [...seen, target].join(' → '),\n }))\n }\n\n const external = !isInternal(resolve(rule.to, rule.from, ctx.input.site) ?? '', ctx.input.site, ctx.options.internalHosts)\n if (!external && !ctx.served.has(target) && !byFrom.has(target)) {\n out.push(finding({\n group,\n check: 'status.redirect-target-missing',\n severity: 'error',\n url: rule.from,\n message: 'Redirect points at a page the build does not serve.',\n detail: rule.to,\n }))\n }\n }\n\n return out\n}\n\n// ─── international ───\n\n/**\n * hreflang has to be symmetric: if A offers B as its Turkish alternate, B must\n * offer A back. A one-sided declaration is ignored by search engines entirely,\n * so a half-migrated set of alternates is not a partial win — it is nothing.\n */\nexport function internationalChecks(ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'international' as const\n const declared = new Map<string, Map<string, string>>()\n\n for (const doc of ctx.input.documents) {\n const self = identity(doc.url, ctx.input.site)\n const alternates = new Map<string, string>()\n for (const link of linksRel(doc.html, 'alternate')) {\n if (!link.hreflang) continue\n const target = resolve(link.href, doc.url, ctx.input.site)\n if (!target) continue\n alternates.set(link.hreflang.toLowerCase(), identity(target, ctx.input.site))\n }\n if (alternates.size) declared.set(self, alternates)\n }\n\n for (const [self, alternates] of declared) {\n const listsSelf = [...alternates.values()].includes(self)\n if (!listsSelf) {\n out.push(finding({\n group,\n check: 'international.hreflang-no-self',\n severity: 'warning',\n url: self,\n message: 'Page declares hreflang alternates but does not list itself.',\n }))\n }\n for (const [lang, target] of alternates) {\n if (target === self) continue\n if (!ctx.byIdentity.has(target)) {\n out.push(finding({\n group,\n check: 'international.hreflang-unknown-target',\n severity: 'warning',\n url: self,\n message: `hreflang=\"${lang}\" points at a page the build does not serve.`,\n detail: target,\n }))\n continue\n }\n const back = declared.get(target)\n if (!back || ![...back.values()].includes(self)) {\n out.push(finding({\n group,\n check: 'international.hreflang-not-reciprocal',\n severity: 'error',\n url: self,\n message: `hreflang=\"${lang}\" names ${target}, which does not name it back — search engines ignore one-sided alternates.`,\n }))\n }\n }\n }\n\n return out\n}\n\n// ─── structured data ───\n\nexport function structuredChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'structured' as const\n const url = doc.url\n const blocks = jsonLd(doc.html)\n\n for (const block of blocks) {\n if (block.value === undefined) {\n out.push(finding({\n group,\n check: 'structured.jsonld-invalid',\n severity: 'error',\n url,\n message: 'A JSON-LD block does not parse — the whole block is discarded by consumers.',\n detail: block.raw.slice(0, 120),\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (!before) return out\n\n const had = new Set(jsonLd(before.html).flatMap(block => jsonLdTypes(block.value)))\n const has = new Set(blocks.flatMap(block => jsonLdTypes(block.value)))\n const lost = [...had].filter(type => !has.has(type))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'structured.type-lost',\n severity: 'warning',\n url,\n message: `Structured data types the old page had are gone: ${lost.join(', ')}.`,\n }))\n }\n\n return out\n}\n\n// ─── navigation ───\n\nexport function navigationChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'navigation' as const\n const url = doc.url\n const redirected = new Set((ctx.input.redirects ?? []).map(rule => identity(rule.from, ctx.input.site)))\n\n const broken = new Set<string>()\n for (const href of anchors(doc.html)) {\n if (/^(#|mailto:|tel:|javascript:|data:)/i.test(href)) continue\n const target = resolve(href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n const id = identity(target, ctx.input.site)\n if (ctx.served.has(id) || redirected.has(id)) continue\n broken.add(href)\n }\n for (const href of broken) {\n out.push(finding({ group, check: 'navigation.broken-internal-link', severity: 'error', url, message: 'Internal link goes nowhere.', detail: href }))\n }\n\n for (const token of ['next', 'prev'] as const) {\n for (const link of linksRel(doc.html, token)) {\n const target = resolve(link.href, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n out.push(finding({\n group,\n check: 'navigation.pagination-broken',\n severity: 'warning',\n url,\n message: `rel=\"${token}\" points at a page the build does not serve.`,\n detail: link.href,\n }))\n }\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const feedRel = (html: string) => linksRel(html, 'alternate').some(link => /rss|atom|feed/i.test(link.type ?? ''))\n if (feedRel(before.html) && !feedRel(doc.html)) {\n out.push(finding({ group, check: 'navigation.feed-lost', severity: 'warning', url, message: 'The old page advertised a feed; this one does not.' }))\n }\n }\n\n return out\n}\n\n// ─── assets ───\n\nexport function assetChecks(doc: VerifyDocument, ctx: Context): Finding[] {\n const out: Finding[] = []\n const group = 'assets' as const\n const url = doc.url\n const refs = images(doc.html)\n\n const broken = new Set<string>()\n for (const image of refs) {\n if (/^data:/i.test(image.src)) continue\n const target = resolve(image.src, url, ctx.input.site)\n if (!target || !isInternal(target, ctx.input.site, ctx.options.internalHosts)) continue\n if (ctx.served.has(identity(target, ctx.input.site))) continue\n broken.add(image.src)\n }\n for (const src of broken) {\n out.push(finding({ group, check: 'assets.broken-image', severity: 'error', url, message: 'Image does not resolve to anything the build serves.', detail: src }))\n }\n\n // `alt=\"\"` is a decision (decorative); no `alt` attribute at all is an\n // omission. Only the second is reported.\n const noAlt = refs.filter(image => image.alt === undefined)\n if (noAlt.length) {\n out.push(finding({\n group,\n check: 'assets.missing-alt',\n severity: 'warning',\n url,\n message: `${noAlt.length} image${noAlt.length === 1 ? '' : 's'} without an alt attribute.`,\n detail: noAlt.slice(0, 5).map(image => image.src).join(', '),\n }))\n }\n\n const before = ctx.baselineByIdentity.get(identity(url, ctx.input.site))\n if (before) {\n const had = new Map(images(before.html).filter(i => i.alt).map(i => [identity(resolve(i.src, before.url, ctx.input.site) ?? i.src, ctx.input.site), i.alt!]))\n const now = new Map(refs.map(i => [identity(resolve(i.src, url, ctx.input.site) ?? i.src, ctx.input.site), i.alt]))\n const lost = [...had.keys()].filter(src => now.has(src) && !now.get(src))\n if (lost.length) {\n out.push(finding({\n group,\n check: 'assets.alt-lost',\n severity: 'warning',\n url,\n message: `${lost.length} image${lost.length === 1 ? '' : 's'} lost alt text the old page had.`,\n detail: lost.slice(0, 5).join(', '),\n }))\n }\n }\n\n return out\n}\n","// ─── Running the gates ───\n\nimport type { CheckGroup, Finding, Severity, VerifyInput, VerifyReport } from './types.js'\nimport { CHECK_GROUPS } from './types.js'\nimport type { Context } from './checks.js'\nimport {\n assetChecks, identityChecks, indexingChecks, internationalChecks, navigationChecks,\n redirectChecks, sitemapStaleChecks, statusChecks, structuredChecks,\n} from './checks.js'\nimport { identity } from './url.js'\n\nconst DEFAULTS = { maxRedirectHops: 1, soft404MaxTextLength: 600 }\n\n/**\n * Checks that need an input the caller did not supply. Reported rather than\n * silently omitted: a green report over a run that never compared anything is\n * the most dangerous output this package could produce.\n */\nfunction absentInputs(input: VerifyInput, groups: readonly CheckGroup[]): { group: CheckGroup, reason: string }[] {\n const skipped: { group: CheckGroup, reason: string }[] = []\n if (!input.baseline) {\n for (const group of ['status', 'structured', 'navigation', 'assets'] as const) {\n if (groups.includes(group)) skipped.push({ group, reason: 'no baseline — parity checks did not run' })\n }\n }\n if (input.sitemap === undefined && groups.includes('indexing')) {\n skipped.push({ group: 'indexing', reason: 'no sitemap — membership checks did not run' })\n }\n if (!input.redirects?.length && groups.includes('status')) {\n skipped.push({ group: 'status', reason: 'no redirect rules — chain and target checks did not run' })\n }\n return skipped\n}\n\nfunction buildContext(input: VerifyInput): Context {\n const options = { ...DEFAULTS, ...input.options }\n const byIdentity = new Map(input.documents.map(doc => [identity(doc.url, input.site), doc]))\n const baselineByIdentity = new Map((input.baseline?.documents ?? []).map(doc => [identity(doc.url, input.site), doc]))\n const served = new Set<string>([\n ...byIdentity.keys(),\n ...(input.assets ?? []).map(asset => identity(asset, input.site)),\n ])\n return { input, options, byIdentity, baselineByIdentity, served }\n}\n\n/**\n * Run the gates over a built site.\n *\n * Nothing is fetched. `input.documents` is the built site as served, and\n * `input.baseline` — when the caller has one — is the site it replaced. Without\n * a baseline every self-check still runs; only the parity checks (status\n * change, lost structured data, lost alt text, lost feed) go quiet, because\n * there is nothing to compare against.\n */\nexport function verify(input: VerifyInput): VerifyReport {\n const groups = [...(input.groups ?? CHECK_GROUPS)]\n const ctx = buildContext(input)\n const findings: Finding[] = []\n const skipped = absentInputs(input, groups)\n\n const run = (group: CheckGroup, fn: () => Finding[]) => {\n if (!groups.includes(group)) return\n findings.push(...fn())\n }\n\n for (const doc of input.documents) {\n run('identity', () => identityChecks(doc, ctx))\n run('indexing', () => indexingChecks(doc, ctx))\n run('status', () => statusChecks(doc, ctx))\n run('structured', () => structuredChecks(doc, ctx))\n run('navigation', () => navigationChecks(doc, ctx))\n run('assets', () => assetChecks(doc, ctx))\n }\n\n // Site-wide checks: these read the whole set, not one document.\n run('indexing', () => sitemapStaleChecks(ctx))\n run('status', () => redirectChecks(ctx))\n run('international', () => internationalChecks(ctx))\n\n const counts: Record<Severity, number> = { error: 0, warning: 0, info: 0 }\n for (const item of findings) counts[item.severity] += 1\n\n return {\n passed: counts.error === 0,\n findings,\n counts,\n documents: input.documents.length,\n groups,\n skipped,\n }\n}\n\n/** A report as lines, most severe first. Used by the CLI and by consumers. */\nexport function formatReport(report: VerifyReport): string {\n const order: Severity[] = ['error', 'warning', 'info']\n const lines: string[] = []\n for (const severity of order) {\n for (const item of report.findings.filter(f => f.severity === severity)) {\n const where = item.url ? ` ${item.url}` : ''\n const detail = item.detail ? `\\n ${item.detail}` : ''\n lines.push(`${severity.toUpperCase().padEnd(7)} ${item.check}${where}\\n ${item.message}${detail}`)\n }\n }\n for (const { group, reason } of report.skipped) {\n lines.push(`SKIPPED ${group} — ${reason}`)\n }\n lines.push(\n `\\n${report.documents} document${report.documents === 1 ? '' : 's'} · `\n + `${report.counts.error} error · ${report.counts.warning} warning · ${report.counts.info} info · `\n + (report.passed ? 'PASS' : 'FAIL'),\n )\n return lines.join('\\n')\n}\n","// ─── Reading a built directory ───\n//\n// The one place this package touches a filesystem. Kept apart from the checks\n// so the engine stays pure: a consumer that already holds the documents — a\n// migration runner, a CI step with an artefact — imports `verify` and never\n// loads this module.\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\nimport type { VerifyDocument, VerifyInput } from './types.js'\n\nconst DOCUMENT_EXT = /\\.html?$/i\n\nasync function walk(dir: string, root: string, files: string[] = []): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(full, root, files)\n continue\n }\n files.push(relative(root, full))\n }\n return files\n}\n\n/**\n * Load a static build as a verification input: every `.html` becomes a\n * document served at its path, everything else becomes an asset.\n *\n * `dist/about/index.html` is served at `/about/`, which `identity()` then\n * reduces to `/about` — the same address a link to `/about/` or\n * `/about/index.html` resolves to.\n */\nexport async function loadSiteDirectory(dir: string, site?: string): Promise<Pick<VerifyInput, 'documents' | 'assets' | 'sitemap' | 'site'>> {\n const files = await walk(dir, dir)\n const documents: VerifyDocument[] = []\n const assets: string[] = []\n let sitemap: string | undefined\n\n for (const file of files.toSorted()) {\n const served = `/${file.split(sep).join('/')}`\n if (DOCUMENT_EXT.test(file)) {\n documents.push({ url: served, html: await readFile(join(dir, file), 'utf8'), status: 200 })\n continue\n }\n if (/^\\/sitemap[^/]*\\.xml$/i.test(served)) sitemap = await readFile(join(dir, file), 'utf8')\n assets.push(served)\n }\n\n return { documents, assets, sitemap, site }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-C_hmH1n3.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;UAaiB,cAAA;EAAc;EAE7B,GAAA;EACA,IAAA;EADA;EAGA,MAAA;EAAA;EAEA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,cAAA;EACf,IAAA;EACA,EAAA;EACA,MAAA;AAAA;AAAA,cAGW,YAAA;AAAA,KAiBD,UAAA,WAAqB,YAAA;AAAA,KAErB,QAAA;;;;AAnBZ;;;UA2BiB,OAAA;EACf,KAAA,EAAO,UAAA;EACP,KAAA;EACA,QAAA,EAAU,QAAA;;EAEV,GAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EAlBG;EAoBlB,MAAA;EACA,QAAA,EAAU,OAAA;EACV,MAAA,EAAQ,MAAA,CAAO,QAAA;;EAEf,SAAA;EAfA;EAiBA,MAAA,EAAQ,UAAA;EAhBR;EAkBA,OAAA;IAAW,KAAA,EAAO,UAAA;IAAY,MAAA;EAAA;AAAA;AAAA,UAGf,aAAA;EAhBT;AAGR;;;;EAmBE,eAAA;EAfQ;;;;;EAqBR,oBAAA;EAtBA;EAwBA,aAAA;AAAA;AAAA,UAGe,WAAA;EA1BA;EA4Bf,SAAA,EAAW,cAAA;EAxBX;EA0BA,IAAA;EAxBA;EA0BA,OAAA;EA1BkB;EA4BlB,SAAA,GAAY,cAAA;EA5BwB;EA8BpC,MAAA;EA3Be;EA6Bf,QAAA;IAAa,SAAA,EAAW,cAAA;EAAA;EAvBxB;EAyBA,MAAA,YAAkB,UAAA;EAClB,OAAA,GAAU,aAAA;AAAA"}