pagetrace 0.5.0 → 0.6.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,21 @@ 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.6.0] - 2026-09-06
10
+
11
+ ### Changed
12
+
13
+ - **Breaking.** `duplicate.title`, `duplicate.description` and
14
+ `duplicate.canonical` now emit one finding per affected route instead of a
15
+ single finding with `route: null`. Integrations reading `route` on these three
16
+ codes will see a path where they saw null; the codes themselves are unchanged.
17
+
18
+ Found by auditing a real site. The report said "2 pages share the same meta
19
+ description" and could not say which two, because the routes lived in `after`,
20
+ which the rollup drops — leaving the only actionable part of the finding
21
+ invisible. Per-route findings also make the counts truthful and point
22
+ `--format github` annotations at the pages rather than at "site".
23
+
9
24
  ## [0.5.0] - 2026-09-06
10
25
 
11
26
  ### Added
@@ -20,8 +35,15 @@ While the version is below 1.0.0, breaking changes ship in a minor release.
20
35
  - A composite GitHub Action (`action.yml`). Three lines in a workflow run the
21
36
  check and post the findings as a pull request comment, editing the previous
22
37
  comment on each push rather than stacking new ones. It fetches the baseline ref
23
- first, since a shallow CI checkout usually has only the PR head.
38
+ first, since a shallow CI checkout usually has only the PR head. The comment is
39
+ skipped for pull requests from a fork, which run with a read-only token and
40
+ would otherwise fail with a 403 through no fault of the contributor; the
41
+ findings still reach the step summary and still set the exit code.
24
42
  - `snapshotFromGitRef(ref, path)` is exported.
43
+ - `examples/site`, a small deliberately correct site with a committed baseline.
44
+ The action runs against it on every pull request to this repo, so a change
45
+ that breaks the action's own wiring fails here rather than in someone else's
46
+ CI. It doubles as a worked example of what a clean surface looks like.
25
47
 
26
48
  ### Fixed
27
49
 
@@ -194,6 +216,7 @@ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
194
216
  crawling; diff classified by transition; absolute, cross-page and hreflang audit
195
217
  rules; pretty, JSON, markdown, GitHub and HTML reporters.
196
218
 
219
+ [0.6.0]: https://github.com/shyamexe/pagetrace/compare/v0.5.0...v0.6.0
197
220
  [0.5.0]: https://github.com/shyamexe/pagetrace/compare/v0.4.0...v0.5.0
198
221
  [0.4.0]: https://github.com/shyamexe/pagetrace/compare/v0.3.0...v0.4.0
199
222
  [0.3.0]: https://github.com/shyamexe/pagetrace/compare/v0.2.0...v0.3.0
package/dist/cli.cjs CHANGED
@@ -266,39 +266,38 @@ function auditCrossPage(snapshot) {
266
266
  }
267
267
  return map;
268
268
  };
269
- for (const [title, routes] of group((p) => p.title)) {
270
- if (routes.length > 1) {
271
- findings.push({
272
- code: "duplicate.title",
273
- severity: "warn",
274
- route: null,
275
- message: `${routes.length} pages share the title "${title}".`,
276
- after: routes
277
- });
278
- }
279
- }
280
- for (const [, routes] of group((p) => p.description)) {
281
- if (routes.length > 1) {
282
- findings.push({
283
- code: "duplicate.description",
284
- severity: "warn",
285
- route: null,
286
- message: `${routes.length} pages share the same meta description.`,
287
- after: routes
288
- });
289
- }
290
- }
291
- for (const [canonical, routes] of group((p) => p.canonical)) {
292
- if (routes.length > 1) {
293
- findings.push({
294
- code: "duplicate.canonical",
295
- severity: "error",
296
- route: null,
297
- message: `${routes.length} pages canonicalise to ${canonical}.`,
298
- after: routes
299
- });
269
+ const duplicates = (code, severity, keyed, message) => {
270
+ for (const [value, routes] of keyed) {
271
+ if (routes.length < 2) continue;
272
+ for (const route of routes) {
273
+ findings.push({
274
+ code,
275
+ severity,
276
+ route,
277
+ message: message(value, routes.length),
278
+ after: routes
279
+ });
280
+ }
300
281
  }
301
- }
282
+ };
283
+ duplicates(
284
+ "duplicate.title",
285
+ "warn",
286
+ group((p) => p.title),
287
+ (title, n) => `${n} pages share the title "${title}".`
288
+ );
289
+ duplicates(
290
+ "duplicate.description",
291
+ "warn",
292
+ group((p) => p.description),
293
+ (_, n) => `${n} pages share the same meta description.`
294
+ );
295
+ duplicates(
296
+ "duplicate.canonical",
297
+ "error",
298
+ group((p) => p.canonical),
299
+ (canonical, n) => `${n} pages canonicalise to ${canonical}.`
300
+ );
302
301
  const expectedOrigin = snapshot.site.origin ?? null;
303
302
  for (const page of pages) {
304
303
  if (!page.canonical) continue;
@@ -1573,7 +1572,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
1573
1572
  }
1574
1573
  });
1575
1574
  cli.help();
1576
- cli.version("0.5.0");
1575
+ cli.version("0.6.0");
1577
1576
  async function main() {
1578
1577
  try {
1579
1578
  cli.parse(process.argv, { run: false });
package/dist/cli.js CHANGED
@@ -243,39 +243,38 @@ function auditCrossPage(snapshot) {
243
243
  }
244
244
  return map;
245
245
  };
246
- for (const [title, routes] of group((p) => p.title)) {
247
- if (routes.length > 1) {
248
- findings.push({
249
- code: "duplicate.title",
250
- severity: "warn",
251
- route: null,
252
- message: `${routes.length} pages share the title "${title}".`,
253
- after: routes
254
- });
255
- }
256
- }
257
- for (const [, routes] of group((p) => p.description)) {
258
- if (routes.length > 1) {
259
- findings.push({
260
- code: "duplicate.description",
261
- severity: "warn",
262
- route: null,
263
- message: `${routes.length} pages share the same meta description.`,
264
- after: routes
265
- });
266
- }
267
- }
268
- for (const [canonical, routes] of group((p) => p.canonical)) {
269
- if (routes.length > 1) {
270
- findings.push({
271
- code: "duplicate.canonical",
272
- severity: "error",
273
- route: null,
274
- message: `${routes.length} pages canonicalise to ${canonical}.`,
275
- after: routes
276
- });
246
+ const duplicates = (code, severity, keyed, message) => {
247
+ for (const [value, routes] of keyed) {
248
+ if (routes.length < 2) continue;
249
+ for (const route of routes) {
250
+ findings.push({
251
+ code,
252
+ severity,
253
+ route,
254
+ message: message(value, routes.length),
255
+ after: routes
256
+ });
257
+ }
277
258
  }
278
- }
259
+ };
260
+ duplicates(
261
+ "duplicate.title",
262
+ "warn",
263
+ group((p) => p.title),
264
+ (title, n) => `${n} pages share the title "${title}".`
265
+ );
266
+ duplicates(
267
+ "duplicate.description",
268
+ "warn",
269
+ group((p) => p.description),
270
+ (_, n) => `${n} pages share the same meta description.`
271
+ );
272
+ duplicates(
273
+ "duplicate.canonical",
274
+ "error",
275
+ group((p) => p.canonical),
276
+ (canonical, n) => `${n} pages canonicalise to ${canonical}.`
277
+ );
279
278
  const expectedOrigin = snapshot.site.origin ?? null;
280
279
  for (const page of pages) {
281
280
  if (!page.canonical) continue;
@@ -1550,7 +1549,7 @@ cli.command("audit", "Audit a site as it stands, with explanations and fixes").o
1550
1549
  }
1551
1550
  });
1552
1551
  cli.help();
1553
- cli.version("0.5.0");
1552
+ cli.version("0.6.0");
1554
1553
  async function main() {
1555
1554
  try {
1556
1555
  cli.parse(process.argv, { run: false });
package/dist/index.cjs CHANGED
@@ -307,39 +307,38 @@ function auditCrossPage(snapshot) {
307
307
  }
308
308
  return map;
309
309
  };
310
- for (const [title, routes] of group((p) => p.title)) {
311
- if (routes.length > 1) {
312
- findings.push({
313
- code: "duplicate.title",
314
- severity: "warn",
315
- route: null,
316
- message: `${routes.length} pages share the title "${title}".`,
317
- after: routes
318
- });
319
- }
320
- }
321
- for (const [, routes] of group((p) => p.description)) {
322
- if (routes.length > 1) {
323
- findings.push({
324
- code: "duplicate.description",
325
- severity: "warn",
326
- route: null,
327
- message: `${routes.length} pages share the same meta description.`,
328
- after: routes
329
- });
330
- }
331
- }
332
- for (const [canonical, routes] of group((p) => p.canonical)) {
333
- if (routes.length > 1) {
334
- findings.push({
335
- code: "duplicate.canonical",
336
- severity: "error",
337
- route: null,
338
- message: `${routes.length} pages canonicalise to ${canonical}.`,
339
- after: routes
340
- });
310
+ const duplicates = (code, severity, keyed, message) => {
311
+ for (const [value, routes] of keyed) {
312
+ if (routes.length < 2) continue;
313
+ for (const route of routes) {
314
+ findings.push({
315
+ code,
316
+ severity,
317
+ route,
318
+ message: message(value, routes.length),
319
+ after: routes
320
+ });
321
+ }
341
322
  }
342
- }
323
+ };
324
+ duplicates(
325
+ "duplicate.title",
326
+ "warn",
327
+ group((p) => p.title),
328
+ (title, n) => `${n} pages share the title "${title}".`
329
+ );
330
+ duplicates(
331
+ "duplicate.description",
332
+ "warn",
333
+ group((p) => p.description),
334
+ (_, n) => `${n} pages share the same meta description.`
335
+ );
336
+ duplicates(
337
+ "duplicate.canonical",
338
+ "error",
339
+ group((p) => p.canonical),
340
+ (canonical, n) => `${n} pages canonicalise to ${canonical}.`
341
+ );
343
342
  const expectedOrigin = snapshot.site.origin ?? null;
344
343
  for (const page of pages) {
345
344
  if (!page.canonical) continue;
package/dist/index.js CHANGED
@@ -236,39 +236,38 @@ function auditCrossPage(snapshot) {
236
236
  }
237
237
  return map;
238
238
  };
239
- for (const [title, routes] of group((p) => p.title)) {
240
- if (routes.length > 1) {
241
- findings.push({
242
- code: "duplicate.title",
243
- severity: "warn",
244
- route: null,
245
- message: `${routes.length} pages share the title "${title}".`,
246
- after: routes
247
- });
248
- }
249
- }
250
- for (const [, routes] of group((p) => p.description)) {
251
- if (routes.length > 1) {
252
- findings.push({
253
- code: "duplicate.description",
254
- severity: "warn",
255
- route: null,
256
- message: `${routes.length} pages share the same meta description.`,
257
- after: routes
258
- });
259
- }
260
- }
261
- for (const [canonical, routes] of group((p) => p.canonical)) {
262
- if (routes.length > 1) {
263
- findings.push({
264
- code: "duplicate.canonical",
265
- severity: "error",
266
- route: null,
267
- message: `${routes.length} pages canonicalise to ${canonical}.`,
268
- after: routes
269
- });
239
+ const duplicates = (code, severity, keyed, message) => {
240
+ for (const [value, routes] of keyed) {
241
+ if (routes.length < 2) continue;
242
+ for (const route of routes) {
243
+ findings.push({
244
+ code,
245
+ severity,
246
+ route,
247
+ message: message(value, routes.length),
248
+ after: routes
249
+ });
250
+ }
270
251
  }
271
- }
252
+ };
253
+ duplicates(
254
+ "duplicate.title",
255
+ "warn",
256
+ group((p) => p.title),
257
+ (title, n) => `${n} pages share the title "${title}".`
258
+ );
259
+ duplicates(
260
+ "duplicate.description",
261
+ "warn",
262
+ group((p) => p.description),
263
+ (_, n) => `${n} pages share the same meta description.`
264
+ );
265
+ duplicates(
266
+ "duplicate.canonical",
267
+ "error",
268
+ group((p) => p.canonical),
269
+ (canonical, n) => `${n} pages canonicalise to ${canonical}.`
270
+ );
272
271
  const expectedOrigin = snapshot.site.origin ?? null;
273
272
  for (const page of pages) {
274
273
  if (!page.canonical) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagetrace",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": {