pagegraph 0.8.4 → 0.9.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/README.md +65 -4
- package/dist/audit.d.ts +6 -0
- package/dist/audit.js +7 -3
- package/dist/audit.js.map +1 -1
- package/dist/{checks-6noG_xTS.d.ts → checks-_X8yjMvV.d.ts} +11 -1
- package/dist/cli.js +3657 -2703
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +28 -21
- package/dist/index.js.map +1 -1
- package/dist/{links-BFJc-pJ9.js → links-DAa8KVzg.js} +16 -2
- package/dist/links-DAa8KVzg.js.map +1 -0
- package/package.json +1 -1
- package/dist/links-BFJc-pJ9.js.map +0 -1
package/README.md
CHANGED
|
@@ -43,6 +43,17 @@ OpenCode SDK and its private Effect runtime only when invoked. `vite` stays a pe
|
|
|
43
43
|
load your app through Vite at runtime — and `lighthouse` is only needed by `pagegraph audit`.
|
|
44
44
|
Importing `pagegraph/audit` programmatically still needs `effect`.
|
|
45
45
|
|
|
46
|
+
### Agent skill
|
|
47
|
+
|
|
48
|
+
The Pagegraph CLI serves its bundled TanStack Start integration skill:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pagegraph skills list
|
|
52
|
+
pagegraph skills get core
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The skill is maintained at [skill-data/core/SKILL.md](skill-data/core/SKILL.md) and compiled into the CLI, so the instructions match the installed Pagegraph version. `skills get core` prints Markdown to stdout; `--json` returns a structured payload.
|
|
56
|
+
|
|
46
57
|
## Quick start
|
|
47
58
|
|
|
48
59
|
**1. Bind your site identity once.** Route files never see an origin or a brand name.
|
|
@@ -423,7 +434,12 @@ private-IP-blocked HTTP path as `pagegraph audit` — and reports what a crawler
|
|
|
423
434
|
actually receives: the real homepage depth, the pages with no incoming internal
|
|
424
435
|
edge (rendered orphans), and, when the app has a `seo.config.ts`, the
|
|
425
436
|
declared-vs-rendered link diff. It is bounded with `--limit` and needs no
|
|
426
|
-
framework or config
|
|
437
|
+
framework or config. Discovery reads same-origin `robots.txt` and bounded
|
|
438
|
+
sitemaps before following page anchors, so a sitemap-only page can appear as an
|
|
439
|
+
orphan. The JSON `crawl` block records skipped URLs, discovery failures, and
|
|
440
|
+
sitemap truncation. Link-followed non-HTML alternates are recorded in `nonHtml`
|
|
441
|
+
without counting as HTML page failures; a sitemap-listed or declared page served
|
|
442
|
+
as non-HTML still blocks a coverage assertion:
|
|
427
443
|
|
|
428
444
|
```bash
|
|
429
445
|
pagegraph links verify https://example.com
|
|
@@ -456,6 +472,11 @@ and the provenance a gate needs to trust it:
|
|
|
456
472
|
"bodyTruncated": false,
|
|
457
473
|
"truncatedPages": [],
|
|
458
474
|
"failures": [],
|
|
475
|
+
"nonHtml": [{ "url": "https://example.com/auth.md", "finalUrl": "https://example.com/auth.md", "contentType": "text/markdown" }],
|
|
476
|
+
"discoveryFailures": [],
|
|
477
|
+
"skipped": [],
|
|
478
|
+
"sitemapTruncated": false,
|
|
479
|
+
"sitemapUsed": true,
|
|
459
480
|
},
|
|
460
481
|
"nodes": ["/", "/pricing"],
|
|
461
482
|
"edges": [{ "from": "/", "to": "/pricing", "region": "body" }],
|
|
@@ -481,6 +502,7 @@ pagegraph links verify --rendered .seo/rendered.json --assert-coverage --json |
|
|
|
481
502
|
The command exits non-zero when any rule is unmet, and **refuses to assert** (also
|
|
482
503
|
non-zero) rather than reporting a false pass when the crawl outran `--limit`, a
|
|
483
504
|
page failed to fetch (its anchors are missing), a body hit `--max-body-bytes`, the
|
|
505
|
+
robots or sitemap discovery is incomplete, the
|
|
484
506
|
config declares no `coverage`, or the artifact's origin differs from
|
|
485
507
|
`seo.config.ts`. Under `--assert-coverage` the report gains a `coverage` block
|
|
486
508
|
(`{ rules, ok, violations }`); the default `--json` summary is unchanged.
|
|
@@ -491,9 +513,11 @@ config declares no `coverage`, or the artifact's origin differs from
|
|
|
491
513
|
`(source, destination)` pairs that are plausible contextual links and not already
|
|
492
514
|
connected. Pages are grouped into clusters — a shared top-level section, or the
|
|
493
515
|
same `kind` for root-level pages — and only sitemap-eligible pages are proposed.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
exclude
|
|
516
|
+
Each direction is evaluated separately. A declared `related` edge or served
|
|
517
|
+
body-region anchor excludes only its own direction. Nav, header, and footer
|
|
518
|
+
anchors do not exclude contextual suggestions. `--rendered` accepts a JSON dump
|
|
519
|
+
of already-served anchors (`[{ from, to, region }]` or `{ edges: [...] }`).
|
|
520
|
+
Legacy edges without a `region` are treated as body links.
|
|
497
521
|
|
|
498
522
|
The output is a deterministic, reviewable plan: human text by default and
|
|
499
523
|
versioned JSON with `--json`. Nothing is applied; `--limit` and `--cluster`
|
|
@@ -507,6 +531,43 @@ pagegraph links candidates --rendered rendered.json --json | jq
|
|
|
507
531
|
pagegraph improve links --page "/docs/**"
|
|
508
532
|
```
|
|
509
533
|
|
|
534
|
+
For content-backed placements, opt in to a bounded same-origin probe. The configured
|
|
535
|
+
`origin` must match `--site`. Each schema-version-2 result carries a sentence
|
|
536
|
+
from served main content, an exact anchor phrase inside it, a target sentence,
|
|
537
|
+
and score components. Unreadable and over-limit pages are reported as skipped.
|
|
538
|
+
Code samples and tabular comparisons are excluded. An anchor is proposed only
|
|
539
|
+
when its distinct content words occur together in a target sentence, so review
|
|
540
|
+
the placement and its context before editing. Repeated passages and clause
|
|
541
|
+
fragments are excluded; named destinations require a destination-specific anchor.
|
|
542
|
+
The home and about pages are omitted as destinations because site navigation already leads there.
|
|
543
|
+
The pair-only command above remains offline and emits schema version 1.
|
|
544
|
+
The page budget selects low-inbound candidate pairs first. Use repeatable
|
|
545
|
+
`--target /exact/path` to put a known weak destination within the budget. Add
|
|
546
|
+
`--source /exact/path` when you have a likely source and want to guarantee the
|
|
547
|
+
pair is probed even on a large site; raise `--page-limit` for more pairs.
|
|
548
|
+
For a site with many legal, comparison, or other low-inbound pages, use
|
|
549
|
+
`--cluster blog` (or another section) to spend the same page budget on an
|
|
550
|
+
editorial cluster. A zero-result broad probe means no placement passed the
|
|
551
|
+
evidence rules within its selected pages; it does not mean the whole site has
|
|
552
|
+
no possible links.
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
pagegraph links verify https://example.com --json
|
|
556
|
+
pagegraph links candidates --site https://example.com --page-limit 25 --json > /tmp/pagegraph-suggestions.json
|
|
557
|
+
pagegraph links candidates --site https://example.com --cluster blog --page-limit 25 --json
|
|
558
|
+
pagegraph links candidates --site https://example.com --source /blog/guide --target /blog/weak --page-limit 25 --json > /tmp/pagegraph-suggestions.json
|
|
559
|
+
pagegraph improve links --suggestions /tmp/pagegraph-suggestions.json --dry-run
|
|
560
|
+
pagegraph improve links --suggestions /tmp/pagegraph-suggestions.json
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
`improve links` validates the artifact against the configured origin and graph,
|
|
564
|
+
then asks Jev to judge the suggested placements. Only accepted `add` or `update`
|
|
565
|
+
decisions can open an edit turn; skipped and review-band links remain in the run
|
|
566
|
+
artifact without authorizing an edit. Before an accepted edit, Pagegraph checks
|
|
567
|
+
that the source sentence and target passage still occur in served copy. Regenerate
|
|
568
|
+
the artifact if either page changed. For a local site, pass `--allow-private` to
|
|
569
|
+
both `links candidates --site` and `improve links --suggestions`.
|
|
570
|
+
|
|
510
571
|
### Contextual-link coverage
|
|
511
572
|
|
|
512
573
|
Declare a contextual-link coverage policy in `seo.config.ts` (a `coverage` array
|
package/dist/audit.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ interface ProbeOptions {
|
|
|
103
103
|
* probe field that grows with document size; the audit report leaves it unset.
|
|
104
104
|
*/
|
|
105
105
|
readonly captureAnchors?: boolean | undefined;
|
|
106
|
+
/** Capture bounded text for robots and sitemap discovery. Never enabled for audit reports. */
|
|
107
|
+
readonly captureBody?: boolean | undefined;
|
|
108
|
+
/** Refuse cross-origin redirects during a same-origin crawl. */
|
|
109
|
+
readonly sameOrigin?: string | undefined;
|
|
110
|
+
/** Optional per-redirect crawl policy, checked before each network request. */
|
|
111
|
+
readonly allowUrl?: ((url: URL) => boolean) | undefined;
|
|
106
112
|
}
|
|
107
113
|
interface AuditOptions {
|
|
108
114
|
readonly concurrency?: number;
|
package/dist/audit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as extractAnchors } from "./links-
|
|
1
|
+
import { a as extractAnchors } from "./links-DAa8KVzg.js";
|
|
2
2
|
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import * as Context from "effect/Context";
|
|
@@ -865,7 +865,7 @@ const canonicalUrl = (html) => {
|
|
|
865
865
|
};
|
|
866
866
|
const countMatches = (value, pattern) => [...value.matchAll(pattern)].length;
|
|
867
867
|
const extractDocumentSignals = (body, contentType, requestedUrl) => {
|
|
868
|
-
const isHtml = contentType.includes("text/html");
|
|
868
|
+
const isHtml = contentType.includes("text/html") || contentType.includes("application/xhtml+xml");
|
|
869
869
|
const isMarkdown = contentType.includes("text/markdown");
|
|
870
870
|
const looksJson = contentType.includes("json") || requestedUrl.pathname.endsWith(".json");
|
|
871
871
|
const titleMatch = isHtml ? /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(body) : null;
|
|
@@ -960,6 +960,8 @@ const probeHttp = async (request, options) => {
|
|
|
960
960
|
let currentUrl = request.url;
|
|
961
961
|
let response = null;
|
|
962
962
|
for (let redirectCount = 0; redirectCount <= 10; redirectCount += 1) {
|
|
963
|
+
if (options.sameOrigin !== void 0 && currentUrl.origin !== options.sameOrigin) throw new Error(`Redirected off-origin to ${currentUrl.href}`);
|
|
964
|
+
if (options.allowUrl !== void 0 && !options.allowUrl(currentUrl)) throw new Error(`Redirected to robots-disallowed URL ${currentUrl.href}`);
|
|
963
965
|
response = await requestPinned(currentUrl, request, options);
|
|
964
966
|
headersAt = performance.now();
|
|
965
967
|
const location = response.headers.location;
|
|
@@ -977,7 +979,7 @@ const probeHttp = async (request, options) => {
|
|
|
977
979
|
} : await readBoundedBody(response, options.maxBodyBytes);
|
|
978
980
|
const contentType = typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : "";
|
|
979
981
|
const body = new TextDecoder().decode(bounded.bytes);
|
|
980
|
-
const anchors = options.captureAnchors === true && contentType
|
|
982
|
+
const anchors = options.captureAnchors === true && (contentType === "" || /text\/html|application\/xhtml\+xml/i.test(contentType)) ? extractAnchors(body, currentUrl.href) : void 0;
|
|
981
983
|
return {
|
|
982
984
|
kind: request.kind,
|
|
983
985
|
method: request.method,
|
|
@@ -994,6 +996,7 @@ const probeHttp = async (request, options) => {
|
|
|
994
996
|
capturedBodySha256: bounded.bytes.byteLength === 0 ? null : createHash("sha256").update(bounded.bytes).digest("hex"),
|
|
995
997
|
bodyTruncated: bounded.truncated,
|
|
996
998
|
bodyExcerpt: bodyExcerpt(body, contentType),
|
|
999
|
+
...options.captureBody === true ? { body } : {},
|
|
997
1000
|
document: body.length === 0 ? null : extractDocumentSignals(body, contentType, currentUrl),
|
|
998
1001
|
anchors,
|
|
999
1002
|
error: null
|
|
@@ -1015,6 +1018,7 @@ const probeHttp = async (request, options) => {
|
|
|
1015
1018
|
capturedBodySha256: null,
|
|
1016
1019
|
bodyTruncated: false,
|
|
1017
1020
|
bodyExcerpt: null,
|
|
1021
|
+
...options.captureBody === true ? { body: void 0 } : {},
|
|
1018
1022
|
document: null,
|
|
1019
1023
|
error: errorMessage$2(error)
|
|
1020
1024
|
};
|