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 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.10.0")) {
1985
- console.log(import_picocolors2.default.green(`pagetrace ${"0.10.0"} is the latest version.`));
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.10.0"} \u2192 ${latest}`));
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.10.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.10.0")) {
1962
- console.log(pc2.green(`pagetrace ${"0.10.0"} is the latest version.`));
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.10.0"} \u2192 ${latest}`));
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.10.0");
2030
+ cli.version("0.11.0");
1984
2031
  async function main() {
1985
2032
  try {
1986
2033
  cli.parse(process.argv, { run: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagetrace",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Baseline your site's SEO and AEO surface, diff every build against it, and fail CI on regressions.",
5
5
  "main": "./dist/index.cjs",
6
6
  "scripts": {