pagetrace 0.2.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 ADDED
@@ -0,0 +1,121 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ While the version is below 1.0.0, breaking changes ship in a minor release.
8
+
9
+ ## [0.2.0] - 2026-09-06
10
+
11
+ A correctness pass over the whole surface. No finding codes were renamed, so
12
+ integrations keying on `canonical.removed` and friends are unaffected. Several
13
+ finding *messages* changed, which is deliberate — see Fixed.
14
+
15
+ ### Removed
16
+
17
+ - **Breaking.** The `pgt` bin alias. The CLI is `pagetrace` only. One name is
18
+ easier to remember, to document and to search for than two, and three-letter
19
+ bins collide freely across packages.
20
+
21
+ ### Added
22
+
23
+ - `canonical.offsite` (error): a canonical pointing at a host other than the
24
+ site's own. A staging or CDN hostname leaking into canonicals removes the live
25
+ site from results, and `canonical.crosspath` could not see it — that rule
26
+ compares paths only, so `/services` canonicalising to
27
+ `https://staging.example.net/services` looked correct. Checked only when the
28
+ site's own origin is known: an origin crawl records it, and a `--dir` crawl
29
+ takes it from the new `siteUrl` config option. Inferring it from the canonicals
30
+ themselves would miss the site-wide leak, which is the case that matters.
31
+ - `siteUrl` config option, and `site.origin` on the snapshot.
32
+
33
+ ### Changed
34
+
35
+ - `canonical.crosspath` no longer fires on paginated archives (`/blog/page/2`,
36
+ `/blog/p/3`) or AMP variants (`/article/amp`, `/amp/guide`) whose canonical
37
+ points at the parent they are a variant of. Both are ordinary CMS output; the
38
+ rule flagged one per page and buried the real canonical mistakes. A paginated
39
+ page canonicalising somewhere unrelated is still flagged.
40
+ - **Breaking.** `hreflang.missing` now fires only for a page that another
41
+ annotated page names as an alternate, instead of for every unannotated page on
42
+ a site that uses hreflang anywhere. The old rule produced a warning per page on
43
+ any partly translated site, and 48 warnings on a `--limit 50` crawl that
44
+ happened to include two annotated pages. Cross-page rules have to degrade
45
+ safely on a partial crawl; this one did not.
46
+ - **Breaking.** A page that fails to fetch mid-crawl now aborts the run instead
47
+ of being skipped. Skipping it is what let a transient outage report the whole
48
+ site as deleted.
49
+ - Several messages are now constant per finding, with the varying values moved to
50
+ `before` / `after`: `content.dropped`, `jsonld.property.removed`,
51
+ `og.removed`, `twitter.removed`, `hreflang.removed`, `canonical.crosspath`.
52
+ Reports aggregate by code *and* message, so a per-page number in the message
53
+ produced one row per page and defeated the template-wide rollup — a
54
+ 400-page canonical defect read as 400 separate issues.
55
+ - `--version` is injected from `package.json` at build time rather than
56
+ hardcoded in the CLI.
57
+
58
+ ### Fixed
59
+
60
+ - `fetchText` swallowed every network error into "not found", so an unreachable
61
+ page was indistinguishable from a deleted one and `check` failed CI with
62
+ fabricated regressions during a 503 or a DNS blip. It now returns `null` only
63
+ for 404 and 410 and throws otherwise. The declared `signal` parameter was also
64
+ never passed by any caller, so no timeout ever applied and a hung origin hung
65
+ the CI job; requests now carry a 15s default timeout, configurable via the
66
+ `timeout` crawl option.
67
+ - `--fail-on` was passed through unvalidated. An unrecognised value (`warning`,
68
+ `ERROR`, a typo) made every severity comparison false, so the exit gate never
69
+ fired and CI went green over a page of errors. Both the CLI and `shouldFail`
70
+ now reject an unknown severity.
71
+ - Meta and link keywords are matched case-insensitively. CSS attribute selectors
72
+ are case-sensitive but the HTML keywords are not, so `<meta NAME="Description">`
73
+ and `<link rel="Canonical">` extracted as `null` and produced a false
74
+ `description.missing` and a false `canonical.missing`. Also affects
75
+ `name="Robots"` (a noindex regression went unreported), `name="Generator"`
76
+ (platform detection fell back to unknown), `application/ld+json`, and
77
+ `rel="alternate"` inside a multi-token `rel`.
78
+ - Sitemap `<loc>` values are XML-entity decoded and CDATA-wrapped locations are
79
+ matched. XML requires `&` to be escaped, so every URL with a query string was
80
+ fetched at the wrong address, and a CDATA-emitting sitemap discovered zero
81
+ pages.
82
+ - JSON-LD entities without an `@id` are keyed positionally rather than by type.
83
+ Keying on the bare type collapsed several entities of one type — three
84
+ `Product`s, a `FAQPage`'s `Question`s — into one, so a page dropping from three
85
+ to one reported no change at all.
86
+ - `hreflang.noindex.target` no longer flags a page against itself, and duplicate
87
+ targets are collapsed. A noindexed page whose `en` and `x-default` shared an
88
+ href produced four findings, two of them the page accusing itself.
89
+ - `check` writes its status messages to stderr. They were going to stdout ahead
90
+ of the report, so `--format json` did not parse and `--format github`
91
+ annotations were interleaved with prose.
92
+ - Route collisions no longer silently drop a page. `snapshotFromDir` walks files
93
+ in sorted order and reports a collision (`blog.html` and `blog/index.html` both
94
+ map to `/blog`) instead of letting readdir order decide the winner; the origin
95
+ crawl discards sitemap URLs from another host, which `routeFromUrl` would have
96
+ folded onto the same route.
97
+ - Sitemap index expansion deduplicates, caps nested fetches at 50, and stops once
98
+ `--limit` is satisfied. A 200-child index cost 200 serial round-trips before
99
+ slicing to 10 targets. Gzipped children are recognised so they are no longer
100
+ crawled as pages and parsed as HTML, which had emitted three false errors each;
101
+ their contents are still not read.
102
+ - `formatMarkdown` escapes pipes. A title containing one — `Buy Widgets | Acme`,
103
+ a common CMS template — split the table row and swallowed the finding code.
104
+ - `leadAnswer` measures the first paragraph after the `h1`, scoped to
105
+ `main`/`article` and ignoring `header`, `nav`, `footer` and `aside`, as its
106
+ docstring always claimed. It was taking the first `<p>` anywhere in the
107
+ document, so a cookie banner stood in for the lead — and edits to that banner
108
+ showed up as content changes in the lockfile.
109
+
110
+ - `test/snapshot.test.ts` covers the crawl layer, which had no tests of its own:
111
+ 404-versus-failure, an unreachable origin, a server error mid-crawl, off-host
112
+ filtering, sitemap index expansion and the `--limit` short-circuit.
113
+
114
+ ## [0.1.0] - 2026-09-06
115
+
116
+ Initial release. `snapshot`, `check` and `audit` commands; filesystem and HTTP
117
+ crawling; diff classified by transition; absolute, cross-page and hreflang audit
118
+ rules; pretty, JSON, markdown, GitHub and HTML reporters.
119
+
120
+ [0.2.0]: https://github.com/shyamexe/pagetrace/compare/v0.1.0...v0.2.0
121
+ [0.1.0]: https://github.com/shyamexe/pagetrace/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shyam
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 ShyamS 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 ADDED
@@ -0,0 +1,176 @@
1
+ # pagetrace
2
+
3
+ A lockfile for your SEO and AEO surface. Snapshot it, diff every build, fail CI on regressions.
4
+
5
+ Existing SEO and AEO tools tell you your score **right now**. They don't tell you that this deploy dropped the canonical tag from 400 pages, that a layout refactor added `noindex`, that a CMS migration stripped `Product` schema, or that someone quietly blocked `GPTBot` in `robots.txt`. Those regressions are silent for weeks until traffic moves.
6
+
7
+ `pagetrace` records the search-visible surface of your site into a committed `pagetrace.lock.json`, then diffs every build against it. It classifies by *transition*, not by state: a reworded title is `info`, a removed canonical is `error`. So you can fail the build on real regressions without drowning in noise from ordinary content edits.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -D pagetrace
13
+ ```
14
+
15
+
16
+ ## Use
17
+
18
+ Record a baseline from your build output:
19
+
20
+ ```bash
21
+ npx pagetrace snapshot --dir ./out
22
+ git add pagetrace.lock.json
23
+ ```
24
+
25
+ Check every build against it:
26
+
27
+ ```bash
28
+ npx pagetrace check --dir ./out
29
+ ```
30
+
31
+ ```
32
+ (site-wide)
33
+ error robots.txt now blocks GPTBot. aeo.crawler.newly_blocked
34
+ warn llms.txt sections removed: Locations. aeo.llmstxt.sections.removed
35
+
36
+ /
37
+ error Canonical was removed. canonical.removed
38
+ error Page became noindex. robots.noindex.added
39
+ error Structured data entity LocalBusiness was removed. jsonld.entity.removed
40
+ warn Open Graph tags removed: og:title. og.removed
41
+
42
+ 5 error, 9 warning, 2 info
43
+ ```
44
+
45
+ Exit code is `1` when anything at or above `--fail-on` (default `error`) is found.
46
+
47
+ Accept the new state once you've reviewed it:
48
+
49
+ ```bash
50
+ npx pagetrace check --dir ./out --update
51
+ ```
52
+
53
+ ### Auditing an existing site
54
+
55
+ `snapshot` and `check` are for guarding a site you control. To assess a site as it stands — a WordPress install you have just inherited, a client site before a rebuild — use `audit`. No lockfile needed.
56
+
57
+ ```bash
58
+ npx pagetrace audit --url https://example.com --limit 300
59
+ npx pagetrace audit --url https://example.com --format html --out audit.html
60
+ ```
61
+
62
+ 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.
63
+
64
+ ```
65
+ ERROR 2 pages canonicalise to https://acme.test/shop. (1)
66
+ Several pages pointing at one canonical means those pages are declaring
67
+ themselves duplicates and will not rank independently.
68
+ Fix: A common symptom of a plugin canonicalising every archive page to the parent.
69
+
70
+ ERROR Page has no <h1>. (1)
71
+ The h1 anchors the document outline used for passage extraction.
72
+ Fix: Many themes render the post title as h2 inside archive templates.
73
+ Check single.php or the block template for this post type.
74
+ /tag/widgets
75
+ ```
76
+
77
+ 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, and a full hreflang check.
78
+
79
+ The hreflang rules are the ones hardest to run by hand. Google discards an entire hreflang cluster when the annotations are not reciprocal — if `/en/about` points at `/ml/about` but `/ml/about` does not point back, *every* link in that group is ignored, not just the broken one, and nothing reports it. `pagetrace` checks reciprocity across the whole crawl, plus self-references, `x-default`, malformed language codes, and alternates that point at noindexed pages. Sites with no hreflang anywhere are left alone.
80
+
81
+ Counts are per issue, not per page: one template defect on 400 pages reads as a single item labelled `template-wide`, so you triage the fix once.
82
+
83
+ `--format html` writes a self-contained report with no external assets and no scripts, suitable for sending to a client. `--format json` gives the same data keyed by stable finding codes. Detected platforms: WordPress, Next.js, Shopify, Webflow, Wix, Squarespace, Drupal.
84
+
85
+ Route discovery follows `robots.txt` sitemap declarations, then falls back through `/sitemap.xml`, `/sitemap_index.xml` and `/wp-sitemap.xml`.
86
+
87
+ ### Against a live site
88
+
89
+ ```bash
90
+ npx pagetrace snapshot --url https://example.com --limit 200
91
+ ```
92
+
93
+ Routes are discovered from `robots.txt` sitemap declarations, falling back to `/sitemap.xml`. Sitemap indexes are followed one level.
94
+
95
+ ## What it records
96
+
97
+ **Per page** — title, meta description, canonical, robots directives, Open Graph and Twitter Card tags, hreflang alternates, `h1` text, heading outline, every JSON-LD entity with its property list, word count, images missing `alt`, and the length of the first quotable paragraph.
98
+
99
+ **Site-wide** — `robots.txt` crawlability per AI user agent (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended, CCBot and others), declared sitemaps, and `llms.txt` presence with its section headings and size.
100
+
101
+ ## What it catches
102
+
103
+ | Code | Severity | Fires when |
104
+ | --- | --- | --- |
105
+ | `canonical.removed` | error | A page lost its canonical tag |
106
+ | `robots.noindex.added` | error | A page became noindex |
107
+ | `jsonld.entity.removed` | error | A structured data entity disappeared |
108
+ | `jsonld.property.removed` | error | An entity lost a property it used to have |
109
+ | `content.dropped` | error | Word count fell by more than half — usually a render failure |
110
+ | `canonical.offsite` | error | A canonical points at a host other than your own |
111
+ | `aeo.crawler.newly_blocked` | error | `robots.txt` started blocking an AI crawler |
112
+ | `aeo.llmstxt.removed` | error | `/llms.txt` disappeared |
113
+ | `page.removed` | warn | A route in the lockfile is no longer there |
114
+ | `og.removed` / `hreflang.removed` | warn | Social or i18n tags dropped |
115
+ | `title.changed` | info | Ordinary copy edit |
116
+
117
+ Alongside the diff, `check` runs absolute rules: missing title, canonical, `h1`, or description; JSON-LD required and recommended properties for the twenty Schema.org types Google supports as rich results; thin content; images without `alt`; and AEO signals like whether the page opens with something an answer engine can quote. Disable with `--no-audit`.
118
+
119
+ ## Config
120
+
121
+ `pagetrace.config.json`:
122
+
123
+ ```json
124
+ {
125
+ "siteUrl": "https://example.com",
126
+ "ignoreRoutes": ["/preview/*", "/draft"],
127
+ "minWordCount": 300,
128
+ "severity": {
129
+ "title.changed": "off",
130
+ "content.thin": "error"
131
+ },
132
+ "aiAgents": ["GPTBot", "ClaudeBot", "MyCustomBot"]
133
+ }
134
+ ```
135
+
136
+ `siteUrl` is only needed for a `--dir` crawl, and only to detect canonicals pointing at another host — a staging hostname leaking into production canonicals. A `--url` crawl infers it.
137
+
138
+ Every finding has a stable `code`. Set any code to `error`, `warn`, `info`, or `off`.
139
+
140
+ ## CI
141
+
142
+ ```yaml
143
+ - run: npm run build
144
+ - run: npx pagetrace check --dir ./out --format github
145
+ ```
146
+
147
+ `--format` accepts `pretty`, `json`, `markdown` (sized for a PR comment), and `github` (workflow annotations).
148
+
149
+ ## Programmatic API
150
+
151
+ ```ts
152
+ import { extractPage, diffPage, auditPage, snapshotFromDir } from 'pagetrace';
153
+
154
+ const before = extractPage(oldHtml, '/pricing');
155
+ const after = extractPage(newHtml, '/pricing');
156
+
157
+ for (const finding of diffPage(before, after)) {
158
+ console.log(finding.severity, finding.code, finding.message);
159
+ }
160
+ ```
161
+
162
+ `extractPage`, `diffPage`, `diffSnapshots`, `auditPage`, `auditSnapshot`, `applyConfig` and the reporters are all pure functions over plain objects, so they compose into whatever pipeline you already have.
163
+
164
+ ## Notes
165
+
166
+ Pages are fingerprinted from rendered HTML. For client-rendered apps, point `--dir` at a pre-rendered or statically exported build, or `--url` at a deployed preview — otherwise you are snapshotting an empty shell.
167
+
168
+ The `llms.txt` convention and AI crawler behaviour are still moving. Treat those rules as signals worth tracking, not settled standards.
169
+
170
+ ## Notes on the name
171
+
172
+ `pagetrace` here means a trace of a page's search-visible surface over time. It is unrelated to memory page tracing in the Linux kernel or the Go runtime, which share the name.
173
+
174
+ ## License
175
+
176
+ MIT