pagetrace 0.10.0 → 0.11.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 +9 -0
- package/README.md +9 -0
- package/dist/cli.cjs +51 -4
- package/dist/cli.js +51 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ 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.11.0] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pagetrace links`, a command for the one question. It crawls once and reports
|
|
14
|
+
only broken internal links, exiting 1 if there are any. The rules are the ones
|
|
15
|
+
`audit` runs, filtered rather than reimplemented, so the two cannot disagree.
|
|
16
|
+
|
|
9
17
|
## [0.10.0] - 2026-09-08
|
|
10
18
|
|
|
11
19
|
### Added
|
|
@@ -330,6 +338,7 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
|
|
|
330
338
|
crawling; diff classified by transition; absolute, cross-page and hreflang audit
|
|
331
339
|
rules; pretty, JSON, markdown, GitHub and HTML reporters.
|
|
332
340
|
|
|
341
|
+
[0.11.0]: https://github.com/shyamexe/pagetrace/compare/v0.10.0...v0.11.0
|
|
333
342
|
[0.10.0]: https://github.com/shyamexe/pagetrace/compare/v0.9.1...v0.10.0
|
|
334
343
|
[0.9.1]: https://github.com/shyamexe/pagetrace/compare/v0.9.0...v0.9.1
|
|
335
344
|
[0.9.0]: https://github.com/shyamexe/pagetrace/compare/v0.8.1...v0.9.0
|
package/README.md
CHANGED
|
@@ -165,6 +165,15 @@ pagetrace update # install it globally
|
|
|
165
165
|
| `og.removed` / `hreflang.removed` | warn | Social or i18n tags dropped |
|
|
166
166
|
| `title.changed` | info | Ordinary copy edit |
|
|
167
167
|
|
|
168
|
+
Broken links have a command of their own, when that is the only question you have:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx pagetrace links --url https://example.com
|
|
172
|
+
npx pagetrace links --dir ./out --format json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
It crawls once, runs the same rules as `audit`, and prints only the link findings — exit 1 if any, or `No broken links found — 42 pages checked.`
|
|
176
|
+
|
|
168
177
|
Internal links are checked too. Only the broken ones are stored, so a site's navigation never lands in the lockfile: `link.broken` for a link that is already dead, `link.broken.added` for one this build broke. A `--dir` crawl is authoritative — the build directory is the whole site — while a crawl confirms each candidate with a real request first, because a sitemap routinely omits pages that are live. External links are deliberately not checked: a Cloudflare 403 and a rate limit both look like a dead page, and that is where link checkers earn their reputation for false positives.
|
|
169
178
|
|
|
170
179
|
Redirects are recorded from the response itself, so they cost no extra requests. A redirect that only adds or drops a trailing slash is server configuration rather than drift and is not reported. `canonical.redirects` is only raised when the canonical's target was actually crawled, so a `--limit` run cannot invent it.
|
package/dist/cli.cjs
CHANGED
|
@@ -1958,6 +1958,53 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1958
1958
|
process.exitCode = EXIT_FINDINGS;
|
|
1959
1959
|
}
|
|
1960
1960
|
});
|
|
1961
|
+
cli.command("links", "Find internal links that point at no page").option("--url <origin>", "Live origin to crawl").option("--dir <dir>", "Directory of built HTML").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--ignore-robots", "Crawl paths that robots.txt disallows").option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown", { default: "pretty" }).option("--out <file>", "Write the report to a file instead of stdout").option("--fail-on <severity>", "error | warn | info | never", { default: "error" }).action(async (flags) => {
|
|
1962
|
+
const failOn = parseFailOn(flags.failOn, true);
|
|
1963
|
+
const config = await loadConfig(flags.config);
|
|
1964
|
+
const snapshot = await build(flags, config);
|
|
1965
|
+
const pages = Object.values(snapshot.pages);
|
|
1966
|
+
if (pages.length === 0) {
|
|
1967
|
+
console.error(
|
|
1968
|
+
import_picocolors2.default.yellow(
|
|
1969
|
+
"No pages found. Check that the sitemap is reachable, or pass --dir with pre-rendered HTML."
|
|
1970
|
+
)
|
|
1971
|
+
);
|
|
1972
|
+
process.exitCode = EXIT_FAILURE;
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const platform = detectPlatform(
|
|
1976
|
+
pages.map((p) => p.generator),
|
|
1977
|
+
pages.flatMap((p) => Object.values(p.og))
|
|
1978
|
+
);
|
|
1979
|
+
const findings = applyConfig(auditSnapshot(snapshot, config), config).filter((f) => f.code.startsWith("link.")).map((f) => withGuidance(f, platform));
|
|
1980
|
+
const target = flags.url ?? flags.dir ?? "site";
|
|
1981
|
+
if (findings.length === 0 && !flags.out) {
|
|
1982
|
+
console.log(
|
|
1983
|
+
import_picocolors2.default.green(
|
|
1984
|
+
`No broken links found \u2014 ${pages.length} page${pages.length === 1 ? "" : "s"} checked.`
|
|
1985
|
+
)
|
|
1986
|
+
);
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
const groups = aggregate(findings);
|
|
1990
|
+
const meta = {
|
|
1991
|
+
target,
|
|
1992
|
+
platform,
|
|
1993
|
+
pageCount: pages.length,
|
|
1994
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1995
|
+
};
|
|
1996
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1997
|
+
if (flags.out) {
|
|
1998
|
+
await (0, import_promises2.writeFile)(flags.out, `${output}
|
|
1999
|
+
`, "utf8");
|
|
2000
|
+
console.log(import_picocolors2.default.green(`Wrote ${flags.out} \u2014 ${groups.length} broken link${groups.length === 1 ? "" : "s"}.`));
|
|
2001
|
+
} else {
|
|
2002
|
+
console.log(output);
|
|
2003
|
+
}
|
|
2004
|
+
if (failOn !== "never" && shouldFail(findings, failOn)) {
|
|
2005
|
+
process.exitCode = EXIT_FINDINGS;
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
1961
2008
|
cli.command("init", "Write a config file and take the first snapshot").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--ignore-robots", "Crawl paths that robots.txt disallows").option("--out <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).action(async (flags) => {
|
|
1962
2009
|
const existing = await (0, import_promises2.readFile)(flags.config, "utf8").catch(() => null);
|
|
1963
2010
|
if (existing === null) {
|
|
@@ -1981,11 +2028,11 @@ Commit both files, then run \`pagetrace check ${flags.dir ? `--dir ${flags.dir}`
|
|
|
1981
2028
|
});
|
|
1982
2029
|
cli.command("update", "Check npm for a newer pagetrace and install it").option("--check", "Only report whether an update exists").action(async (flags) => {
|
|
1983
2030
|
const latest = await latestVersion("pagetrace");
|
|
1984
|
-
if (!isNewer(latest, "0.
|
|
1985
|
-
console.log(import_picocolors2.default.green(`pagetrace ${"0.
|
|
2031
|
+
if (!isNewer(latest, "0.11.0")) {
|
|
2032
|
+
console.log(import_picocolors2.default.green(`pagetrace ${"0.11.0"} is the latest version.`));
|
|
1986
2033
|
return;
|
|
1987
2034
|
}
|
|
1988
|
-
console.log(import_picocolors2.default.yellow(`Update available: ${"0.
|
|
2035
|
+
console.log(import_picocolors2.default.yellow(`Update available: ${"0.11.0"} \u2192 ${latest}`));
|
|
1989
2036
|
if (flags.check) return;
|
|
1990
2037
|
if (process.argv[1]?.startsWith(process.cwd())) {
|
|
1991
2038
|
console.log(
|
|
@@ -2003,7 +2050,7 @@ cli.command("update", "Check npm for a newer pagetrace and install it").option("
|
|
|
2003
2050
|
console.log(import_picocolors2.default.green(`Updated to pagetrace ${latest}.`));
|
|
2004
2051
|
});
|
|
2005
2052
|
cli.help();
|
|
2006
|
-
cli.version("0.
|
|
2053
|
+
cli.version("0.11.0");
|
|
2007
2054
|
async function main() {
|
|
2008
2055
|
try {
|
|
2009
2056
|
cli.parse(process.argv, { run: false });
|
package/dist/cli.js
CHANGED
|
@@ -1935,6 +1935,53 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
|
|
|
1935
1935
|
process.exitCode = EXIT_FINDINGS;
|
|
1936
1936
|
}
|
|
1937
1937
|
});
|
|
1938
|
+
cli.command("links", "Find internal links that point at no page").option("--url <origin>", "Live origin to crawl").option("--dir <dir>", "Directory of built HTML").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--ignore-robots", "Crawl paths that robots.txt disallows").option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).option("--format <format>", "pretty | json | markdown", { default: "pretty" }).option("--out <file>", "Write the report to a file instead of stdout").option("--fail-on <severity>", "error | warn | info | never", { default: "error" }).action(async (flags) => {
|
|
1939
|
+
const failOn = parseFailOn(flags.failOn, true);
|
|
1940
|
+
const config = await loadConfig(flags.config);
|
|
1941
|
+
const snapshot = await build(flags, config);
|
|
1942
|
+
const pages = Object.values(snapshot.pages);
|
|
1943
|
+
if (pages.length === 0) {
|
|
1944
|
+
console.error(
|
|
1945
|
+
pc2.yellow(
|
|
1946
|
+
"No pages found. Check that the sitemap is reachable, or pass --dir with pre-rendered HTML."
|
|
1947
|
+
)
|
|
1948
|
+
);
|
|
1949
|
+
process.exitCode = EXIT_FAILURE;
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const platform = detectPlatform(
|
|
1953
|
+
pages.map((p) => p.generator),
|
|
1954
|
+
pages.flatMap((p) => Object.values(p.og))
|
|
1955
|
+
);
|
|
1956
|
+
const findings = applyConfig(auditSnapshot(snapshot, config), config).filter((f) => f.code.startsWith("link.")).map((f) => withGuidance(f, platform));
|
|
1957
|
+
const target = flags.url ?? flags.dir ?? "site";
|
|
1958
|
+
if (findings.length === 0 && !flags.out) {
|
|
1959
|
+
console.log(
|
|
1960
|
+
pc2.green(
|
|
1961
|
+
`No broken links found \u2014 ${pages.length} page${pages.length === 1 ? "" : "s"} checked.`
|
|
1962
|
+
)
|
|
1963
|
+
);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const groups = aggregate(findings);
|
|
1967
|
+
const meta = {
|
|
1968
|
+
target,
|
|
1969
|
+
platform,
|
|
1970
|
+
pageCount: pages.length,
|
|
1971
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1972
|
+
};
|
|
1973
|
+
const output = flags.format === "json" ? JSON.stringify({ schemaVersion: 1, meta, summary: summarize(findings), groups }, null, 2) : flags.format === "markdown" ? formatAuditMarkdown(groups, meta) : formatAuditPretty(groups, meta, process.stdout.columns);
|
|
1974
|
+
if (flags.out) {
|
|
1975
|
+
await writeFile(flags.out, `${output}
|
|
1976
|
+
`, "utf8");
|
|
1977
|
+
console.log(pc2.green(`Wrote ${flags.out} \u2014 ${groups.length} broken link${groups.length === 1 ? "" : "s"}.`));
|
|
1978
|
+
} else {
|
|
1979
|
+
console.log(output);
|
|
1980
|
+
}
|
|
1981
|
+
if (failOn !== "never" && shouldFail(findings, failOn)) {
|
|
1982
|
+
process.exitCode = EXIT_FINDINGS;
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1938
1985
|
cli.command("init", "Write a config file and take the first snapshot").option("--dir <dir>", "Directory of built HTML").option("--url <origin>", "Live origin to crawl").option("--limit <n>", "Max pages to crawl", { default: 200 }).option("--concurrency <n>", "Parallel requests", { default: 5 }).option("--ignore-robots", "Crawl paths that robots.txt disallows").option("--out <file>", "Lockfile path", { default: DEFAULT_LOCKFILE }).option("--config <file>", "Config file", { default: DEFAULT_CONFIG }).action(async (flags) => {
|
|
1939
1986
|
const existing = await readFile2(flags.config, "utf8").catch(() => null);
|
|
1940
1987
|
if (existing === null) {
|
|
@@ -1958,11 +2005,11 @@ Commit both files, then run \`pagetrace check ${flags.dir ? `--dir ${flags.dir}`
|
|
|
1958
2005
|
});
|
|
1959
2006
|
cli.command("update", "Check npm for a newer pagetrace and install it").option("--check", "Only report whether an update exists").action(async (flags) => {
|
|
1960
2007
|
const latest = await latestVersion("pagetrace");
|
|
1961
|
-
if (!isNewer(latest, "0.
|
|
1962
|
-
console.log(pc2.green(`pagetrace ${"0.
|
|
2008
|
+
if (!isNewer(latest, "0.11.0")) {
|
|
2009
|
+
console.log(pc2.green(`pagetrace ${"0.11.0"} is the latest version.`));
|
|
1963
2010
|
return;
|
|
1964
2011
|
}
|
|
1965
|
-
console.log(pc2.yellow(`Update available: ${"0.
|
|
2012
|
+
console.log(pc2.yellow(`Update available: ${"0.11.0"} \u2192 ${latest}`));
|
|
1966
2013
|
if (flags.check) return;
|
|
1967
2014
|
if (process.argv[1]?.startsWith(process.cwd())) {
|
|
1968
2015
|
console.log(
|
|
@@ -1980,7 +2027,7 @@ cli.command("update", "Check npm for a newer pagetrace and install it").option("
|
|
|
1980
2027
|
console.log(pc2.green(`Updated to pagetrace ${latest}.`));
|
|
1981
2028
|
});
|
|
1982
2029
|
cli.help();
|
|
1983
|
-
cli.version("0.
|
|
2030
|
+
cli.version("0.11.0");
|
|
1984
2031
|
async function main() {
|
|
1985
2032
|
try {
|
|
1986
2033
|
cli.parse(process.argv, { run: false });
|