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 +121 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/cli.cjs +1541 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1518 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1478 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +1408 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
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
|