@rivi-software/agentic-seo-check 0.0.0-stage → 0.1.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +149 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/core/audit.d.ts +72 -0
  7. package/dist/core/audit.js +337 -0
  8. package/dist/core/audit.js.map +1 -0
  9. package/dist/core/concurrency.d.ts +4 -0
  10. package/dist/core/concurrency.js +29 -0
  11. package/dist/core/concurrency.js.map +1 -0
  12. package/dist/core/crawl.d.ts +40 -0
  13. package/dist/core/crawl.js +195 -0
  14. package/dist/core/crawl.js.map +1 -0
  15. package/dist/core/fetch.d.ts +27 -0
  16. package/dist/core/fetch.js +135 -0
  17. package/dist/core/fetch.js.map +1 -0
  18. package/dist/core/html/extract.d.ts +10 -0
  19. package/dist/core/html/extract.js +150 -0
  20. package/dist/core/html/extract.js.map +1 -0
  21. package/dist/core/html/structured-data.d.ts +11 -0
  22. package/dist/core/html/structured-data.js +185 -0
  23. package/dist/core/html/structured-data.js.map +1 -0
  24. package/dist/core/html/text.d.ts +23 -0
  25. package/dist/core/html/text.js +149 -0
  26. package/dist/core/html/text.js.map +1 -0
  27. package/dist/core/lighthouse.d.ts +49 -0
  28. package/dist/core/lighthouse.js +140 -0
  29. package/dist/core/lighthouse.js.map +1 -0
  30. package/dist/core/performance-rules.d.ts +3 -0
  31. package/dist/core/performance-rules.js +53 -0
  32. package/dist/core/performance-rules.js.map +1 -0
  33. package/dist/core/render-rules.d.ts +7 -0
  34. package/dist/core/render-rules.js +77 -0
  35. package/dist/core/render-rules.js.map +1 -0
  36. package/dist/core/render.d.ts +22 -0
  37. package/dist/core/render.js +73 -0
  38. package/dist/core/render.js.map +1 -0
  39. package/dist/core/report/markdown.d.ts +2 -0
  40. package/dist/core/report/markdown.js +152 -0
  41. package/dist/core/report/markdown.js.map +1 -0
  42. package/dist/core/robots.d.ts +11 -0
  43. package/dist/core/robots.js +33 -0
  44. package/dist/core/robots.js.map +1 -0
  45. package/dist/core/rules/canonical.d.ts +2 -0
  46. package/dist/core/rules/canonical.js +105 -0
  47. package/dist/core/rules/canonical.js.map +1 -0
  48. package/dist/core/rules/content.d.ts +1 -0
  49. package/dist/core/rules/content.js +41 -0
  50. package/dist/core/rules/content.js.map +1 -0
  51. package/dist/core/rules/document.d.ts +1 -0
  52. package/dist/core/rules/document.js +60 -0
  53. package/dist/core/rules/document.js.map +1 -0
  54. package/dist/core/rules/headings.d.ts +1 -0
  55. package/dist/core/rules/headings.js +61 -0
  56. package/dist/core/rules/headings.js.map +1 -0
  57. package/dist/core/rules/hreflang.d.ts +1 -0
  58. package/dist/core/rules/hreflang.js +68 -0
  59. package/dist/core/rules/hreflang.js.map +1 -0
  60. package/dist/core/rules/images.d.ts +1 -0
  61. package/dist/core/rules/images.js +59 -0
  62. package/dist/core/rules/images.js.map +1 -0
  63. package/dist/core/rules/index.d.ts +14 -0
  64. package/dist/core/rules/index.js +76 -0
  65. package/dist/core/rules/index.js.map +1 -0
  66. package/dist/core/rules/indexing.d.ts +10 -0
  67. package/dist/core/rules/indexing.js +53 -0
  68. package/dist/core/rules/indexing.js.map +1 -0
  69. package/dist/core/rules/keyword.d.ts +1 -0
  70. package/dist/core/rules/keyword.js +74 -0
  71. package/dist/core/rules/keyword.js.map +1 -0
  72. package/dist/core/rules/links.d.ts +1 -0
  73. package/dist/core/rules/links.js +72 -0
  74. package/dist/core/rules/links.js.map +1 -0
  75. package/dist/core/rules/meta.d.ts +1 -0
  76. package/dist/core/rules/meta.js +60 -0
  77. package/dist/core/rules/meta.js.map +1 -0
  78. package/dist/core/rules/response.d.ts +1 -0
  79. package/dist/core/rules/response.js +59 -0
  80. package/dist/core/rules/response.js.map +1 -0
  81. package/dist/core/rules/rule.d.ts +42 -0
  82. package/dist/core/rules/rule.js +51 -0
  83. package/dist/core/rules/rule.js.map +1 -0
  84. package/dist/core/rules/social.d.ts +1 -0
  85. package/dist/core/rules/social.js +74 -0
  86. package/dist/core/rules/social.js.map +1 -0
  87. package/dist/core/rules/structured-data.d.ts +23 -0
  88. package/dist/core/rules/structured-data.js +169 -0
  89. package/dist/core/rules/structured-data.js.map +1 -0
  90. package/dist/core/rules/title.d.ts +1 -0
  91. package/dist/core/rules/title.js +59 -0
  92. package/dist/core/rules/title.js.map +1 -0
  93. package/dist/core/score.d.ts +3 -0
  94. package/dist/core/score.js +15 -0
  95. package/dist/core/score.js.map +1 -0
  96. package/dist/core/site-rules.d.ts +64 -0
  97. package/dist/core/site-rules.js +251 -0
  98. package/dist/core/site-rules.js.map +1 -0
  99. package/dist/core/sitemap.d.ts +13 -0
  100. package/dist/core/sitemap.js +95 -0
  101. package/dist/core/sitemap.js.map +1 -0
  102. package/dist/core/static.d.ts +9 -0
  103. package/dist/core/static.js +184 -0
  104. package/dist/core/static.js.map +1 -0
  105. package/dist/core/summary.d.ts +2 -0
  106. package/dist/core/summary.js +28 -0
  107. package/dist/core/summary.js.map +1 -0
  108. package/dist/core/types.d.ts +296 -0
  109. package/dist/core/types.js +26 -0
  110. package/dist/core/types.js.map +1 -0
  111. package/dist/core/url.d.ts +20 -0
  112. package/dist/core/url.js +139 -0
  113. package/dist/core/url.js.map +1 -0
  114. package/dist/index.d.ts +11 -0
  115. package/dist/index.js +10 -0
  116. package/dist/index.js.map +1 -0
  117. package/dist/mcp-bin.d.ts +2 -0
  118. package/dist/mcp-bin.js +7 -0
  119. package/dist/mcp-bin.js.map +1 -0
  120. package/dist/mcp.d.ts +5 -0
  121. package/dist/mcp.js +190 -0
  122. package/dist/mcp.js.map +1 -0
  123. package/dist/version.d.ts +2 -0
  124. package/dist/version.js +4 -0
  125. package/dist/version.js.map +1 -0
  126. package/package.json +89 -3
  127. package/skills/seo-audit/SKILL.md +53 -0
@@ -0,0 +1,149 @@
1
+ /** Text analysis helpers: word counts, readability, placeholder detection. */
2
+ const WORD_RE = /[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu;
3
+ export function words(text) {
4
+ return text.match(WORD_RE) ?? [];
5
+ }
6
+ export function wordCount(text) {
7
+ return words(text).length;
8
+ }
9
+ export function collapseWhitespace(text) {
10
+ return text.replace(/\s+/g, ' ').trim();
11
+ }
12
+ export function sentenceCount(text) {
13
+ const parts = text.split(/[.!?]+(?:\s+|$)/).filter((s) => s.trim().length > 0);
14
+ return Math.max(1, parts.length);
15
+ }
16
+ /** English syllable heuristic: vowel groups minus silent trailing e, min 1. */
17
+ export function syllables(word) {
18
+ const w = word.toLowerCase().replace(/[^a-z]/g, '');
19
+ if (!w)
20
+ return 0;
21
+ if (w.length <= 3)
22
+ return 1;
23
+ let count = (w.replace(/e$/, '').match(/[aeiouy]+/g) ?? []).length;
24
+ if (/le$/.test(w) && !/[aeiouy]le$/.test(w))
25
+ count++;
26
+ return Math.max(1, count);
27
+ }
28
+ /**
29
+ * Flesch Reading Ease (English formula). Returns null when there is not enough text.
30
+ * 90-100 very easy, 60-70 plain English, 30-50 difficult, <30 very difficult.
31
+ */
32
+ export function fleschReadingEase(text) {
33
+ const ws = words(text);
34
+ if (ws.length < 100)
35
+ return null;
36
+ const sentences = sentenceCount(text);
37
+ const syl = ws.reduce((sum, w) => sum + syllables(w), 0);
38
+ const score = 206.835 - 1.015 * (ws.length / sentences) - 84.6 * (syl / ws.length);
39
+ return Math.round(score * 10) / 10;
40
+ }
41
+ const PLACEHOLDER_RE = /lorem ipsum|dolor sit amet|consectetur adipiscing/i;
42
+ export function hasPlaceholderText(text) {
43
+ return PLACEHOLDER_RE.test(text);
44
+ }
45
+ export const GENERIC_TITLES = new Set([
46
+ 'home',
47
+ 'homepage',
48
+ 'index',
49
+ 'untitled',
50
+ 'untitled page',
51
+ 'untitled document',
52
+ 'document',
53
+ 'new page',
54
+ 'page',
55
+ 'title',
56
+ 'welcome',
57
+ 'react app',
58
+ 'vite app',
59
+ 'vite + react',
60
+ 'vite + react + ts',
61
+ 'vite + vue',
62
+ 'vite + svelte',
63
+ 'create next app',
64
+ 'next app',
65
+ 'nuxt app',
66
+ 'nuxt',
67
+ 'astro',
68
+ 'my app',
69
+ 'my site',
70
+ 'my website',
71
+ 'website',
72
+ 'default',
73
+ 'test',
74
+ 'hello world',
75
+ 'svelte app',
76
+ 'angular app',
77
+ 'gatsby default starter',
78
+ 'remix app',
79
+ 'tanstack start starter',
80
+ 'example',
81
+ ]);
82
+ export function isGenericTitle(title) {
83
+ return GENERIC_TITLES.has(collapseWhitespace(title).toLowerCase());
84
+ }
85
+ export const GENERIC_ANCHORS = new Set([
86
+ 'click here',
87
+ 'here',
88
+ 'read more',
89
+ 'learn more',
90
+ 'more',
91
+ 'link',
92
+ 'this',
93
+ 'this page',
94
+ 'continue',
95
+ 'continue reading',
96
+ 'details',
97
+ 'more info',
98
+ 'info',
99
+ 'go',
100
+ 'page',
101
+ 'article',
102
+ 'website',
103
+ 'url',
104
+ 'www',
105
+ ]);
106
+ export function isGenericAnchor(text) {
107
+ const t = collapseWhitespace(text).toLowerCase().replace(/[.!…»›>]+$/, '').trim();
108
+ return GENERIC_ANCHORS.has(t);
109
+ }
110
+ export function normalizeForComparison(text) {
111
+ return collapseWhitespace(text).toLowerCase();
112
+ }
113
+ export function includesKeyword(haystack, keyword) {
114
+ if (!haystack)
115
+ return false;
116
+ return normalizeForComparison(haystack).includes(normalizeForComparison(keyword));
117
+ }
118
+ export function keywordOccurrences(text, keyword) {
119
+ const hay = normalizeForComparison(text);
120
+ const needle = normalizeForComparison(keyword);
121
+ if (!needle)
122
+ return 0;
123
+ let count = 0;
124
+ let idx = hay.indexOf(needle);
125
+ while (idx !== -1) {
126
+ count++;
127
+ idx = hay.indexOf(needle, idx + needle.length);
128
+ }
129
+ return count;
130
+ }
131
+ /** 5-word shingles for near-duplicate detection. */
132
+ export function shingles(text, size = 5) {
133
+ const ws = words(normalizeForComparison(text));
134
+ const out = new Set();
135
+ for (let i = 0; i + size <= ws.length; i++)
136
+ out.add(ws.slice(i, i + size).join(' '));
137
+ return out;
138
+ }
139
+ export function jaccard(a, b) {
140
+ if (a.size === 0 && b.size === 0)
141
+ return 1;
142
+ let inter = 0;
143
+ const [small, large] = a.size < b.size ? [a, b] : [b, a];
144
+ for (const s of small)
145
+ if (large.has(s))
146
+ inter++;
147
+ return inter / (a.size + b.size - inter);
148
+ }
149
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../../src/core/html/text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { PerformanceResult } from './types.js';
2
+ export declare class LighthouseUnavailableError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface LighthouseRunOptions {
6
+ formFactor?: 'mobile' | 'desktop';
7
+ timeoutMs?: number;
8
+ chromePath?: string;
9
+ userAgent?: string;
10
+ headers?: Record<string, string>;
11
+ }
12
+ export declare const LIGHTHOUSE_INSTALL_HINT = "Install Lighthouse and a Chrome: `pnpm add -D lighthouse chrome-launcher` plus Google Chrome, or `pnpm exec playwright install chromium` (the Playwright Chromium is used automatically when found).";
13
+ type LhrLike = {
14
+ lighthouseVersion: string;
15
+ requestedUrl?: string;
16
+ finalDisplayedUrl?: string;
17
+ finalUrl?: string;
18
+ runtimeError?: {
19
+ code: string;
20
+ message: string;
21
+ };
22
+ categories: Record<string, {
23
+ score: number | null;
24
+ auditRefs: {
25
+ id: string;
26
+ weight: number;
27
+ group?: string;
28
+ }[];
29
+ } | undefined>;
30
+ audits: Record<string, LhAudit | undefined>;
31
+ };
32
+ type LhAudit = {
33
+ id: string;
34
+ title: string;
35
+ description: string;
36
+ score: number | null;
37
+ scoreDisplayMode: string;
38
+ numericValue?: number;
39
+ displayValue?: string;
40
+ metricSavings?: Record<string, number | undefined>;
41
+ details?: {
42
+ type?: string;
43
+ overallSavingsMs?: number;
44
+ overallSavingsBytes?: number;
45
+ };
46
+ };
47
+ export declare function runLighthouse(url: string, options?: LighthouseRunOptions): Promise<PerformanceResult>;
48
+ export declare function summarize(lhr: LhrLike, requestedUrl: string, formFactor: 'mobile' | 'desktop'): PerformanceResult;
49
+ export {};
@@ -0,0 +1,140 @@
1
+ export class LighthouseUnavailableError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'LighthouseUnavailableError';
5
+ }
6
+ }
7
+ export const LIGHTHOUSE_INSTALL_HINT = 'Install Lighthouse and a Chrome: `pnpm add -D lighthouse chrome-launcher` plus Google Chrome, or `pnpm exec playwright install chromium` (the Playwright Chromium is used automatically when found).';
8
+ /** Audits we already cover with our own rules; Lighthouse duplicates are suppressed. */
9
+ const COVERED_SEO_AUDITS = new Set(['document-title', 'meta-description', 'image-alt', 'canonical', 'hreflang', 'link-text', 'http-status-code', 'is-crawlable', 'structured-data', 'viewport']);
10
+ export async function runLighthouse(url, options = {}) {
11
+ const formFactor = options.formFactor ?? 'mobile';
12
+ let lighthouse;
13
+ let launch;
14
+ try {
15
+ lighthouse = (await import('lighthouse')).default;
16
+ launch = (await import('chrome-launcher')).launch;
17
+ }
18
+ catch {
19
+ throw new LighthouseUnavailableError(`Lighthouse or chrome-launcher is not installed. ${LIGHTHOUSE_INSTALL_HINT}`);
20
+ }
21
+ const chromePath = options.chromePath ?? process.env['CHROME_PATH'] ?? (await playwrightChromiumPath());
22
+ let chrome;
23
+ try {
24
+ chrome = await launch({
25
+ chromeFlags: ['--headless=new', '--no-first-run', '--disable-gpu', '--no-default-browser-check'],
26
+ ...(chromePath ? { chromePath } : {}),
27
+ logLevel: 'silent',
28
+ });
29
+ }
30
+ catch (err) {
31
+ throw new LighthouseUnavailableError(`Could not launch Chrome: ${err.message}. ${LIGHTHOUSE_INSTALL_HINT}`);
32
+ }
33
+ const timeoutMs = options.timeoutMs ?? 120_000;
34
+ let timer;
35
+ try {
36
+ const config = formFactor === 'desktop' ? (await import('lighthouse/core/config/desktop-config.js')).default : undefined;
37
+ const flags = {
38
+ port: chrome.port,
39
+ output: 'json',
40
+ logLevel: 'silent',
41
+ onlyCategories: ['performance', 'seo', 'agentic-browsing'],
42
+ extraHeaders: options.headers,
43
+ ...(options.userAgent ? { emulatedUserAgent: options.userAgent } : {}),
44
+ };
45
+ const run = lighthouse(url, flags, config);
46
+ const timeout = new Promise((_, reject) => {
47
+ timer = setTimeout(() => reject(new Error(`Lighthouse timed out after ${timeoutMs} ms`)), timeoutMs);
48
+ });
49
+ const result = await Promise.race([run, timeout]);
50
+ if (!result)
51
+ throw new Error('Lighthouse returned no result');
52
+ const lhr = result.lhr;
53
+ if (lhr.runtimeError && lhr.runtimeError.code !== 'NO_ERROR')
54
+ throw new Error(`Lighthouse runtime error ${lhr.runtimeError.code}: ${lhr.runtimeError.message}`);
55
+ return summarize(lhr, url, formFactor);
56
+ }
57
+ finally {
58
+ if (timer)
59
+ clearTimeout(timer);
60
+ chrome.kill();
61
+ }
62
+ }
63
+ async function playwrightChromiumPath() {
64
+ try {
65
+ const pw = await import('playwright');
66
+ const p = pw.chromium.executablePath();
67
+ const { promises: fs } = await import('node:fs');
68
+ await fs.access(p);
69
+ return p;
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ export function summarize(lhr, requestedUrl, formFactor) {
76
+ const numeric = (id) => {
77
+ const v = lhr.audits[id]?.numericValue;
78
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
79
+ };
80
+ const pct = (score) => (typeof score === 'number' ? Math.round(score * 100) : null);
81
+ const perfRefs = lhr.categories['performance']?.auditRefs ?? [];
82
+ const opportunities = [];
83
+ for (const ref of perfRefs) {
84
+ const audit = lhr.audits[ref.id];
85
+ if (!audit || audit.score === null || audit.score >= 0.9)
86
+ continue;
87
+ if (ref.group === 'metrics' || ref.group === 'hidden')
88
+ continue;
89
+ const savings = audit.metricSavings ?? {};
90
+ const msValues = ['LCP', 'FCP', 'TBT', 'INP', 'SI'].map((k) => savings[k]).filter((v) => typeof v === 'number' && v > 0);
91
+ const savingsMs = audit.details?.overallSavingsMs ?? (msValues.length ? Math.max(...msValues) : null);
92
+ const savingsBytes = audit.details?.overallSavingsBytes ?? null;
93
+ const hasSavings = (savingsMs ?? 0) > 0 || (savingsBytes ?? 0) > 0 || (typeof savings['CLS'] === 'number' && savings['CLS'] > 0) || audit.details?.type === 'opportunity';
94
+ if (!hasSavings)
95
+ continue;
96
+ opportunities.push({ id: audit.id, title: audit.title, description: stripMarkdownLinks(audit.description), score: audit.score, savingsMs: savingsMs ?? null, savingsBytes });
97
+ }
98
+ opportunities.sort((a, b) => (b.savingsMs ?? 0) - (a.savingsMs ?? 0) || (b.savingsBytes ?? 0) - (a.savingsBytes ?? 0));
99
+ const failed = (category, skip) => {
100
+ const refs = lhr.categories[category]?.auditRefs ?? [];
101
+ const out = [];
102
+ for (const ref of refs) {
103
+ const audit = lhr.audits[ref.id];
104
+ if (!audit || skip.has(audit.id))
105
+ continue;
106
+ if (!['binary', 'numeric', 'metricSavings'].includes(audit.scoreDisplayMode))
107
+ continue;
108
+ if (audit.score === null || audit.score >= 1)
109
+ continue;
110
+ out.push({ id: audit.id, title: audit.title, description: stripMarkdownLinks(audit.description), score: audit.score, displayValue: audit.displayValue ?? null });
111
+ }
112
+ return out;
113
+ };
114
+ return {
115
+ tool: 'lighthouse',
116
+ lighthouseVersion: lhr.lighthouseVersion,
117
+ formFactor,
118
+ requestedUrl,
119
+ finalUrl: lhr.finalDisplayedUrl ?? lhr.finalUrl ?? requestedUrl,
120
+ metrics: {
121
+ performanceScore: pct(lhr.categories['performance']?.score),
122
+ seoScore: pct(lhr.categories['seo']?.score),
123
+ agenticBrowsingScore: pct(lhr.categories['agentic-browsing']?.score),
124
+ lcpMs: numeric('largest-contentful-paint'),
125
+ cls: numeric('cumulative-layout-shift'),
126
+ tbtMs: numeric('total-blocking-time'),
127
+ fcpMs: numeric('first-contentful-paint'),
128
+ speedIndexMs: numeric('speed-index'),
129
+ ttfbMs: numeric('server-response-time'),
130
+ inpMs: numeric('interaction-to-next-paint'),
131
+ },
132
+ opportunities,
133
+ failedSeoAudits: failed('seo', COVERED_SEO_AUDITS),
134
+ agenticBrowsingAudits: failed('agentic-browsing', new Set()),
135
+ };
136
+ }
137
+ function stripMarkdownLinks(text) {
138
+ return text.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, '$1 ($2)').trim();
139
+ }
140
+ //# sourceMappingURL=lighthouse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lighthouse.js","sourceRoot":"","sources":["../../src/core/lighthouse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Finding, PerformanceResult, RuleMeta } from './types.js';
2
+ export declare const PERFORMANCE_RULES: RuleMeta[];
3
+ export declare function performanceFindings(result: PerformanceResult): Finding[];
@@ -0,0 +1,53 @@
1
+ import { finding, GOOGLE_DOCS } from './rules/rule.js';
2
+ const meta = (id, severity, description, recommendation, docs = GOOGLE_DOCS.cwv) => ({
3
+ id,
4
+ category: 'performance',
5
+ severity,
6
+ description,
7
+ recommendation,
8
+ docs,
9
+ requires: 'lighthouse',
10
+ });
11
+ export const PERFORMANCE_RULES = [
12
+ meta('perf.lcp-slow', 'warning', 'Largest Contentful Paint above 2.5 s (poor above 4 s). Lab measurement from Lighthouse.', 'Preload the LCP image/font, serve it from a CDN in a modern format, inline critical CSS, and cut server response time.'),
13
+ meta('perf.cls-high', 'warning', 'Cumulative Layout Shift above 0.1 (poor above 0.25).', 'Reserve space for images, ads and embeds with width/height or aspect-ratio; avoid inserting content above existing content; use font-display: optional or size-adjusted fallbacks.'),
14
+ meta('perf.tbt-high', 'warning', 'Total Blocking Time above 200 ms (poor above 600 ms). Lab proxy for Interaction to Next Paint.', 'Split long JavaScript tasks, defer non-critical scripts, remove unused JavaScript, and move work off the main thread.'),
15
+ meta('perf.fcp-slow', 'info', 'First Contentful Paint above 1.8 s (poor above 3 s).', 'Eliminate render-blocking resources, inline critical CSS, and reduce server response time.'),
16
+ meta('perf.ttfb-slow', 'warning', 'Server response time (TTFB) above 800 ms.', 'Cache HTML at the edge, avoid slow data fetches during render, and use a nearby region/CDN.'),
17
+ meta('perf.score-low', 'warning', 'Lighthouse performance score below 50.', 'Work through the opportunities listed in the performance result, biggest savings first.'),
18
+ meta('perf.opportunity', 'info', 'A Lighthouse opportunity with measurable estimated savings.', 'Apply the Lighthouse recommendation described in the finding.'),
19
+ meta('lighthouse.seo-audit-failed', 'warning', 'A Lighthouse SEO audit failed (rendered-DOM checks such as crawlable anchors, robots.txt validity).', 'Follow the Lighthouse audit description in the finding.', 'https://developer.chrome.com/docs/lighthouse/seo/'),
20
+ meta('lighthouse.agentic-audit-failed', 'info', 'A Lighthouse agentic-browsing audit failed (llms.txt, accessibility tree quality, WebMCP). Experimental.', 'Follow the Lighthouse audit description. These checks reflect how well AI agents can use the site and are not classic ranking factors.', 'https://developer.chrome.com/docs/lighthouse/'),
21
+ ];
22
+ const byId = new Map(PERFORMANCE_RULES.map((r) => [r.id, r]));
23
+ const m = (id) => byId.get(id);
24
+ export function performanceFindings(result) {
25
+ const out = [];
26
+ const url = result.finalUrl || result.requestedUrl;
27
+ const add = (f) => out.push({ ...f, url });
28
+ const { metrics } = result;
29
+ if (metrics.lcpMs !== null && metrics.lcpMs > 2500)
30
+ add(finding(m('perf.lcp-slow'), `LCP is ${(metrics.lcpMs / 1000).toFixed(1)} s (${metrics.lcpMs > 4000 ? 'poor' : 'needs improvement'}).`, { severity: metrics.lcpMs > 4000 ? 'error' : 'warning', evidence: { lcpMs: Math.round(metrics.lcpMs), formFactor: result.formFactor } }));
31
+ if (metrics.cls !== null && metrics.cls > 0.1)
32
+ add(finding(m('perf.cls-high'), `CLS is ${metrics.cls.toFixed(3)} (${metrics.cls > 0.25 ? 'poor' : 'needs improvement'}).`, { severity: metrics.cls > 0.25 ? 'error' : 'warning', evidence: { cls: Number(metrics.cls.toFixed(3)) } }));
33
+ if (metrics.tbtMs !== null && metrics.tbtMs > 200)
34
+ add(finding(m('perf.tbt-high'), `TBT is ${Math.round(metrics.tbtMs)} ms (${metrics.tbtMs > 600 ? 'poor' : 'needs improvement'}).`, { severity: metrics.tbtMs > 600 ? 'error' : 'warning', evidence: { tbtMs: Math.round(metrics.tbtMs) } }));
35
+ if (metrics.fcpMs !== null && metrics.fcpMs > 1800)
36
+ add(finding(m('perf.fcp-slow'), `FCP is ${(metrics.fcpMs / 1000).toFixed(1)} s.`, { severity: metrics.fcpMs > 3000 ? 'warning' : 'info', evidence: { fcpMs: Math.round(metrics.fcpMs) } }));
37
+ if (metrics.ttfbMs !== null && metrics.ttfbMs > 800)
38
+ add(finding(m('perf.ttfb-slow'), `Server response time is ${Math.round(metrics.ttfbMs)} ms.`, { evidence: { ttfbMs: Math.round(metrics.ttfbMs) } }));
39
+ if (metrics.performanceScore !== null && metrics.performanceScore < 50)
40
+ add(finding(m('perf.score-low'), `Lighthouse performance score is ${metrics.performanceScore}/100.`, { evidence: { performanceScore: metrics.performanceScore, formFactor: result.formFactor } }));
41
+ for (const opp of result.opportunities.slice(0, 8)) {
42
+ const savings = [opp.savingsMs ? `${Math.round(opp.savingsMs)} ms` : null, opp.savingsBytes ? `${Math.round(opp.savingsBytes / 1024)} KiB` : null].filter(Boolean).join(', ');
43
+ add(finding(m('perf.opportunity'), `${opp.title}${savings ? ` (est. savings ${savings})` : ''}.`, { recommendation: opp.description, evidence: { auditId: opp.id, savingsMs: opp.savingsMs, savingsBytes: opp.savingsBytes, score: opp.score } }));
44
+ }
45
+ for (const audit of result.failedSeoAudits) {
46
+ add(finding(m('lighthouse.seo-audit-failed'), `Lighthouse SEO audit "${audit.title}" failed${audit.displayValue ? `: ${audit.displayValue}` : ''}.`, { recommendation: audit.description, evidence: { auditId: audit.id, score: audit.score } }));
47
+ }
48
+ for (const audit of result.agenticBrowsingAudits) {
49
+ add(finding(m('lighthouse.agentic-audit-failed'), `Lighthouse agentic-browsing audit "${audit.title}" failed${audit.displayValue ? `: ${audit.displayValue}` : ''}.`, { recommendation: audit.description, evidence: { auditId: audit.id, score: audit.score } }));
50
+ }
51
+ return out;
52
+ }
53
+ //# sourceMappingURL=performance-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-rules.js","sourceRoot":"","sources":["../../src/core/performance-rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { ExtractedPage, Finding, RenderComparison, RenderSnapshot, ResponseInfo, RuleMeta } from './types.js';
2
+ export declare const RENDER_RULES: RuleMeta[];
3
+ export declare function snapshot(page: ExtractedPage, response?: ResponseInfo): RenderSnapshot;
4
+ export declare function compareRender(raw: ExtractedPage, rendered: ExtractedPage, renderedUrl: string, consoleErrors: string[], rawResponse?: ResponseInfo): {
5
+ comparison: RenderComparison;
6
+ findings: Finding[];
7
+ };
@@ -0,0 +1,77 @@
1
+ import { robotsDirectives } from './rules/indexing.js';
2
+ import { finding, GOOGLE_DOCS } from './rules/rule.js';
3
+ const meta = (id, severity, description, recommendation) => ({
4
+ id,
5
+ category: 'rendering',
6
+ severity,
7
+ description,
8
+ recommendation,
9
+ docs: GOOGLE_DOCS.javascript,
10
+ requires: 'render',
11
+ });
12
+ const SSR_FIX = 'Render this in the server HTML (SSR/SSG/prerender). Googlebot queues JavaScript rendering separately and other crawlers and link previews never run it.';
13
+ export const RENDER_RULES = [
14
+ meta('rendering.title-client-only', 'warning', 'The <title> only exists after JavaScript runs.', SSR_FIX),
15
+ meta('rendering.title-changed', 'info', 'JavaScript changes the <title> after load.', 'Make the server HTML title match the final title so both agree.'),
16
+ meta('rendering.description-client-only', 'warning', 'The meta description only exists after JavaScript runs.', SSR_FIX),
17
+ meta('rendering.canonical-client-only', 'warning', 'The canonical link only exists after JavaScript runs.', SSR_FIX),
18
+ meta('rendering.canonical-changed', 'warning', 'JavaScript changes the canonical URL after load, sending conflicting signals.', 'Emit the final canonical in the server HTML and do not modify it client-side.'),
19
+ meta('rendering.noindex-client-only', 'error', 'A noindex directive is injected by JavaScript. Google applies it when it renders the page, which removes the page from the index.', 'Remove the client-side noindex (often a framework/head-manager default or an auth guard) or move the decision to the server.'),
20
+ meta('rendering.noindex-removed-by-js', 'warning', 'The server HTML says noindex but JavaScript removes it. Google honours the server noindex and skips rendering, so the page stays out of the index.', 'Remove noindex from the server HTML if the page should be indexed.'),
21
+ meta('rendering.h1-client-only', 'warning', 'The <h1> only exists after JavaScript runs.', SSR_FIX),
22
+ meta('rendering.content-client-only', 'warning', 'Most of the body text only exists after JavaScript runs.', SSR_FIX),
23
+ meta('rendering.links-client-only', 'warning', 'Most internal links only exist after JavaScript runs, delaying discovery of the linked pages.', `${SSR_FIX} At minimum, render the main navigation and content links server-side.`),
24
+ meta('rendering.structured-data-client-only', 'info', 'Structured data is only added by JavaScript.', 'Google processes JSON-LD injected by JavaScript, but other consumers do not. Prefer emitting it in the server HTML.'),
25
+ meta('rendering.console-errors', 'info', 'The page logged JavaScript errors while rendering.', 'Fix runtime errors; a failing script can leave crawlers with an incomplete page.'),
26
+ ];
27
+ const byId = new Map(RENDER_RULES.map((r) => [r.id, r]));
28
+ const m = (id) => byId.get(id);
29
+ export function snapshot(page, response) {
30
+ return {
31
+ title: page.title,
32
+ metaDescription: page.metaDescription,
33
+ canonical: page.canonical,
34
+ robots: robotsDirectives(page, response).directives,
35
+ h1Count: page.h1s.filter(Boolean).length,
36
+ wordCount: page.wordCount,
37
+ internalLinks: page.links.filter((l) => l.internal).length,
38
+ structuredDataCount: page.structuredData.length,
39
+ };
40
+ }
41
+ export function compareRender(raw, rendered, renderedUrl, consoleErrors, rawResponse) {
42
+ const a = snapshot(raw, rawResponse);
43
+ const b = snapshot(rendered);
44
+ const findings = [];
45
+ const url = rawResponse?.finalUrl ?? raw.url;
46
+ const add = (f) => findings.push({ ...f, url });
47
+ if (!a.title && b.title)
48
+ add(finding(m('rendering.title-client-only'), `Title "${b.title}" is only present after rendering.`, { selector: 'head > title', evidence: { raw: a.title, rendered: b.title } }));
49
+ else if (a.title && b.title && a.title !== b.title)
50
+ add(finding(m('rendering.title-changed'), 'Title differs between server HTML and rendered DOM.', { selector: 'head > title', evidence: { raw: a.title, rendered: b.title } }));
51
+ if (!a.metaDescription && b.metaDescription)
52
+ add(finding(m('rendering.description-client-only'), 'Meta description is only present after rendering.', { selector: 'head > meta[name="description"]', evidence: { rendered: b.metaDescription } }));
53
+ if (!a.canonical && b.canonical)
54
+ add(finding(m('rendering.canonical-client-only'), `Canonical ${b.canonical} is only present after rendering.`, { selector: 'head > link[rel="canonical"]', evidence: { rendered: b.canonical } }));
55
+ else if (a.canonical && b.canonical && a.canonical !== b.canonical)
56
+ add(finding(m('rendering.canonical-changed'), 'Canonical differs between server HTML and rendered DOM.', { selector: 'head > link[rel="canonical"]', evidence: { raw: a.canonical, rendered: b.canonical } }));
57
+ const rawNoindex = a.robots.some((d) => d === 'noindex' || d === 'none');
58
+ const renderedNoindex = b.robots.some((d) => d === 'noindex' || d === 'none');
59
+ if (!rawNoindex && renderedNoindex)
60
+ add(finding(m('rendering.noindex-client-only'), 'noindex is added by JavaScript.', { selector: 'head > meta[name="robots"]', evidence: { raw: a.robots, rendered: b.robots } }));
61
+ if (rawNoindex && !renderedNoindex)
62
+ add(finding(m('rendering.noindex-removed-by-js'), 'Server HTML contains noindex; JavaScript removes it.', { selector: 'head > meta[name="robots"]', evidence: { raw: a.robots, rendered: b.robots } }));
63
+ if (a.h1Count === 0 && b.h1Count > 0)
64
+ add(finding(m('rendering.h1-client-only'), 'The <h1> is only present after rendering.', { selector: 'h1', evidence: { rawH1Count: a.h1Count, renderedH1Count: b.h1Count } }));
65
+ if (b.wordCount - a.wordCount >= 100 && a.wordCount < b.wordCount * 0.5) {
66
+ add(finding(m('rendering.content-client-only'), `Server HTML has ${a.wordCount} words; rendered DOM has ${b.wordCount}.`, { evidence: { rawWords: a.wordCount, renderedWords: b.wordCount } }));
67
+ }
68
+ if (b.internalLinks - a.internalLinks >= 10 && a.internalLinks < b.internalLinks * 0.5) {
69
+ add(finding(m('rendering.links-client-only'), `Server HTML has ${a.internalLinks} internal links; rendered DOM has ${b.internalLinks}.`, { evidence: { rawLinks: a.internalLinks, renderedLinks: b.internalLinks } }));
70
+ }
71
+ if (a.structuredDataCount === 0 && b.structuredDataCount > 0)
72
+ add(finding(m('rendering.structured-data-client-only'), `${b.structuredDataCount} structured data item(s) are only present after rendering.`, { evidence: { renderedCount: b.structuredDataCount } }));
73
+ if (consoleErrors.length)
74
+ add(finding(m('rendering.console-errors'), `${consoleErrors.length} JavaScript error(s) logged during render.`, { evidence: { errors: consoleErrors.slice(0, 5) } }));
75
+ return { comparison: { tool: 'playwright', renderedUrl, consoleErrors: consoleErrors.slice(0, 20), raw: a, rendered: b }, findings };
76
+ }
77
+ //# sourceMappingURL=render-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-rules.js","sourceRoot":"","sources":["../../src/core/render-rules.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,QAA8B,EAAE,WAAmB,EAAE,cAAsB,EAAY,EAAE,CAAC,CAAC;IACnH,EAAE;IACF,QAAQ,EAAE,WAAW;IACrB,QAAQ;IACR,WAAW;IACX,cAAc;IACd,IAAI,EAAE,WAAW,CAAC,UAAU;IAC5B,QAAQ,EAAE,QAAQ;CACnB,CAAC,CAAC;AAEH,MAAM,OAAO,GAAG,yJAAyJ,CAAC;AAE1K,MAAM,CAAC,MAAM,YAAY,GAAe;IACtC,IAAI,CAAC,6BAA6B,EAAE,SAAS,EAAE,gDAAgD,EAAE,OAAO,CAAC;IACzG,IAAI,CAAC,yBAAyB,EAAE,MAAM,EAAE,4CAA4C,EAAE,iEAAiE,CAAC;IACxJ,IAAI,CAAC,mCAAmC,EAAE,SAAS,EAAE,yDAAyD,EAAE,OAAO,CAAC;IACxH,IAAI,CAAC,iCAAiC,EAAE,SAAS,EAAE,uDAAuD,EAAE,OAAO,CAAC;IACpH,IAAI,CAAC,6BAA6B,EAAE,SAAS,EAAE,+EAA+E,EAAE,+EAA+E,CAAC;IAChN,IAAI,CAAC,+BAA+B,EAAE,OAAO,EAAE,mIAAmI,EAAE,8HAA8H,CAAC;IACnT,IAAI,CAAC,iCAAiC,EAAE,SAAS,EAAE,oJAAoJ,EAAE,oEAAoE,CAAC;IAC9Q,IAAI,CAAC,0BAA0B,EAAE,SAAS,EAAE,6CAA6C,EAAE,OAAO,CAAC;IACnG,IAAI,CAAC,+BAA+B,EAAE,SAAS,EAAE,0DAA0D,EAAE,OAAO,CAAC;IACrH,IAAI,CAAC,6BAA6B,EAAE,SAAS,EAAE,+FAA+F,EAAE,GAAG,OAAO,wEAAwE,CAAC;IACnO,IAAI,CAAC,uCAAuC,EAAE,MAAM,EAAE,8CAA8C,EAAE,qHAAqH,CAAC;IAC5N,IAAI,CAAC,0BAA0B,EAAE,MAAM,EAAE,oDAAoD,EAAE,kFAAkF,CAAC;CACnL,CAAC;AAEF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,MAAM,CAAC,GAAG,CAAC,EAAU,EAAY,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC;AAElD,MAAM,UAAU,QAAQ,CAAC,IAAmB,EAAE,QAAuB;IACnE,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,UAAU;QACnD,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM;QACxC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM;QAC1D,mBAAmB,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM;KAChD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,GAAkB,EAClB,QAAuB,EACvB,WAAmB,EACnB,aAAuB,EACvB,WAA0B;IAE1B,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACrC,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,WAAW,EAAE,QAAQ,IAAI,GAAG,CAAC,GAAG,CAAC;IAC7C,MAAM,GAAG,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;IAEzD,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,EAAE,UAAU,CAAC,CAAC,KAAK,oCAAoC,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;SACvM,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,EAAE,qDAAqD,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IAEnO,IAAI,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,eAAe;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,mCAAmC,CAAC,EAAE,mDAAmD,EAAE,EAAE,QAAQ,EAAE,iCAAiC,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC;IAEnP,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC,EAAE,aAAa,CAAC,CAAC,SAAS,mCAAmC,EAAE,EAAE,QAAQ,EAAE,8BAA8B,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;SAC/N,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,EAAE,yDAAyD,EAAE,EAAE,QAAQ,EAAE,8BAA8B,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;IAEnR,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;IACzE,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;IAC9E,IAAI,CAAC,UAAU,IAAI,eAAe;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC,EAAE,iCAAiC,EAAE,EAAE,QAAQ,EAAE,4BAA4B,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IACrN,IAAI,UAAU,IAAI,CAAC,eAAe;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC,EAAE,sDAAsD,EAAE,EAAE,QAAQ,EAAE,4BAA4B,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IAE5O,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC,EAAE,2CAA2C,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAEpN,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;QACxE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC,EAAE,mBAAmB,CAAC,CAAC,SAAS,4BAA4B,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;IAClM,CAAC;IACD,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,aAAa,IAAI,EAAE,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,aAAa,GAAG,GAAG,EAAE,CAAC;QACvF,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,EAAE,mBAAmB,CAAC,CAAC,aAAa,qCAAqC,CAAC,CAAC,aAAa,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,aAAa,EAAE,aAAa,EAAE,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC;IACzN,CAAC;IACD,IAAI,CAAC,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,CAAC,mBAAmB,GAAG,CAAC;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,EAAE,GAAG,CAAC,CAAC,mBAAmB,4DAA4D,EAAE,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,CAAC,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,CAAC;IACrQ,IAAI,aAAa,CAAC,MAAM;QAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC,EAAE,GAAG,aAAa,CAAC,MAAM,4CAA4C,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAEhM,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;AACvI,CAAC"}
@@ -0,0 +1,22 @@
1
+ export declare class RenderUnavailableError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface RenderOptions {
5
+ timeoutMs?: number;
6
+ userAgent?: string;
7
+ headers?: Record<string, string>;
8
+ viewport?: {
9
+ width: number;
10
+ height: number;
11
+ };
12
+ /** Wait condition passed to Playwright. */
13
+ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
14
+ }
15
+ export interface RenderOutput {
16
+ html: string;
17
+ finalUrl: string;
18
+ consoleErrors: string[];
19
+ }
20
+ export declare const RENDER_INSTALL_HINT = "Install Playwright to enable rendering: `pnpm add -D playwright && pnpm exec playwright install chromium` (or npm/npx equivalents).";
21
+ /** Load the page in headless Chromium and return the rendered DOM. */
22
+ export declare function renderPage(url: string, options?: RenderOptions): Promise<RenderOutput>;
@@ -0,0 +1,73 @@
1
+ import { DEFAULT_USER_AGENT } from './fetch.js';
2
+ export class RenderUnavailableError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'RenderUnavailableError';
6
+ }
7
+ }
8
+ export const RENDER_INSTALL_HINT = 'Install Playwright to enable rendering: `pnpm add -D playwright && pnpm exec playwright install chromium` (or npm/npx equivalents).';
9
+ async function launchSystemBrowser(playwright) {
10
+ for (const channel of ['chrome', 'msedge']) {
11
+ try {
12
+ return await playwright.chromium.launch({ headless: true, channel });
13
+ }
14
+ catch {
15
+ /* try next */
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ function firstLine(message) {
21
+ return message.split('\n')[0] ?? message;
22
+ }
23
+ /** Load the page in headless Chromium and return the rendered DOM. */
24
+ export async function renderPage(url, options = {}) {
25
+ let playwright;
26
+ try {
27
+ playwright = await import('playwright');
28
+ }
29
+ catch {
30
+ throw new RenderUnavailableError(`Playwright is not installed. ${RENDER_INSTALL_HINT}`);
31
+ }
32
+ let browser;
33
+ try {
34
+ browser = await playwright.chromium.launch({ headless: true });
35
+ }
36
+ catch (err) {
37
+ // No downloaded Playwright browser: fall back to an installed Google Chrome / Edge.
38
+ const fallback = await launchSystemBrowser(playwright);
39
+ if (!fallback)
40
+ throw new RenderUnavailableError(`Could not launch Chromium: ${firstLine(err.message)}. ${RENDER_INSTALL_HINT}`);
41
+ browser = fallback;
42
+ }
43
+ const timeout = options.timeoutMs ?? 30_000;
44
+ const consoleErrors = [];
45
+ try {
46
+ const context = await browser.newContext({
47
+ userAgent: options.userAgent ?? DEFAULT_USER_AGENT,
48
+ viewport: options.viewport ?? { width: 1366, height: 900 },
49
+ extraHTTPHeaders: options.headers,
50
+ });
51
+ const page = await context.newPage();
52
+ page.on('console', (msg) => {
53
+ if (msg.type() === 'error')
54
+ consoleErrors.push(msg.text().slice(0, 300));
55
+ });
56
+ page.on('pageerror', (err) => consoleErrors.push(err.message.slice(0, 300)));
57
+ try {
58
+ await page.goto(url, { waitUntil: options.waitUntil ?? 'networkidle', timeout });
59
+ }
60
+ catch (err) {
61
+ // networkidle can time out on pages with long-polling; fall back to whatever has loaded.
62
+ if (!(err instanceof Error) || !/Timeout/i.test(err.message))
63
+ throw err;
64
+ }
65
+ await page.waitForTimeout(250);
66
+ const html = await page.content();
67
+ return { html, finalUrl: page.url(), consoleErrors };
68
+ }
69
+ finally {
70
+ await browser.close().catch(() => undefined);
71
+ }
72
+ }
73
+ //# sourceMappingURL=render.js.map