pagegraph 0.8.3 → 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 +84 -7
- 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 +1556 -409
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js.map +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.
|
|
@@ -229,6 +240,8 @@ export default defineSeoConfig({
|
|
|
229
240
|
opencode: {
|
|
230
241
|
configDirectory: ".pagegraph/opencode",
|
|
231
242
|
defaultModel: "openrouter/example/model",
|
|
243
|
+
// Optional per-completion budget in milliseconds; default 180000.
|
|
244
|
+
timeoutMs: 360_000,
|
|
232
245
|
models: {
|
|
233
246
|
"research.keywords": "openrouter/example/research-model",
|
|
234
247
|
},
|
|
@@ -254,9 +267,23 @@ pagegraph research keywords \
|
|
|
254
267
|
```
|
|
255
268
|
|
|
256
269
|
Every workflow starts from the selected graph and context files, asks the SEO agent to search
|
|
257
|
-
Executor's live catalog and
|
|
258
|
-
Jev. Tool paths are discovered at runtime; the generated skills contain
|
|
259
|
-
not a fixed integration list.
|
|
270
|
+
Executor's live catalog and inspect the discovered tools' argument schemas, then evaluates its
|
|
271
|
+
structured observations with Jev. Tool paths are discovered at runtime; the generated skills contain
|
|
272
|
+
editable starter recipes, not a fixed integration list.
|
|
273
|
+
|
|
274
|
+
`workflows.opencode.timeoutMs` sets the positive integer deadline in milliseconds for each OpenCode
|
|
275
|
+
completion (maximum `2147483647`). It covers prompt admission, model execution, and any workflow JSON
|
|
276
|
+
repair prompt. A later action or continuation turn gets a separate budget. The default is `180000`.
|
|
277
|
+
|
|
278
|
+
Selecting pages preserves their incoming and outgoing relationships to non-selected pages in
|
|
279
|
+
`evidence.neighborhood`. Those neighbors supply architecture context without becoming workflow
|
|
280
|
+
targets or consuming `--limit`. The agent receives the limit before research, and PageGraph also caps
|
|
281
|
+
the decoded result before decisions.
|
|
282
|
+
|
|
283
|
+
After research is validated, the run directory contains `research.json`: an incomplete checkpoint
|
|
284
|
+
with the collected evidence, decision inputs, and OpenCode transcript. A downstream failure reports
|
|
285
|
+
its path so the evidence remains available for diagnosis. Only a completed workflow writes
|
|
286
|
+
`run.json` and `summary.md`; a research checkpoint is not a final recommendation.
|
|
260
287
|
|
|
261
288
|
| Command | Result |
|
|
262
289
|
| --- | --- |
|
|
@@ -407,7 +434,12 @@ private-IP-blocked HTTP path as `pagegraph audit` — and reports what a crawler
|
|
|
407
434
|
actually receives: the real homepage depth, the pages with no incoming internal
|
|
408
435
|
edge (rendered orphans), and, when the app has a `seo.config.ts`, the
|
|
409
436
|
declared-vs-rendered link diff. It is bounded with `--limit` and needs no
|
|
410
|
-
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:
|
|
411
443
|
|
|
412
444
|
```bash
|
|
413
445
|
pagegraph links verify https://example.com
|
|
@@ -440,6 +472,11 @@ and the provenance a gate needs to trust it:
|
|
|
440
472
|
"bodyTruncated": false,
|
|
441
473
|
"truncatedPages": [],
|
|
442
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,
|
|
443
480
|
},
|
|
444
481
|
"nodes": ["/", "/pricing"],
|
|
445
482
|
"edges": [{ "from": "/", "to": "/pricing", "region": "body" }],
|
|
@@ -465,6 +502,7 @@ pagegraph links verify --rendered .seo/rendered.json --assert-coverage --json |
|
|
|
465
502
|
The command exits non-zero when any rule is unmet, and **refuses to assert** (also
|
|
466
503
|
non-zero) rather than reporting a false pass when the crawl outran `--limit`, a
|
|
467
504
|
page failed to fetch (its anchors are missing), a body hit `--max-body-bytes`, the
|
|
505
|
+
robots or sitemap discovery is incomplete, the
|
|
468
506
|
config declares no `coverage`, or the artifact's origin differs from
|
|
469
507
|
`seo.config.ts`. Under `--assert-coverage` the report gains a `coverage` block
|
|
470
508
|
(`{ rules, ok, violations }`); the default `--json` summary is unchanged.
|
|
@@ -475,9 +513,11 @@ config declares no `coverage`, or the artifact's origin differs from
|
|
|
475
513
|
`(source, destination)` pairs that are plausible contextual links and not already
|
|
476
514
|
connected. Pages are grouped into clusters — a shared top-level section, or the
|
|
477
515
|
same `kind` for root-level pages — and only sitemap-eligible pages are proposed.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
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.
|
|
481
521
|
|
|
482
522
|
The output is a deterministic, reviewable plan: human text by default and
|
|
483
523
|
versioned JSON with `--json`. Nothing is applied; `--limit` and `--cluster`
|
|
@@ -491,6 +531,43 @@ pagegraph links candidates --rendered rendered.json --json | jq
|
|
|
491
531
|
pagegraph improve links --page "/docs/**"
|
|
492
532
|
```
|
|
493
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
|
+
|
|
494
571
|
### Contextual-link coverage
|
|
495
572
|
|
|
496
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
|
};
|