pagegraph 0.8.4 → 0.10.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 +158 -6
- package/dist/audit.d.ts +7 -0
- package/dist/audit.js +33 -7
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +3827 -2198
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.js.map +1 -1
- package/dist/{checks-6noG_xTS.d.ts → content-DVL2iyGU.d.ts} +274 -126
- package/dist/content-DbyJoRpj.js +1384 -0
- package/dist/content-DbyJoRpj.js.map +1 -0
- package/dist/index.d.ts +19 -8
- package/dist/index.js +31 -555
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/links-BFJc-pJ9.js +0 -318
- 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.
|
|
@@ -88,6 +99,7 @@ export const Route = createFileRoute("/pricing")({
|
|
|
88
99
|
sitemap: { priority: 0.9, changeFrequency: "weekly" },
|
|
89
100
|
related: ["/features", "/docs"],
|
|
90
101
|
link: { title: "Pricing", description: "Simple volume pricing." },
|
|
102
|
+
modifiedAt: "2026-09-29", // sitemap <lastmod> and the freshness report
|
|
91
103
|
},
|
|
92
104
|
},
|
|
93
105
|
head: (ctx) =>
|
|
@@ -349,9 +361,15 @@ severities:
|
|
|
349
361
|
- **structural** — internally broken declarations; these fail `pagegraph check` (exit 1):
|
|
350
362
|
dead or duplicate edges, path collisions, a redirect in the sitemap, a
|
|
351
363
|
sitemap/noindex contradiction, a `related` target with no `link` card, an
|
|
352
|
-
instance without a title, or an unmet
|
|
364
|
+
instance without a title, a date that is not ISO 8601, or an unmet
|
|
365
|
+
contextual-link coverage rule.
|
|
353
366
|
- **editorial** — quality smells, reported but non-failing: duplicate or mis-sized
|
|
354
|
-
titles and descriptions.
|
|
367
|
+
titles and descriptions, and pages older than the freshness policy.
|
|
368
|
+
|
|
369
|
+
Each sitemap entry's `<lastmod>` is the page's last declared change: an
|
|
370
|
+
instance's `modifiedAt`, else its `publishedAt`; a route's `modifiedAt`. A param
|
|
371
|
+
route's date is never inherited by its instances. Undated pages emit no
|
|
372
|
+
`<lastmod>`.
|
|
355
373
|
|
|
356
374
|
## Coverage gate (Vite)
|
|
357
375
|
|
|
@@ -401,6 +419,8 @@ export default defineSeoConfig({
|
|
|
401
419
|
|
|
402
420
|
```bash
|
|
403
421
|
pagegraph check # CI gate — exit 1 on structural violations
|
|
422
|
+
pagegraph check --site <url> # also check every declared page's rendered HTML
|
|
423
|
+
pagegraph stale # refresh queue: sitemap pages past the freshness policy
|
|
404
424
|
pagegraph graph # the graph as a tree · --format mermaid | json
|
|
405
425
|
pagegraph inspect /pricing # one node: policy, sitemap status, in/out edges
|
|
406
426
|
pagegraph inspect <url> --live # fetch a deployed page, validate its rendered <head>
|
|
@@ -423,7 +443,12 @@ private-IP-blocked HTTP path as `pagegraph audit` — and reports what a crawler
|
|
|
423
443
|
actually receives: the real homepage depth, the pages with no incoming internal
|
|
424
444
|
edge (rendered orphans), and, when the app has a `seo.config.ts`, the
|
|
425
445
|
declared-vs-rendered link diff. It is bounded with `--limit` and needs no
|
|
426
|
-
framework or config
|
|
446
|
+
framework or config. Discovery reads same-origin `robots.txt` and bounded
|
|
447
|
+
sitemaps before following page anchors, so a sitemap-only page can appear as an
|
|
448
|
+
orphan. The JSON `crawl` block records skipped URLs, discovery failures, and
|
|
449
|
+
sitemap truncation. Link-followed non-HTML alternates are recorded in `nonHtml`
|
|
450
|
+
without counting as HTML page failures; a sitemap-listed or declared page served
|
|
451
|
+
as non-HTML still blocks a coverage assertion:
|
|
427
452
|
|
|
428
453
|
```bash
|
|
429
454
|
pagegraph links verify https://example.com
|
|
@@ -456,6 +481,11 @@ and the provenance a gate needs to trust it:
|
|
|
456
481
|
"bodyTruncated": false,
|
|
457
482
|
"truncatedPages": [],
|
|
458
483
|
"failures": [],
|
|
484
|
+
"nonHtml": [{ "url": "https://example.com/auth.md", "finalUrl": "https://example.com/auth.md", "contentType": "text/markdown" }],
|
|
485
|
+
"discoveryFailures": [],
|
|
486
|
+
"skipped": [],
|
|
487
|
+
"sitemapTruncated": false,
|
|
488
|
+
"sitemapUsed": true,
|
|
459
489
|
},
|
|
460
490
|
"nodes": ["/", "/pricing"],
|
|
461
491
|
"edges": [{ "from": "/", "to": "/pricing", "region": "body" }],
|
|
@@ -481,6 +511,7 @@ pagegraph links verify --rendered .seo/rendered.json --assert-coverage --json |
|
|
|
481
511
|
The command exits non-zero when any rule is unmet, and **refuses to assert** (also
|
|
482
512
|
non-zero) rather than reporting a false pass when the crawl outran `--limit`, a
|
|
483
513
|
page failed to fetch (its anchors are missing), a body hit `--max-body-bytes`, the
|
|
514
|
+
robots or sitemap discovery is incomplete, the
|
|
484
515
|
config declares no `coverage`, or the artifact's origin differs from
|
|
485
516
|
`seo.config.ts`. Under `--assert-coverage` the report gains a `coverage` block
|
|
486
517
|
(`{ rules, ok, violations }`); the default `--json` summary is unchanged.
|
|
@@ -491,9 +522,11 @@ config declares no `coverage`, or the artifact's origin differs from
|
|
|
491
522
|
`(source, destination)` pairs that are plausible contextual links and not already
|
|
492
523
|
connected. Pages are grouped into clusters — a shared top-level section, or the
|
|
493
524
|
same `kind` for root-level pages — and only sitemap-eligible pages are proposed.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
exclude
|
|
525
|
+
Each direction is evaluated separately. A declared `related` edge or served
|
|
526
|
+
body-region anchor excludes only its own direction. Nav, header, and footer
|
|
527
|
+
anchors do not exclude contextual suggestions. `--rendered` accepts a JSON dump
|
|
528
|
+
of already-served anchors (`[{ from, to, region }]` or `{ edges: [...] }`).
|
|
529
|
+
Legacy edges without a `region` are treated as body links.
|
|
497
530
|
|
|
498
531
|
The output is a deterministic, reviewable plan: human text by default and
|
|
499
532
|
versioned JSON with `--json`. Nothing is applied; `--limit` and `--cluster`
|
|
@@ -507,6 +540,43 @@ pagegraph links candidates --rendered rendered.json --json | jq
|
|
|
507
540
|
pagegraph improve links --page "/docs/**"
|
|
508
541
|
```
|
|
509
542
|
|
|
543
|
+
For content-backed placements, opt in to a bounded same-origin probe. The configured
|
|
544
|
+
`origin` must match `--site`. Each schema-version-2 result carries a sentence
|
|
545
|
+
from served main content, an exact anchor phrase inside it, a target sentence,
|
|
546
|
+
and score components. Unreadable and over-limit pages are reported as skipped.
|
|
547
|
+
Code samples and tabular comparisons are excluded. An anchor is proposed only
|
|
548
|
+
when its distinct content words occur together in a target sentence, so review
|
|
549
|
+
the placement and its context before editing. Repeated passages and clause
|
|
550
|
+
fragments are excluded; named destinations require a destination-specific anchor.
|
|
551
|
+
The home and about pages are omitted as destinations because site navigation already leads there.
|
|
552
|
+
The pair-only command above remains offline and emits schema version 1.
|
|
553
|
+
The page budget selects low-inbound candidate pairs first. Use repeatable
|
|
554
|
+
`--target /exact/path` to put a known weak destination within the budget. Add
|
|
555
|
+
`--source /exact/path` when you have a likely source and want to guarantee the
|
|
556
|
+
pair is probed even on a large site; raise `--page-limit` for more pairs.
|
|
557
|
+
For a site with many legal, comparison, or other low-inbound pages, use
|
|
558
|
+
`--cluster blog` (or another section) to spend the same page budget on an
|
|
559
|
+
editorial cluster. A zero-result broad probe means no placement passed the
|
|
560
|
+
evidence rules within its selected pages; it does not mean the whole site has
|
|
561
|
+
no possible links.
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
pagegraph links verify https://example.com --json
|
|
565
|
+
pagegraph links candidates --site https://example.com --page-limit 25 --json > /tmp/pagegraph-suggestions.json
|
|
566
|
+
pagegraph links candidates --site https://example.com --cluster blog --page-limit 25 --json
|
|
567
|
+
pagegraph links candidates --site https://example.com --source /blog/guide --target /blog/weak --page-limit 25 --json > /tmp/pagegraph-suggestions.json
|
|
568
|
+
pagegraph improve links --suggestions /tmp/pagegraph-suggestions.json --dry-run
|
|
569
|
+
pagegraph improve links --suggestions /tmp/pagegraph-suggestions.json
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`improve links` validates the artifact against the configured origin and graph,
|
|
573
|
+
then asks Jev to judge the suggested placements. Only accepted `add` or `update`
|
|
574
|
+
decisions can open an edit turn; skipped and review-band links remain in the run
|
|
575
|
+
artifact without authorizing an edit. Before an accepted edit, Pagegraph checks
|
|
576
|
+
that the source sentence and target passage still occur in served copy. Regenerate
|
|
577
|
+
the artifact if either page changed. For a local site, pass `--allow-private` to
|
|
578
|
+
both `links candidates --site` and `improve links --suggestions`.
|
|
579
|
+
|
|
510
580
|
### Contextual-link coverage
|
|
511
581
|
|
|
512
582
|
Declare a contextual-link coverage policy in `seo.config.ts` (a `coverage` array
|
|
@@ -524,6 +594,83 @@ pagegraph check --require-inbound "/pricing=2" --require-inbound "/features/*=1"
|
|
|
524
594
|
actually served, assert the same policy on the rendered graph — see
|
|
525
595
|
[Assert coverage on the rendered graph](#assert-coverage-on-the-rendered-graph).
|
|
526
596
|
|
|
597
|
+
### Check rendered content
|
|
598
|
+
|
|
599
|
+
The declared graph cannot see what a page renders: headings come from
|
|
600
|
+
components, and the `<title>`, JSON-LD, and visible text are composed at render
|
|
601
|
+
time. `pagegraph check --site <url>` fetches every sitemap-eligible page (and
|
|
602
|
+
every concrete page that declares `noindex`) from a running server and checks
|
|
603
|
+
the server-rendered HTML — what a crawler receives before any script runs. A
|
|
604
|
+
server error or failed request is retried once, because a dev server's first
|
|
605
|
+
render of a route can fail while it compiles.
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
pagegraph check --site http://localhost:3000 --allow-private
|
|
609
|
+
pagegraph check --site https://staging.example.com --json | jq '.violations'
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
| Rule | Severity | Fails when |
|
|
613
|
+
| --- | --- | --- |
|
|
614
|
+
| `rendered-page-unavailable` | structural | The page errors, is not HTML, is truncated, or redirects off its declared path |
|
|
615
|
+
| `rendered-robots-mismatch` | structural | An indexable page renders a `noindex` robots meta, or a `noindex` declaration renders none |
|
|
616
|
+
| `rendered-canonical-mismatch` | structural | The canonical is missing or its path is not the declared path |
|
|
617
|
+
| `missing-h1`, `multiple-h1`, `empty-h1` | structural | The page does not have exactly one H1 with text |
|
|
618
|
+
| `faq-not-visible` | structural | A FAQPage question or answer is not in the rendered text |
|
|
619
|
+
| `offer-price-not-visible` | structural | An Offer or AggregateOffer price does not appear on the page |
|
|
620
|
+
| `heading-level-skip` | editorial | The main-content outline skips a level (h2 → h4) |
|
|
621
|
+
| `h1-title-mismatch` | editorial | The H1 and `<title>` share no significant term |
|
|
622
|
+
| `webpage-head-mismatch` | editorial | A WebPage `name` or `description` disagrees with the head |
|
|
623
|
+
| `thin-content` | editorial | Main content is under the page's `content.minWords` floor |
|
|
624
|
+
|
|
625
|
+
Google requires structured data to describe content the page visibly shows, so
|
|
626
|
+
a FAQ answer that exists only in JSON-LD or in hydration data (an accordion that
|
|
627
|
+
mounts answers on open, say) is structural. Comparisons ignore punctuation,
|
|
628
|
+
case, and markup; prices match as numbers (`19` matches `$19/mo` and `19.00`,
|
|
629
|
+
not `$199`). Canonicals compare by path, and robots are read from the meta tag
|
|
630
|
+
only, so a local or preview host that sends `X-Robots-Tag: noindex` everywhere
|
|
631
|
+
can still run the check.
|
|
632
|
+
|
|
633
|
+
Word-count floors are project policy, declared per path glob; a page matching
|
|
634
|
+
several rules must meet the highest:
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
export default defineSeoConfig({
|
|
638
|
+
// ...
|
|
639
|
+
content: {
|
|
640
|
+
minWords: [
|
|
641
|
+
{ path: "/blog/*", minWords: 500 },
|
|
642
|
+
{ path: "/compare/*", minWords: 800 },
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
`pagegraph audit` runs the heading and structured-data rules on every audited
|
|
649
|
+
HTML page too. The robots, canonical, and word-count rules need the declared
|
|
650
|
+
graph, so they stay in `check`.
|
|
651
|
+
|
|
652
|
+
### Freshness
|
|
653
|
+
|
|
654
|
+
Declare a freshness policy to turn old pages into editorial `stale-page`
|
|
655
|
+
findings in `pagegraph check`, and list the refresh queue with `pagegraph stale`:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
export default defineSeoConfig({
|
|
659
|
+
// ...
|
|
660
|
+
freshness: { maxAgeDays: 90 },
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
```bash
|
|
665
|
+
pagegraph stale # stale pages, oldest first; undated pages counted
|
|
666
|
+
pagegraph stale --max-age-days 180 --json
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Only sitemap-eligible pages are judged. A page without a date is reported as
|
|
670
|
+
undated rather than stale; give content its `modifiedAt` (or a route its
|
|
671
|
+
`staticData.seo.modifiedAt`) to bring it into the report and the sitemap's
|
|
672
|
+
`<lastmod>`.
|
|
673
|
+
|
|
527
674
|
### Audit any website
|
|
528
675
|
|
|
529
676
|
`pagegraph audit` is framework-independent and does not need `seo.config.ts`. It
|
|
@@ -584,6 +731,8 @@ Everything the CLI checks is a pure function you can call from a test:
|
|
|
584
731
|
```ts
|
|
585
732
|
import {
|
|
586
733
|
checkGraph,
|
|
734
|
+
checkPageContent,
|
|
735
|
+
extractPageContent,
|
|
587
736
|
hasStructuralViolations,
|
|
588
737
|
inspectHtml,
|
|
589
738
|
} from "pagegraph";
|
|
@@ -593,6 +742,9 @@ expect(hasStructuralViolations(checkGraph(loadSeoGraph()))).toBe(false);
|
|
|
593
742
|
// render a page however you like, then assert the head it actually ships
|
|
594
743
|
const report = inspectHtml(url, 200, html);
|
|
595
744
|
expect(report.issues).toEqual([]);
|
|
745
|
+
|
|
746
|
+
// …and the content it ships: one H1, visible FAQ answers, shown prices
|
|
747
|
+
expect(checkPageContent(extractPageContent(html))).toEqual([]);
|
|
596
748
|
```
|
|
597
749
|
|
|
598
750
|
## TanStack Start example
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./content-DVL2iyGU.js";
|
|
1
2
|
import * as Context from "effect/Context";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import * as Layer from "effect/Layer";
|
|
@@ -103,6 +104,12 @@ interface ProbeOptions {
|
|
|
103
104
|
* probe field that grows with document size; the audit report leaves it unset.
|
|
104
105
|
*/
|
|
105
106
|
readonly captureAnchors?: boolean | undefined;
|
|
107
|
+
/** Capture bounded text for robots and sitemap discovery. Never enabled for audit reports. */
|
|
108
|
+
readonly captureBody?: boolean | undefined;
|
|
109
|
+
/** Refuse cross-origin redirects during a same-origin crawl. */
|
|
110
|
+
readonly sameOrigin?: string | undefined;
|
|
111
|
+
/** Optional per-redirect crawl policy, checked before each network request. */
|
|
112
|
+
readonly allowUrl?: ((url: URL) => boolean) | undefined;
|
|
106
113
|
}
|
|
107
114
|
interface AuditOptions {
|
|
108
115
|
readonly concurrency?: number;
|
package/dist/audit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as extractAnchors, r as extractPageContent, t as checkPageContent } from "./content-DbyJoRpj.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";
|
|
@@ -110,6 +110,22 @@ const pageRules = [
|
|
|
110
110
|
return [finding(input, "invalid-canonical", "Page canonical is not a valid URL.", "Use an absolute or resolvable canonical URL.", canonical)];
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "content",
|
|
116
|
+
evaluate: (observation, input) => {
|
|
117
|
+
const content = record(record(pageEvidence(observation.evidence))?.["document"])?.["content"];
|
|
118
|
+
const list = record(content)?.["findings"];
|
|
119
|
+
if (!Array.isArray(list)) return [];
|
|
120
|
+
return list.flatMap((entry) => {
|
|
121
|
+
const rule = stringAt(entry, "rule");
|
|
122
|
+
const message = stringAt(entry, "message");
|
|
123
|
+
const fix = stringAt(entry, "fix");
|
|
124
|
+
const severity = stringAt(entry, "severity");
|
|
125
|
+
if (rule === void 0 || message === void 0 || fix === void 0) return [];
|
|
126
|
+
return [finding(input, rule, message, fix, void 0, severity === "editorial" ? "editorial" : "structural")];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
113
129
|
}
|
|
114
130
|
];
|
|
115
131
|
const evaluateRules = (rules, observation, input) => rules.flatMap((rule) => rule.evaluate(observation, input));
|
|
@@ -864,10 +880,11 @@ const canonicalUrl = (html) => {
|
|
|
864
880
|
return null;
|
|
865
881
|
};
|
|
866
882
|
const countMatches = (value, pattern) => [...value.matchAll(pattern)].length;
|
|
867
|
-
const extractDocumentSignals = (body, contentType, requestedUrl) => {
|
|
868
|
-
const isHtml = contentType.includes("text/html");
|
|
883
|
+
const extractDocumentSignals = (body, contentType, requestedUrl, bodyTruncated = false) => {
|
|
884
|
+
const isHtml = contentType.includes("text/html") || contentType.includes("application/xhtml+xml");
|
|
869
885
|
const isMarkdown = contentType.includes("text/markdown");
|
|
870
886
|
const looksJson = contentType.includes("json") || requestedUrl.pathname.endsWith(".json");
|
|
887
|
+
const content = isHtml && !bodyTruncated ? extractPageContent(body) : null;
|
|
871
888
|
const titleMatch = isHtml ? /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(body) : null;
|
|
872
889
|
let jsonValid = null;
|
|
873
890
|
if (looksJson && body.trim().length > 0) try {
|
|
@@ -885,8 +902,13 @@ const extractDocumentSignals = (body, contentType, requestedUrl) => {
|
|
|
885
902
|
modulePreloadCount: isHtml ? countMatches(body, /<link\b[^>]*rel\s*=\s*["'][^"']*modulepreload[^"']*["'][^>]*>/gi) : 0,
|
|
886
903
|
scriptCount: isHtml ? countMatches(body, /<script\b/gi) : 0,
|
|
887
904
|
stylesheetCount: isHtml ? countMatches(body, /<link\b[^>]*rel\s*=\s*["'][^"']*stylesheet[^"']*["'][^>]*>/gi) : 0,
|
|
888
|
-
wordCount: isMarkdown ? body.trim().split(/\s+/).filter(Boolean).length : null,
|
|
889
|
-
jsonValid
|
|
905
|
+
wordCount: isMarkdown ? body.trim().split(/\s+/).filter(Boolean).length : content?.wordCount ?? null,
|
|
906
|
+
jsonValid,
|
|
907
|
+
content: content === null ? null : {
|
|
908
|
+
h1: content.h1,
|
|
909
|
+
outline: content.outline,
|
|
910
|
+
findings: checkPageContent(content)
|
|
911
|
+
}
|
|
890
912
|
};
|
|
891
913
|
};
|
|
892
914
|
const readBoundedBody = async (response, maxBodyBytes) => {
|
|
@@ -960,6 +982,8 @@ const probeHttp = async (request, options) => {
|
|
|
960
982
|
let currentUrl = request.url;
|
|
961
983
|
let response = null;
|
|
962
984
|
for (let redirectCount = 0; redirectCount <= 10; redirectCount += 1) {
|
|
985
|
+
if (options.sameOrigin !== void 0 && currentUrl.origin !== options.sameOrigin) throw new Error(`Redirected off-origin to ${currentUrl.href}`);
|
|
986
|
+
if (options.allowUrl !== void 0 && !options.allowUrl(currentUrl)) throw new Error(`Redirected to robots-disallowed URL ${currentUrl.href}`);
|
|
963
987
|
response = await requestPinned(currentUrl, request, options);
|
|
964
988
|
headersAt = performance.now();
|
|
965
989
|
const location = response.headers.location;
|
|
@@ -977,7 +1001,7 @@ const probeHttp = async (request, options) => {
|
|
|
977
1001
|
} : await readBoundedBody(response, options.maxBodyBytes);
|
|
978
1002
|
const contentType = typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : "";
|
|
979
1003
|
const body = new TextDecoder().decode(bounded.bytes);
|
|
980
|
-
const anchors = options.captureAnchors === true && contentType
|
|
1004
|
+
const anchors = options.captureAnchors === true && (contentType === "" || /text\/html|application\/xhtml\+xml/i.test(contentType)) ? extractAnchors(body, currentUrl.href) : void 0;
|
|
981
1005
|
return {
|
|
982
1006
|
kind: request.kind,
|
|
983
1007
|
method: request.method,
|
|
@@ -994,7 +1018,8 @@ const probeHttp = async (request, options) => {
|
|
|
994
1018
|
capturedBodySha256: bounded.bytes.byteLength === 0 ? null : createHash("sha256").update(bounded.bytes).digest("hex"),
|
|
995
1019
|
bodyTruncated: bounded.truncated,
|
|
996
1020
|
bodyExcerpt: bodyExcerpt(body, contentType),
|
|
997
|
-
|
|
1021
|
+
...options.captureBody === true ? { body } : {},
|
|
1022
|
+
document: body.length === 0 ? null : extractDocumentSignals(body, contentType, currentUrl, bounded.truncated),
|
|
998
1023
|
anchors,
|
|
999
1024
|
error: null
|
|
1000
1025
|
};
|
|
@@ -1015,6 +1040,7 @@ const probeHttp = async (request, options) => {
|
|
|
1015
1040
|
capturedBodySha256: null,
|
|
1016
1041
|
bodyTruncated: false,
|
|
1017
1042
|
bodyExcerpt: null,
|
|
1043
|
+
...options.captureBody === true ? { body: void 0 } : {},
|
|
1018
1044
|
document: null,
|
|
1019
1045
|
error: errorMessage$2(error)
|
|
1020
1046
|
};
|