pagetrace 0.6.0 → 0.8.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/CHANGELOG.md +35 -0
- package/README.md +17 -6
- package/dist/cli.cjs +91 -18
- package/dist/cli.js +91 -18
- package/dist/index.cjs +89 -16
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +89 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
While the version is below 1.0.0, breaking changes ship in a minor release.
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-09-06
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `siteUrl` now overrides the crawled origin for an origin crawl, not just a
|
|
14
|
+
`--dir` crawl. Checking a local build or a preview deployment means crawling
|
|
15
|
+
`http://localhost:3000` while every page's canonical points at production, so
|
|
16
|
+
`canonical.offsite` fired on every page and made the check useless in exactly
|
|
17
|
+
the setup it is most wanted. Where you crawl and what the site calls itself are
|
|
18
|
+
separate things; `siteUrl` is now the authority on the latter. Without it the
|
|
19
|
+
crawled origin is still used, so nothing changes for a plain production crawl.
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2026-09-06
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- The terminal audit report is laid out rather than printed. Findings sit under a
|
|
26
|
+
rule per severity, prose wraps to the terminal instead of running off the right
|
|
27
|
+
edge, the affected routes get their own line, and the page count is right
|
|
28
|
+
aligned against the headline. The old output put explanation, fix and routes at
|
|
29
|
+
the same indent with no wrapping, so on a real site the useful part was the
|
|
30
|
+
hardest part to find.
|
|
31
|
+
- `formatAuditPretty(groups, meta, columns?)` takes an optional terminal width,
|
|
32
|
+
defaulting to 80. The CLI passes `process.stdout.columns`, which keeps
|
|
33
|
+
`report.ts` free of environment reads. Piped output falls back to 80 and drops
|
|
34
|
+
colour, so it stays readable in a CI log.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- A word longer than the terminal is hard-broken instead of overflowing. A
|
|
39
|
+
canonical URL is a single word and is routinely longer than 80 characters, so
|
|
40
|
+
`duplicate.canonical` could push a line well past the edge.
|
|
41
|
+
|
|
9
42
|
## [0.6.0] - 2026-09-06
|
|
10
43
|
|
|
11
44
|
### Changed
|
|
@@ -216,6 +249,8 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
|
|
|
216
249
|
crawling; diff classified by transition; absolute, cross-page and hreflang audit
|
|
217
250
|
rules; pretty, JSON, markdown, GitHub and HTML reporters.
|
|
218
251
|
|
|
252
|
+
[0.8.0]: https://github.com/shyamexe/pagetrace/compare/v0.7.0...v0.8.0
|
|
253
|
+
[0.7.0]: https://github.com/shyamexe/pagetrace/compare/v0.6.0...v0.7.0
|
|
219
254
|
[0.6.0]: https://github.com/shyamexe/pagetrace/compare/v0.5.0...v0.6.0
|
|
220
255
|
[0.5.0]: https://github.com/shyamexe/pagetrace/compare/v0.4.0...v0.5.0
|
|
221
256
|
[0.4.0]: https://github.com/shyamexe/pagetrace/compare/v0.3.0...v0.4.0
|
package/README.md
CHANGED
|
@@ -72,16 +72,27 @@ npx pagetrace audit --url https://example.com --format html --out audit.html
|
|
|
72
72
|
Findings are rolled up by issue rather than by page, so one template defect reads as a single row affecting 43 pages instead of 43 separate lines. Each row carries why it matters and how to fix it, and the fix is platform-aware — `pagetrace` reads the generator tag and asset paths, so a WordPress site gets Yoast and Rank Math instructions rather than generic advice.
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
|
|
75
|
+
pagetrace · https://acme.test
|
|
76
|
+
43 pages · WordPress · 2026-09-06
|
|
77
|
+
|
|
78
|
+
ERRORS ─────────────────────────────────────────────────────────────────────── 2
|
|
79
|
+
|
|
80
|
+
✗ 2 pages canonicalise to https://acme.test/shop. 2 pages
|
|
76
81
|
Several pages pointing at one canonical means those pages are declaring
|
|
77
82
|
themselves duplicates and will not rank independently.
|
|
78
|
-
|
|
83
|
+
→ A common symptom of a plugin canonicalising every archive page to the
|
|
84
|
+
parent.
|
|
85
|
+
/shop/page/2, /shop/page/3
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
✗ Page has no <h1>. 1 page
|
|
81
88
|
The h1 anchors the document outline used for passage extraction.
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
→ Many themes render the post title as h2 inside archive templates. Check
|
|
90
|
+
single.php or the block template for this post type.
|
|
84
91
|
/tag/widgets
|
|
92
|
+
|
|
93
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
2 issues 2 errors
|
|
95
|
+
3 findings across 43 pages
|
|
85
96
|
```
|
|
86
97
|
|
|
87
98
|
Auditing runs cross-page rules the per-page checks cannot see: duplicate titles and descriptions, several pages canonicalising to one URL, canonicals pointing away from their own path or at another host entirely, and a full hreflang check. Paginated archives and AMP variants are left alone, since canonicalising those to their parent is correct.
|
|
@@ -145,7 +156,7 @@ Alongside the diff, `check` runs absolute rules: missing title, canonical, `h1`,
|
|
|
145
156
|
}
|
|
146
157
|
```
|
|
147
158
|
|
|
148
|
-
`siteUrl` is
|
|
159
|
+
`siteUrl` is what the site calls itself, which is not always where you are crawling it. It is what `canonical.offsite` compares against, so set it when checking a `--dir` build, and when crawling a local build or a preview deployment whose pages carry production canonicals. A plain crawl of production infers it.
|
|
149
160
|
|
|
150
161
|
Every finding has a stable `code`. Set any code to `error`, `warn`, `info`, or `off`.
|
|
151
162
|
|
package/dist/cli.cjs
CHANGED
|
@@ -940,33 +940,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
940
940
|
const shown = routes.slice(0, limit).join(", ");
|
|
941
941
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
942
942
|
}
|
|
943
|
-
|
|
943
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
944
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
945
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
946
|
+
function wrapText(text2, width) {
|
|
947
|
+
const lines = [];
|
|
948
|
+
let line = "";
|
|
949
|
+
const flush = () => {
|
|
950
|
+
if (line) lines.push(line);
|
|
951
|
+
line = "";
|
|
952
|
+
};
|
|
953
|
+
for (const word of text2.split(/\s+/)) {
|
|
954
|
+
if (word.length > width) {
|
|
955
|
+
flush();
|
|
956
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (line && line.length + 1 + word.length > width) {
|
|
960
|
+
flush();
|
|
961
|
+
line = word;
|
|
962
|
+
} else {
|
|
963
|
+
line = line ? `${line} ${word}` : word;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
flush();
|
|
967
|
+
return lines;
|
|
968
|
+
}
|
|
969
|
+
function sectionRule(severity, count, width) {
|
|
970
|
+
const label = PLURAL[severity].toUpperCase();
|
|
971
|
+
const tail = String(count);
|
|
972
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
973
|
+
return `${BADGE[severity](import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\u2500".repeat(dashes))} ${import_picocolors.default.dim(tail)}`;
|
|
974
|
+
}
|
|
975
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
976
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
977
|
+
const indent = " ";
|
|
978
|
+
const body = width - indent.length;
|
|
944
979
|
const lines = [
|
|
945
|
-
import_picocolors.default.bold(meta.target
|
|
946
|
-
import_picocolors.default.dim(
|
|
980
|
+
`${import_picocolors.default.bold("pagetrace")} ${import_picocolors.default.dim("\xB7")} ${meta.target}`,
|
|
981
|
+
import_picocolors.default.dim(
|
|
982
|
+
[
|
|
983
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
984
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
985
|
+
meta.generatedAt
|
|
986
|
+
].filter(Boolean).join(" \xB7 ")
|
|
987
|
+
),
|
|
947
988
|
""
|
|
948
989
|
];
|
|
949
990
|
if (groups.length === 0) {
|
|
950
|
-
lines.push(import_picocolors.default.green(
|
|
991
|
+
lines.push(import_picocolors.default.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
951
992
|
return lines.join("\n");
|
|
952
993
|
}
|
|
953
|
-
for (const
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
994
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
995
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
996
|
+
if (inSeverity.length === 0) continue;
|
|
997
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
998
|
+
for (const group of inSeverity) {
|
|
999
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1000
|
+
const headline = group.message;
|
|
1001
|
+
const room = width - 2 - scope.length - 1;
|
|
1002
|
+
if (headline.length <= room) {
|
|
1003
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1004
|
+
lines.push(
|
|
1005
|
+
`${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(headline)}${pad}${import_picocolors.default.dim(scope)}`
|
|
1006
|
+
);
|
|
1007
|
+
} else {
|
|
1008
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1009
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(line)}` : `${indent}${import_picocolors.default.bold(line)}`);
|
|
1010
|
+
}
|
|
1011
|
+
lines.push(`${indent}${import_picocolors.default.dim(scope)}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (group.detail) {
|
|
1014
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (group.fix) {
|
|
1017
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1018
|
+
lines.push(`${indent}${import_picocolors.default.cyan("\u2192")} ${first}`);
|
|
1019
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (group.routes.length > 0) {
|
|
1022
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1023
|
+
lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
lines.push("");
|
|
1027
|
+
}
|
|
960
1028
|
}
|
|
961
1029
|
const { issues, instances, total } = countIssues(groups);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
);
|
|
1030
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
|
|
1031
|
+
lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
|
|
1032
|
+
lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
965
1033
|
lines.push(
|
|
966
1034
|
import_picocolors.default.dim(
|
|
967
|
-
|
|
1035
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
968
1036
|
)
|
|
969
1037
|
);
|
|
1038
|
+
lines.push("");
|
|
970
1039
|
return lines.join("\n");
|
|
971
1040
|
}
|
|
972
1041
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1382,7 +1451,11 @@ async function snapshotFromOrigin(origin, options = {}) {
|
|
|
1382
1451
|
const base = new URL(origin);
|
|
1383
1452
|
const agents = options.aiAgents ?? DEFAULT_AI_AGENTS;
|
|
1384
1453
|
const timeout = options.timeout;
|
|
1385
|
-
const site = {
|
|
1454
|
+
const site = {
|
|
1455
|
+
origin: options.siteUrl ? new URL(options.siteUrl).origin : base.origin,
|
|
1456
|
+
robotsTxt: null,
|
|
1457
|
+
llmsTxt: null
|
|
1458
|
+
};
|
|
1386
1459
|
const limit = options.limit ?? 200;
|
|
1387
1460
|
const robots = await fetchText(new URL("/robots.txt", base).href, timeout);
|
|
1388
1461
|
if (robots !== null) site.robotsTxt = extractRobotsTxt(robots, agents);
|
|
@@ -1559,7 +1632,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1559
1632
|
pageCount: pages.length,
|
|
1560
1633
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1561
1634
|
};
|
|
1562
|
-
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta);
|
|
1635
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1563
1636
|
if (flags.out) {
|
|
1564
1637
|
await (0, import_promises2.writeFile)(flags.out, `${output}
|
|
1565
1638
|
`, "utf8");
|
|
@@ -1572,7 +1645,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1572
1645
|
}
|
|
1573
1646
|
});
|
|
1574
1647
|
cli.help();
|
|
1575
|
-
cli.version("0.
|
|
1648
|
+
cli.version("0.8.0");
|
|
1576
1649
|
async function main() {
|
|
1577
1650
|
try {
|
|
1578
1651
|
cli.parse(process.argv, { run: false });
|
package/dist/cli.js
CHANGED
|
@@ -917,33 +917,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
917
917
|
const shown = routes.slice(0, limit).join(", ");
|
|
918
918
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
919
919
|
}
|
|
920
|
-
|
|
920
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
921
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
922
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
923
|
+
function wrapText(text2, width) {
|
|
924
|
+
const lines = [];
|
|
925
|
+
let line = "";
|
|
926
|
+
const flush = () => {
|
|
927
|
+
if (line) lines.push(line);
|
|
928
|
+
line = "";
|
|
929
|
+
};
|
|
930
|
+
for (const word of text2.split(/\s+/)) {
|
|
931
|
+
if (word.length > width) {
|
|
932
|
+
flush();
|
|
933
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (line && line.length + 1 + word.length > width) {
|
|
937
|
+
flush();
|
|
938
|
+
line = word;
|
|
939
|
+
} else {
|
|
940
|
+
line = line ? `${line} ${word}` : word;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
flush();
|
|
944
|
+
return lines;
|
|
945
|
+
}
|
|
946
|
+
function sectionRule(severity, count, width) {
|
|
947
|
+
const label = PLURAL[severity].toUpperCase();
|
|
948
|
+
const tail = String(count);
|
|
949
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
950
|
+
return `${BADGE[severity](pc.bold(label))} ${pc.dim("\u2500".repeat(dashes))} ${pc.dim(tail)}`;
|
|
951
|
+
}
|
|
952
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
953
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
954
|
+
const indent = " ";
|
|
955
|
+
const body = width - indent.length;
|
|
921
956
|
const lines = [
|
|
922
|
-
pc.bold(meta.target
|
|
923
|
-
pc.dim(
|
|
957
|
+
`${pc.bold("pagetrace")} ${pc.dim("\xB7")} ${meta.target}`,
|
|
958
|
+
pc.dim(
|
|
959
|
+
[
|
|
960
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
961
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
962
|
+
meta.generatedAt
|
|
963
|
+
].filter(Boolean).join(" \xB7 ")
|
|
964
|
+
),
|
|
924
965
|
""
|
|
925
966
|
];
|
|
926
967
|
if (groups.length === 0) {
|
|
927
|
-
lines.push(pc.green(
|
|
968
|
+
lines.push(pc.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
928
969
|
return lines.join("\n");
|
|
929
970
|
}
|
|
930
|
-
for (const
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
971
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
972
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
973
|
+
if (inSeverity.length === 0) continue;
|
|
974
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
975
|
+
for (const group of inSeverity) {
|
|
976
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
977
|
+
const headline = group.message;
|
|
978
|
+
const room = width - 2 - scope.length - 1;
|
|
979
|
+
if (headline.length <= room) {
|
|
980
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
981
|
+
lines.push(
|
|
982
|
+
`${BADGE[severity](GLYPH[severity])} ${pc.bold(headline)}${pad}${pc.dim(scope)}`
|
|
983
|
+
);
|
|
984
|
+
} else {
|
|
985
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
986
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${pc.bold(line)}` : `${indent}${pc.bold(line)}`);
|
|
987
|
+
}
|
|
988
|
+
lines.push(`${indent}${pc.dim(scope)}`);
|
|
989
|
+
}
|
|
990
|
+
if (group.detail) {
|
|
991
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${pc.dim(line)}`);
|
|
992
|
+
}
|
|
993
|
+
if (group.fix) {
|
|
994
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
995
|
+
lines.push(`${indent}${pc.cyan("\u2192")} ${first}`);
|
|
996
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
997
|
+
}
|
|
998
|
+
if (group.routes.length > 0) {
|
|
999
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1000
|
+
lines.push(`${indent}${pc.dim(line)}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
}
|
|
937
1005
|
}
|
|
938
1006
|
const { issues, instances, total } = countIssues(groups);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
);
|
|
1007
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
|
|
1008
|
+
lines.push(pc.dim("\u2500".repeat(width)));
|
|
1009
|
+
lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
942
1010
|
lines.push(
|
|
943
1011
|
pc.dim(
|
|
944
|
-
|
|
1012
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
945
1013
|
)
|
|
946
1014
|
);
|
|
1015
|
+
lines.push("");
|
|
947
1016
|
return lines.join("\n");
|
|
948
1017
|
}
|
|
949
1018
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1359,7 +1428,11 @@ async function snapshotFromOrigin(origin, options = {}) {
|
|
|
1359
1428
|
const base = new URL(origin);
|
|
1360
1429
|
const agents = options.aiAgents ?? DEFAULT_AI_AGENTS;
|
|
1361
1430
|
const timeout = options.timeout;
|
|
1362
|
-
const site = {
|
|
1431
|
+
const site = {
|
|
1432
|
+
origin: options.siteUrl ? new URL(options.siteUrl).origin : base.origin,
|
|
1433
|
+
robotsTxt: null,
|
|
1434
|
+
llmsTxt: null
|
|
1435
|
+
};
|
|
1363
1436
|
const limit = options.limit ?? 200;
|
|
1364
1437
|
const robots = await fetchText(new URL("/robots.txt", base).href, timeout);
|
|
1365
1438
|
if (robots !== null) site.robotsTxt = extractRobotsTxt(robots, agents);
|
|
@@ -1536,7 +1609,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1536
1609
|
pageCount: pages.length,
|
|
1537
1610
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1538
1611
|
};
|
|
1539
|
-
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta);
|
|
1612
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : flags.format === "html" ? formatAuditHtml(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1540
1613
|
if (flags.out) {
|
|
1541
1614
|
await writeFile(flags.out, `${output}
|
|
1542
1615
|
`, "utf8");
|
|
@@ -1549,7 +1622,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1549
1622
|
}
|
|
1550
1623
|
});
|
|
1551
1624
|
cli.help();
|
|
1552
|
-
cli.version("0.
|
|
1625
|
+
cli.version("0.8.0");
|
|
1553
1626
|
async function main() {
|
|
1554
1627
|
try {
|
|
1555
1628
|
cli.parse(process.argv, { run: false });
|
package/dist/index.cjs
CHANGED
|
@@ -1185,33 +1185,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
1185
1185
|
const shown = routes.slice(0, limit).join(", ");
|
|
1186
1186
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
1187
1187
|
}
|
|
1188
|
-
|
|
1188
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
1189
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
1190
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
1191
|
+
function wrapText(text2, width) {
|
|
1192
|
+
const lines = [];
|
|
1193
|
+
let line = "";
|
|
1194
|
+
const flush = () => {
|
|
1195
|
+
if (line) lines.push(line);
|
|
1196
|
+
line = "";
|
|
1197
|
+
};
|
|
1198
|
+
for (const word of text2.split(/\s+/)) {
|
|
1199
|
+
if (word.length > width) {
|
|
1200
|
+
flush();
|
|
1201
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1205
|
+
flush();
|
|
1206
|
+
line = word;
|
|
1207
|
+
} else {
|
|
1208
|
+
line = line ? `${line} ${word}` : word;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
flush();
|
|
1212
|
+
return lines;
|
|
1213
|
+
}
|
|
1214
|
+
function sectionRule(severity, count, width) {
|
|
1215
|
+
const label = PLURAL[severity].toUpperCase();
|
|
1216
|
+
const tail = String(count);
|
|
1217
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
1218
|
+
return `${BADGE[severity](import_picocolors.default.bold(label))} ${import_picocolors.default.dim("\u2500".repeat(dashes))} ${import_picocolors.default.dim(tail)}`;
|
|
1219
|
+
}
|
|
1220
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
1221
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
1222
|
+
const indent = " ";
|
|
1223
|
+
const body = width - indent.length;
|
|
1189
1224
|
const lines = [
|
|
1190
|
-
import_picocolors.default.bold(meta.target
|
|
1191
|
-
import_picocolors.default.dim(
|
|
1225
|
+
`${import_picocolors.default.bold("pagetrace")} ${import_picocolors.default.dim("\xB7")} ${meta.target}`,
|
|
1226
|
+
import_picocolors.default.dim(
|
|
1227
|
+
[
|
|
1228
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
1229
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
1230
|
+
meta.generatedAt
|
|
1231
|
+
].filter(Boolean).join(" \xB7 ")
|
|
1232
|
+
),
|
|
1192
1233
|
""
|
|
1193
1234
|
];
|
|
1194
1235
|
if (groups.length === 0) {
|
|
1195
|
-
lines.push(import_picocolors.default.green(
|
|
1236
|
+
lines.push(import_picocolors.default.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
1196
1237
|
return lines.join("\n");
|
|
1197
1238
|
}
|
|
1198
|
-
for (const
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1239
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
1240
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
1241
|
+
if (inSeverity.length === 0) continue;
|
|
1242
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
1243
|
+
for (const group of inSeverity) {
|
|
1244
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1245
|
+
const headline = group.message;
|
|
1246
|
+
const room = width - 2 - scope.length - 1;
|
|
1247
|
+
if (headline.length <= room) {
|
|
1248
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1249
|
+
lines.push(
|
|
1250
|
+
`${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(headline)}${pad}${import_picocolors.default.dim(scope)}`
|
|
1251
|
+
);
|
|
1252
|
+
} else {
|
|
1253
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1254
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${import_picocolors.default.bold(line)}` : `${indent}${import_picocolors.default.bold(line)}`);
|
|
1255
|
+
}
|
|
1256
|
+
lines.push(`${indent}${import_picocolors.default.dim(scope)}`);
|
|
1257
|
+
}
|
|
1258
|
+
if (group.detail) {
|
|
1259
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (group.fix) {
|
|
1262
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1263
|
+
lines.push(`${indent}${import_picocolors.default.cyan("\u2192")} ${first}`);
|
|
1264
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (group.routes.length > 0) {
|
|
1267
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1268
|
+
lines.push(`${indent}${import_picocolors.default.dim(line)}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
lines.push("");
|
|
1272
|
+
}
|
|
1205
1273
|
}
|
|
1206
1274
|
const { issues, instances, total } = countIssues(groups);
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
);
|
|
1275
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(import_picocolors.default.dim(" \xB7 "));
|
|
1276
|
+
lines.push(import_picocolors.default.dim("\u2500".repeat(width)));
|
|
1277
|
+
lines.push(`${import_picocolors.default.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
1210
1278
|
lines.push(
|
|
1211
1279
|
import_picocolors.default.dim(
|
|
1212
|
-
|
|
1280
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
1213
1281
|
)
|
|
1214
1282
|
);
|
|
1283
|
+
lines.push("");
|
|
1215
1284
|
return lines.join("\n");
|
|
1216
1285
|
}
|
|
1217
1286
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1421,7 +1490,11 @@ async function snapshotFromOrigin(origin, options = {}) {
|
|
|
1421
1490
|
const base = new URL(origin);
|
|
1422
1491
|
const agents = options.aiAgents ?? DEFAULT_AI_AGENTS;
|
|
1423
1492
|
const timeout = options.timeout;
|
|
1424
|
-
const site = {
|
|
1493
|
+
const site = {
|
|
1494
|
+
origin: options.siteUrl ? new URL(options.siteUrl).origin : base.origin,
|
|
1495
|
+
robotsTxt: null,
|
|
1496
|
+
llmsTxt: null
|
|
1497
|
+
};
|
|
1425
1498
|
const limit = options.limit ?? 200;
|
|
1426
1499
|
const robots = await fetchText(new URL("/robots.txt", base).href, timeout);
|
|
1427
1500
|
if (robots !== null) site.robotsTxt = extractRobotsTxt(robots, agents);
|
package/dist/index.d.cts
CHANGED
|
@@ -94,8 +94,10 @@ interface Config {
|
|
|
94
94
|
/** Minimum word count before a page is flagged as thin. */
|
|
95
95
|
minWordCount?: number;
|
|
96
96
|
/**
|
|
97
|
-
* The site's own origin, e.g. "https://example.com".
|
|
98
|
-
*
|
|
97
|
+
* The site's own origin, e.g. "https://example.com". Used to detect canonicals
|
|
98
|
+
* pointing at another host. A --dir crawl has no other source for it, and an
|
|
99
|
+
* origin crawl of a local build or preview deployment needs it to override the
|
|
100
|
+
* URL being crawled, since those serve production canonicals.
|
|
99
101
|
*/
|
|
100
102
|
siteUrl?: string;
|
|
101
103
|
}
|
|
@@ -192,7 +194,13 @@ interface AuditMeta {
|
|
|
192
194
|
pageCount: number;
|
|
193
195
|
generatedAt: string;
|
|
194
196
|
}
|
|
195
|
-
|
|
197
|
+
/**
|
|
198
|
+
* The terminal report. Findings are grouped under a rule per severity, prose is
|
|
199
|
+
* wrapped to the terminal rather than running off it, and the affected routes
|
|
200
|
+
* sit on their own line — they are the part you act on, and they used to blend
|
|
201
|
+
* into the surrounding text.
|
|
202
|
+
*/
|
|
203
|
+
declare function formatAuditPretty(groups: Aggregate[], meta: AuditMeta, columns?: number): string;
|
|
196
204
|
declare function formatAuditMarkdown(groups: Aggregate[], meta: AuditMeta): string;
|
|
197
205
|
/** Self-contained HTML report, suitable for handing to a client. */
|
|
198
206
|
declare function formatAuditHtml(groups: Aggregate[], meta: AuditMeta): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -94,8 +94,10 @@ interface Config {
|
|
|
94
94
|
/** Minimum word count before a page is flagged as thin. */
|
|
95
95
|
minWordCount?: number;
|
|
96
96
|
/**
|
|
97
|
-
* The site's own origin, e.g. "https://example.com".
|
|
98
|
-
*
|
|
97
|
+
* The site's own origin, e.g. "https://example.com". Used to detect canonicals
|
|
98
|
+
* pointing at another host. A --dir crawl has no other source for it, and an
|
|
99
|
+
* origin crawl of a local build or preview deployment needs it to override the
|
|
100
|
+
* URL being crawled, since those serve production canonicals.
|
|
99
101
|
*/
|
|
100
102
|
siteUrl?: string;
|
|
101
103
|
}
|
|
@@ -192,7 +194,13 @@ interface AuditMeta {
|
|
|
192
194
|
pageCount: number;
|
|
193
195
|
generatedAt: string;
|
|
194
196
|
}
|
|
195
|
-
|
|
197
|
+
/**
|
|
198
|
+
* The terminal report. Findings are grouped under a rule per severity, prose is
|
|
199
|
+
* wrapped to the terminal rather than running off it, and the affected routes
|
|
200
|
+
* sit on their own line — they are the part you act on, and they used to blend
|
|
201
|
+
* into the surrounding text.
|
|
202
|
+
*/
|
|
203
|
+
declare function formatAuditPretty(groups: Aggregate[], meta: AuditMeta, columns?: number): string;
|
|
196
204
|
declare function formatAuditMarkdown(groups: Aggregate[], meta: AuditMeta): string;
|
|
197
205
|
/** Self-contained HTML report, suitable for handing to a client. */
|
|
198
206
|
declare function formatAuditHtml(groups: Aggregate[], meta: AuditMeta): string;
|
package/dist/index.js
CHANGED
|
@@ -1114,33 +1114,102 @@ function sampleRoutes(routes, limit = 5) {
|
|
|
1114
1114
|
const shown = routes.slice(0, limit).join(", ");
|
|
1115
1115
|
return routes.length > limit ? `${shown} +${routes.length - limit} more` : shown;
|
|
1116
1116
|
}
|
|
1117
|
-
|
|
1117
|
+
var GLYPH = { error: "\u2717", warn: "\u25B2", info: "\xB7" };
|
|
1118
|
+
var PLURAL = { error: "errors", warn: "warnings", info: "notes" };
|
|
1119
|
+
var SINGULAR = { error: "error", warn: "warning", info: "note" };
|
|
1120
|
+
function wrapText(text2, width) {
|
|
1121
|
+
const lines = [];
|
|
1122
|
+
let line = "";
|
|
1123
|
+
const flush = () => {
|
|
1124
|
+
if (line) lines.push(line);
|
|
1125
|
+
line = "";
|
|
1126
|
+
};
|
|
1127
|
+
for (const word of text2.split(/\s+/)) {
|
|
1128
|
+
if (word.length > width) {
|
|
1129
|
+
flush();
|
|
1130
|
+
for (let i = 0; i < word.length; i += width) lines.push(word.slice(i, i + width));
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (line && line.length + 1 + word.length > width) {
|
|
1134
|
+
flush();
|
|
1135
|
+
line = word;
|
|
1136
|
+
} else {
|
|
1137
|
+
line = line ? `${line} ${word}` : word;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
flush();
|
|
1141
|
+
return lines;
|
|
1142
|
+
}
|
|
1143
|
+
function sectionRule(severity, count, width) {
|
|
1144
|
+
const label = PLURAL[severity].toUpperCase();
|
|
1145
|
+
const tail = String(count);
|
|
1146
|
+
const dashes = Math.max(3, width - label.length - tail.length - 2);
|
|
1147
|
+
return `${BADGE[severity](pc.bold(label))} ${pc.dim("\u2500".repeat(dashes))} ${pc.dim(tail)}`;
|
|
1148
|
+
}
|
|
1149
|
+
function formatAuditPretty(groups, meta, columns = 80) {
|
|
1150
|
+
const width = Math.min(Math.max(columns, 48), 96);
|
|
1151
|
+
const indent = " ";
|
|
1152
|
+
const body = width - indent.length;
|
|
1118
1153
|
const lines = [
|
|
1119
|
-
pc.bold(meta.target
|
|
1120
|
-
pc.dim(
|
|
1154
|
+
`${pc.bold("pagetrace")} ${pc.dim("\xB7")} ${meta.target}`,
|
|
1155
|
+
pc.dim(
|
|
1156
|
+
[
|
|
1157
|
+
`${meta.pageCount} page${meta.pageCount === 1 ? "" : "s"}`,
|
|
1158
|
+
meta.platform === "unknown" ? null : PLATFORM_LABEL[meta.platform],
|
|
1159
|
+
meta.generatedAt
|
|
1160
|
+
].filter(Boolean).join(" \xB7 ")
|
|
1161
|
+
),
|
|
1121
1162
|
""
|
|
1122
1163
|
];
|
|
1123
1164
|
if (groups.length === 0) {
|
|
1124
|
-
lines.push(pc.green(
|
|
1165
|
+
lines.push(pc.green(`\u2713 No issues found across ${meta.pageCount} pages.`), "");
|
|
1125
1166
|
return lines.join("\n");
|
|
1126
1167
|
}
|
|
1127
|
-
for (const
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1168
|
+
for (const severity of ["error", "warn", "info"]) {
|
|
1169
|
+
const inSeverity = groups.filter((g) => g.severity === severity);
|
|
1170
|
+
if (inSeverity.length === 0) continue;
|
|
1171
|
+
lines.push(sectionRule(severity, inSeverity.length, width), "");
|
|
1172
|
+
for (const group of inSeverity) {
|
|
1173
|
+
const scope = isTemplateWide(group, meta.pageCount) ? `${group.count} pages \xB7 one template fix` : `${group.count} page${group.count === 1 ? "" : "s"}`;
|
|
1174
|
+
const headline = group.message;
|
|
1175
|
+
const room = width - 2 - scope.length - 1;
|
|
1176
|
+
if (headline.length <= room) {
|
|
1177
|
+
const pad = " ".repeat(Math.max(1, room - headline.length + 1));
|
|
1178
|
+
lines.push(
|
|
1179
|
+
`${BADGE[severity](GLYPH[severity])} ${pc.bold(headline)}${pad}${pc.dim(scope)}`
|
|
1180
|
+
);
|
|
1181
|
+
} else {
|
|
1182
|
+
for (const [i, line] of wrapText(headline, width - 2).entries()) {
|
|
1183
|
+
lines.push(i === 0 ? `${BADGE[severity](GLYPH[severity])} ${pc.bold(line)}` : `${indent}${pc.bold(line)}`);
|
|
1184
|
+
}
|
|
1185
|
+
lines.push(`${indent}${pc.dim(scope)}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (group.detail) {
|
|
1188
|
+
for (const line of wrapText(group.detail, body)) lines.push(`${indent}${pc.dim(line)}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (group.fix) {
|
|
1191
|
+
const [first, ...rest] = wrapText(group.fix, body - 2);
|
|
1192
|
+
lines.push(`${indent}${pc.cyan("\u2192")} ${first}`);
|
|
1193
|
+
for (const line of rest) lines.push(`${indent} ${line}`);
|
|
1194
|
+
}
|
|
1195
|
+
if (group.routes.length > 0) {
|
|
1196
|
+
for (const line of wrapText(sampleRoutes(group.routes), body)) {
|
|
1197
|
+
lines.push(`${indent}${pc.dim(line)}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
lines.push("");
|
|
1201
|
+
}
|
|
1134
1202
|
}
|
|
1135
1203
|
const { issues, instances, total } = countIssues(groups);
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
);
|
|
1204
|
+
const counted = ["error", "warn", "info"].filter((s) => issues[s] > 0).map((s) => BADGE[s](`${issues[s]} ${issues[s] === 1 ? SINGULAR[s] : PLURAL[s]}`)).join(pc.dim(" \xB7 "));
|
|
1205
|
+
lines.push(pc.dim("\u2500".repeat(width)));
|
|
1206
|
+
lines.push(`${pc.bold(`${total} issue${total === 1 ? "" : "s"}`)} ${counted}`);
|
|
1139
1207
|
lines.push(
|
|
1140
1208
|
pc.dim(
|
|
1141
|
-
|
|
1209
|
+
`${instances.error + instances.warn + instances.info} findings across ${meta.pageCount} pages`
|
|
1142
1210
|
)
|
|
1143
1211
|
);
|
|
1212
|
+
lines.push("");
|
|
1144
1213
|
return lines.join("\n");
|
|
1145
1214
|
}
|
|
1146
1215
|
function formatAuditMarkdown(groups, meta) {
|
|
@@ -1350,7 +1419,11 @@ async function snapshotFromOrigin(origin, options = {}) {
|
|
|
1350
1419
|
const base = new URL(origin);
|
|
1351
1420
|
const agents = options.aiAgents ?? DEFAULT_AI_AGENTS;
|
|
1352
1421
|
const timeout = options.timeout;
|
|
1353
|
-
const site = {
|
|
1422
|
+
const site = {
|
|
1423
|
+
origin: options.siteUrl ? new URL(options.siteUrl).origin : base.origin,
|
|
1424
|
+
robotsTxt: null,
|
|
1425
|
+
llmsTxt: null
|
|
1426
|
+
};
|
|
1354
1427
|
const limit = options.limit ?? 200;
|
|
1355
1428
|
const robots = await fetchText(new URL("/robots.txt", base).href, timeout);
|
|
1356
1429
|
if (robots !== null) site.robotsTxt = extractRobotsTxt(robots, agents);
|