@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,184 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { extractPage } from './html/extract.js';
4
+ import { DEFAULT_USER_AGENT } from './fetch.js';
5
+ import { buildRobots } from './robots.js';
6
+ import { parseSitemap } from './sitemap.js';
7
+ import { normalizePath, normalizeUrl } from './url.js';
8
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.cache', '.vercel', '.output/server']);
9
+ export async function scanDirectory(dir, options) {
10
+ const started = performance.now();
11
+ const root = path.resolve(dir);
12
+ const stat = await fs.stat(root).catch(() => null);
13
+ if (!stat?.isDirectory())
14
+ throw new Error(`Not a directory: ${root}`);
15
+ const base = new URL(options.baseUrl.endsWith('/') ? options.baseUrl : `${options.baseUrl}/`);
16
+ const files = (await walk(root)).sort();
17
+ const htmlFiles = files.filter((f) => /\.html?$/i.test(f));
18
+ const hitLimit = htmlFiles.length > options.maxPages;
19
+ const selected = htmlFiles.slice(0, options.maxPages);
20
+ const toUrlPath = (file) => `/${path.relative(root, file).split(path.sep).map(encodeURIComponent).join('/')}`;
21
+ const fileByPathKey = new Map();
22
+ for (const f of htmlFiles)
23
+ fileByPathKey.set(normalizePath(toUrlPath(f)), f);
24
+ const records = [];
25
+ const linkChecks = new Map();
26
+ const linkSources = new Map();
27
+ const recordByKey = new Map();
28
+ for (const file of selected) {
29
+ const html = await fs.readFile(file, 'utf8');
30
+ const urlPath = toUrlPath(file).replace(/\/index\.html?$/i, '/');
31
+ const url = new URL(urlPath, base).href;
32
+ const page = extractPage(html, { url, siteHost: base.hostname, includeSubdomains: options.includeSubdomains });
33
+ const record = { url, requestedUrl: url, page, depth: -1, file: path.relative(root, file) };
34
+ records.push(record);
35
+ recordByKey.set(normalizePath(new URL(url).pathname), record);
36
+ }
37
+ for (const record of records) {
38
+ for (const link of record.page.links) {
39
+ if (!link.internal || !link.url)
40
+ continue;
41
+ const target = normalizeUrl(link.url) ?? link.url;
42
+ if (!linkSources.has(target))
43
+ linkSources.set(target, new Set());
44
+ linkSources.get(target).add(record.url);
45
+ if (!linkChecks.has(target)) {
46
+ const key = normalizePath(new URL(target).pathname);
47
+ const exists = fileByPathKey.has(key) || (await fileExists(root, new URL(target).pathname));
48
+ linkChecks.set(target, { url: target, status: exists ? 200 : 404, finalUrl: target, redirectChain: [], error: exists ? undefined : 'No matching file in output directory' });
49
+ }
50
+ }
51
+ }
52
+ /* Depth via BFS from the root index (if present). */
53
+ const start = recordByKey.get('/') ?? records[0];
54
+ if (start) {
55
+ start.depth = 0;
56
+ const queue = [start];
57
+ while (queue.length) {
58
+ const current = queue.shift();
59
+ for (const link of current.page.links) {
60
+ if (!link.internal || !link.url)
61
+ continue;
62
+ const target = recordByKey.get(normalizePath(new URL(link.url).pathname));
63
+ if (target && target.depth === -1) {
64
+ target.depth = current.depth + 1;
65
+ queue.push(target);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ const linkedKeys = new Set([...linkSources.keys()].map((u) => normalizePath(new URL(u).pathname)));
71
+ const unlinkedPages = records.filter((r) => r !== start && !linkedKeys.has(normalizePath(new URL(r.url).pathname))).map((r) => r.file ?? r.url);
72
+ /* robots.txt and sitemap.xml from the output directory. */
73
+ const robotsText = await fs.readFile(path.join(root, 'robots.txt'), 'utf8').catch(() => null);
74
+ const robots = buildRobots(new URL('/robots.txt', base).href, robotsText === null ? 404 : 200, undefined, robotsText, options.userAgent ?? DEFAULT_USER_AGENT);
75
+ const sitemaps = { sources: [], urls: [], urlCount: 0, errors: [] };
76
+ const sitemapCandidates = [...new Set([...robots.info.sitemaps, new URL('/sitemap.xml', base).href])];
77
+ const seen = new Set();
78
+ const urls = new Set();
79
+ while (sitemapCandidates.length && seen.size < 25) {
80
+ const candidate = sitemapCandidates.shift();
81
+ if (seen.has(candidate))
82
+ continue;
83
+ seen.add(candidate);
84
+ let pathname;
85
+ try {
86
+ pathname = decodeURIComponent(new URL(candidate, base).pathname);
87
+ }
88
+ catch {
89
+ sitemaps.errors.push(`${candidate}: invalid URL`);
90
+ continue;
91
+ }
92
+ const file = path.join(root, pathname);
93
+ const xml = await fs.readFile(file, 'utf8').catch(() => null);
94
+ if (xml === null) {
95
+ if (robots.info.sitemaps.includes(candidate))
96
+ sitemaps.errors.push(`${candidate}: declared in robots.txt but ${pathname} is not in the output directory`);
97
+ continue;
98
+ }
99
+ const parsed = parseSitemap(xml);
100
+ if (parsed.kind === 'unknown') {
101
+ sitemaps.errors.push(`${pathname}: could not parse as a sitemap`);
102
+ continue;
103
+ }
104
+ sitemaps.sources.push(candidate);
105
+ if (parsed.kind === 'sitemapindex')
106
+ sitemapCandidates.push(...parsed.locs);
107
+ else
108
+ parsed.locs.forEach((l) => urls.add(l));
109
+ }
110
+ sitemaps.urls = [...urls];
111
+ sitemaps.urlCount = urls.size;
112
+ for (const u of sitemaps.urls) {
113
+ let key;
114
+ try {
115
+ key = normalizePath(new URL(u).pathname);
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ const exists = fileByPathKey.has(key);
121
+ const synthetic = new URL(key === '/' ? '/' : key, base).href;
122
+ if (!linkChecks.has(synthetic))
123
+ linkChecks.set(synthetic, { url: u, status: exists ? 200 : 404, finalUrl: u, redirectChain: [], error: exists ? undefined : 'No matching file in output directory' });
124
+ }
125
+ return {
126
+ startUrl: base.href,
127
+ records,
128
+ linkChecks,
129
+ linkSources,
130
+ robots: robots.info,
131
+ robotsAllows: robots.info.found ? robots.allows : null,
132
+ blockedByRobots: robots.info.found ? records.filter((r) => !robots.allows(r.url)).map((r) => r.url) : [],
133
+ sitemaps,
134
+ probes: {},
135
+ stats: {
136
+ pagesCrawled: records.length,
137
+ pagesDiscovered: htmlFiles.length,
138
+ linksChecked: linkChecks.size,
139
+ maxPages: options.maxPages,
140
+ maxDepth: Infinity,
141
+ hitPageLimit: hitLimit,
142
+ durationMs: Math.round(performance.now() - started),
143
+ respectedRobots: false,
144
+ },
145
+ unlinkedPages,
146
+ };
147
+ }
148
+ async function walk(dir) {
149
+ const out = [];
150
+ const entries = await fs.readdir(dir, { withFileTypes: true });
151
+ for (const entry of entries) {
152
+ if (entry.name.startsWith('.') && entry.name !== '.well-known')
153
+ continue;
154
+ const full = path.join(dir, entry.name);
155
+ if (entry.isDirectory()) {
156
+ if (SKIP_DIRS.has(entry.name))
157
+ continue;
158
+ out.push(...(await walk(full)));
159
+ }
160
+ else if (entry.isFile())
161
+ out.push(full);
162
+ }
163
+ return out;
164
+ }
165
+ async function fileExists(root, pathname) {
166
+ let decoded = pathname;
167
+ try {
168
+ decoded = decodeURIComponent(pathname);
169
+ }
170
+ catch {
171
+ /* keep */
172
+ }
173
+ const candidates = [decoded, `${decoded}.html`, `${decoded}.htm`, path.posix.join(decoded, 'index.html'), path.posix.join(decoded, 'index.htm')];
174
+ for (const c of candidates) {
175
+ const full = path.join(root, c);
176
+ if (!full.startsWith(root))
177
+ continue;
178
+ const stat = await fs.stat(full).catch(() => null);
179
+ if (stat?.isFile())
180
+ return true;
181
+ }
182
+ return false;
183
+ }
184
+ //# sourceMappingURL=static.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/core/static.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { ExtractedPage, PageSummary, ResponseInfo } from './types.js';
2
+ export declare function buildSummary(page: ExtractedPage, response?: ResponseInfo): PageSummary;
@@ -0,0 +1,28 @@
1
+ import { robotsDirectives } from './rules/indexing.js';
2
+ export function buildSummary(page, response) {
3
+ const robots = robotsDirectives(page, response);
4
+ return {
5
+ title: page.title,
6
+ titleLength: page.title?.length ?? 0,
7
+ metaDescription: page.metaDescription,
8
+ metaDescriptionLength: page.metaDescription?.length ?? 0,
9
+ canonical: page.canonical,
10
+ lang: page.lang,
11
+ robots: robots.directives,
12
+ indexable: !robots.noindex && (response?.status ?? 200) < 400,
13
+ h1: page.h1s,
14
+ headings: page.headings.slice(0, 40),
15
+ wordCount: page.wordCount,
16
+ images: { total: page.images.length, missingAlt: page.images.filter((i) => i.alt === null).length },
17
+ links: {
18
+ internal: page.links.filter((l) => l.internal).length,
19
+ external: page.links.filter((l) => l.url && !l.internal).length,
20
+ nofollow: page.links.filter((l) => l.nofollow).length,
21
+ },
22
+ openGraph: page.og,
23
+ twitterCard: page.twitter['card'] ?? null,
24
+ structuredDataTypes: [...new Set(page.structuredData.flatMap((i) => i.types))],
25
+ hreflangCount: page.hreflang.length,
26
+ };
27
+ }
28
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/core/summary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,UAAU,YAAY,CAAC,IAAmB,EAAE,QAAuB;IACvE,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;QACpC,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,qBAAqB,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,IAAI,CAAC;QACxD,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,MAAM,CAAC,UAAU;QACzB,SAAS,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG;QAC7D,EAAE,EAAE,IAAI,CAAC,GAAG;QACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QACpC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,MAAM,EAAE;QACnG,KAAK,EAAE;YACL,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM;YACrD,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM;YAC/D,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM;SACtD;QACD,SAAS,EAAE,IAAI,CAAC,EAAE;QAClB,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI;QACzC,mBAAmB,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9E,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Shared types for the SEO audit core. These shapes are the public contract
3
+ * of the CLI JSON output and the MCP tool results (`schemaVersion: 1`).
4
+ */
5
+ export type Severity = 'error' | 'warning' | 'info';
6
+ export declare const SEVERITY_RANK: Record<Severity, number>;
7
+ export type Category = 'title' | 'meta' | 'headings' | 'canonical' | 'indexing' | 'document' | 'social' | 'images' | 'links' | 'international' | 'structured-data' | 'content' | 'keyword' | 'response' | 'rendering' | 'performance' | 'site' | 'tool';
8
+ export type Requirement = 'render' | 'lighthouse' | 'crawl' | 'keyword';
9
+ /** Static description of a rule, exposed through `seo_list_rules` / `agentic-seo-check rules`. */
10
+ export interface RuleMeta {
11
+ id: string;
12
+ category: Category;
13
+ /** Typical severity. Individual findings may escalate or downgrade. */
14
+ severity: Severity;
15
+ description: string;
16
+ recommendation: string;
17
+ docs?: string;
18
+ /** Only runs when this capability/mode is active. */
19
+ requires?: Requirement;
20
+ }
21
+ /** One actionable problem. Designed to be read by an agent that will edit code. */
22
+ export interface Finding {
23
+ rule: string;
24
+ severity: Severity;
25
+ category: Category;
26
+ /** What is wrong, stated as a fact about this page/site. */
27
+ message: string;
28
+ /** What to change. Concrete and framework-agnostic. */
29
+ recommendation: string;
30
+ /** Page URL or file path the finding belongs to (absent for site-wide findings that list pages in evidence). */
31
+ url?: string;
32
+ /** CSS selector or location hint for where the problem lives in the HTML. */
33
+ selector?: string;
34
+ /** Current values, counts, offending items (capped lists). */
35
+ evidence?: Record<string, unknown>;
36
+ docs?: string;
37
+ }
38
+ export interface SeverityCounts {
39
+ error: number;
40
+ warning: number;
41
+ info: number;
42
+ }
43
+ export interface HeadingInfo {
44
+ level: number;
45
+ text: string;
46
+ }
47
+ export interface ImageInfo {
48
+ src: string | null;
49
+ alt: string | null;
50
+ width: string | null;
51
+ height: string | null;
52
+ loading: string | null;
53
+ }
54
+ export interface LinkInfo {
55
+ href: string;
56
+ /** Absolute resolved URL, or null if it could not be resolved (mailto:, javascript:, malformed). */
57
+ url: string | null;
58
+ text: string;
59
+ rel: string | null;
60
+ internal: boolean;
61
+ nofollow: boolean;
62
+ /** True when the link has visible text, an aria-label, a title, or an image with alt text. */
63
+ hasAccessibleName: boolean;
64
+ }
65
+ export interface HreflangInfo {
66
+ lang: string;
67
+ href: string;
68
+ url: string | null;
69
+ }
70
+ export interface StructuredDataItem {
71
+ source: 'json-ld' | 'microdata';
72
+ types: string[];
73
+ context: string | null;
74
+ data: Record<string, unknown>;
75
+ }
76
+ export interface StructuredDataError {
77
+ source: 'json-ld';
78
+ message: string;
79
+ snippet: string;
80
+ }
81
+ /** Everything the extractor pulls out of one HTML document. Rules read only this. */
82
+ export interface ExtractedPage {
83
+ /** URL used to resolve relative links (the final URL after redirects, or the base URL for files/inline HTML). */
84
+ url: string;
85
+ hasDoctype: boolean;
86
+ lang: string | null;
87
+ charset: string | null;
88
+ viewport: string | null;
89
+ titles: string[];
90
+ title: string | null;
91
+ metaDescriptions: string[];
92
+ metaDescription: string | null;
93
+ /** Content of <meta name="robots"> and <meta name="googlebot">, lower-cased. */
94
+ metaRobots: string[];
95
+ canonicals: string[];
96
+ canonical: string | null;
97
+ headings: HeadingInfo[];
98
+ h1s: string[];
99
+ og: Record<string, string>;
100
+ twitter: Record<string, string>;
101
+ images: ImageInfo[];
102
+ links: LinkInfo[];
103
+ hreflang: HreflangInfo[];
104
+ structuredData: StructuredDataItem[];
105
+ structuredDataErrors: StructuredDataError[];
106
+ /** Visible text of <body> with scripts/styles removed and whitespace collapsed. */
107
+ text: string;
108
+ wordCount: number;
109
+ htmlBytes: number;
110
+ hasBaseHref: boolean;
111
+ }
112
+ export interface RedirectHop {
113
+ url: string;
114
+ status: number;
115
+ }
116
+ export interface ResponseInfo {
117
+ status: number | null;
118
+ finalUrl: string;
119
+ redirectChain: RedirectHop[];
120
+ timeMs: number | null;
121
+ contentType: string | null;
122
+ /** Lower-cased header names. Only SEO-relevant headers are kept. */
123
+ headers: Record<string, string>;
124
+ error?: string;
125
+ }
126
+ export interface PageSummary {
127
+ title: string | null;
128
+ titleLength: number;
129
+ metaDescription: string | null;
130
+ metaDescriptionLength: number;
131
+ canonical: string | null;
132
+ lang: string | null;
133
+ robots: string[];
134
+ indexable: boolean;
135
+ h1: string[];
136
+ headings: HeadingInfo[];
137
+ wordCount: number;
138
+ images: {
139
+ total: number;
140
+ missingAlt: number;
141
+ };
142
+ links: {
143
+ internal: number;
144
+ external: number;
145
+ nofollow: number;
146
+ };
147
+ openGraph: Record<string, string>;
148
+ twitterCard: string | null;
149
+ structuredDataTypes: string[];
150
+ hreflangCount: number;
151
+ }
152
+ export interface LighthouseMetrics {
153
+ performanceScore: number | null;
154
+ seoScore: number | null;
155
+ agenticBrowsingScore: number | null;
156
+ lcpMs: number | null;
157
+ cls: number | null;
158
+ tbtMs: number | null;
159
+ fcpMs: number | null;
160
+ speedIndexMs: number | null;
161
+ ttfbMs: number | null;
162
+ inpMs: number | null;
163
+ }
164
+ export interface LighthouseOpportunity {
165
+ id: string;
166
+ title: string;
167
+ description: string;
168
+ score: number | null;
169
+ savingsMs: number | null;
170
+ savingsBytes: number | null;
171
+ }
172
+ export interface LighthouseAuditSummary {
173
+ id: string;
174
+ title: string;
175
+ description: string;
176
+ score: number | null;
177
+ displayValue: string | null;
178
+ }
179
+ export interface PerformanceResult {
180
+ tool: 'lighthouse';
181
+ lighthouseVersion: string;
182
+ formFactor: 'mobile' | 'desktop';
183
+ requestedUrl: string;
184
+ finalUrl: string;
185
+ metrics: LighthouseMetrics;
186
+ opportunities: LighthouseOpportunity[];
187
+ failedSeoAudits: LighthouseAuditSummary[];
188
+ agenticBrowsingAudits: LighthouseAuditSummary[];
189
+ }
190
+ export interface RenderComparison {
191
+ tool: 'playwright';
192
+ renderedUrl: string;
193
+ consoleErrors: string[];
194
+ raw: RenderSnapshot;
195
+ rendered: RenderSnapshot;
196
+ }
197
+ export interface RenderSnapshot {
198
+ title: string | null;
199
+ metaDescription: string | null;
200
+ canonical: string | null;
201
+ robots: string[];
202
+ h1Count: number;
203
+ wordCount: number;
204
+ internalLinks: number;
205
+ structuredDataCount: number;
206
+ }
207
+ export type PageSource = 'url' | 'file' | 'html';
208
+ export interface PageAudit {
209
+ schemaVersion: 1;
210
+ kind: 'page';
211
+ /** What the caller asked for (URL, path, or "inline-html"). */
212
+ target: string;
213
+ /** Final URL after redirects, or the file path. */
214
+ url: string;
215
+ source: PageSource;
216
+ auditedAt: string;
217
+ response?: ResponseInfo;
218
+ summary: PageSummary;
219
+ findings: Finding[];
220
+ counts: SeverityCounts;
221
+ /** 0-100. Indicative only: 100 minus weighted deductions per finding. */
222
+ score: number;
223
+ performance?: PerformanceResult;
224
+ rendering?: RenderComparison;
225
+ /** Crawl depth from the start URL (site audits only). */
226
+ depth?: number;
227
+ }
228
+ export interface RobotsTxtInfo {
229
+ url: string;
230
+ status: number | null;
231
+ found: boolean;
232
+ sitemaps: string[];
233
+ /** Raw text, capped. */
234
+ text: string | null;
235
+ error?: string;
236
+ }
237
+ export interface SitemapInfo {
238
+ /** Sitemap (and sitemap index) URLs that were fetched. */
239
+ sources: string[];
240
+ /** Page URLs listed across all sitemaps (capped). */
241
+ urls: string[];
242
+ urlCount: number;
243
+ errors: string[];
244
+ }
245
+ export interface CrawlStats {
246
+ pagesCrawled: number;
247
+ pagesDiscovered: number;
248
+ linksChecked: number;
249
+ maxPages: number;
250
+ maxDepth: number;
251
+ hitPageLimit: boolean;
252
+ durationMs: number;
253
+ respectedRobots: boolean;
254
+ }
255
+ export interface IssueRollup {
256
+ rule: string;
257
+ severity: Severity;
258
+ category: Category;
259
+ pages: number;
260
+ message: string;
261
+ recommendation: string;
262
+ /** Up to 10 example URLs. */
263
+ examples: string[];
264
+ }
265
+ export interface SiteAudit {
266
+ schemaVersion: 1;
267
+ kind: 'site';
268
+ target: string;
269
+ source: 'crawl' | 'directory';
270
+ auditedAt: string;
271
+ crawl: CrawlStats;
272
+ robotsTxt: RobotsTxtInfo | null;
273
+ sitemaps: SitemapInfo;
274
+ pages: PageAudit[];
275
+ /** Findings that span pages (duplicates, broken links, sitemap mismatches, ...). */
276
+ siteFindings: Finding[];
277
+ /** Page-level findings rolled up by rule, most severe and most frequent first. */
278
+ issues: IssueRollup[];
279
+ counts: SeverityCounts;
280
+ score: number;
281
+ }
282
+ export type Audit = PageAudit | SiteAudit;
283
+ export interface Thresholds {
284
+ titleMinChars: number;
285
+ titleMaxChars: number;
286
+ descriptionMinChars: number;
287
+ descriptionMaxChars: number;
288
+ h1MaxChars: number;
289
+ thinContentWords: number;
290
+ slowResponseMs: number;
291
+ deepPageDepth: number;
292
+ maxKeywordDensityPercent: number;
293
+ }
294
+ export declare const DEFAULT_THRESHOLDS: Thresholds;
295
+ export declare function emptyCounts(): SeverityCounts;
296
+ export declare function countFindings(findings: Finding[]): SeverityCounts;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared types for the SEO audit core. These shapes are the public contract
3
+ * of the CLI JSON output and the MCP tool results (`schemaVersion: 1`).
4
+ */
5
+ export const SEVERITY_RANK = { error: 3, warning: 2, info: 1 };
6
+ export const DEFAULT_THRESHOLDS = {
7
+ titleMinChars: 30,
8
+ titleMaxChars: 60,
9
+ descriptionMinChars: 70,
10
+ descriptionMaxChars: 160,
11
+ h1MaxChars: 70,
12
+ thinContentWords: 300,
13
+ slowResponseMs: 2000,
14
+ deepPageDepth: 3,
15
+ maxKeywordDensityPercent: 3,
16
+ };
17
+ export function emptyCounts() {
18
+ return { error: 0, warning: 0, info: 0 };
19
+ }
20
+ export function countFindings(findings) {
21
+ const counts = emptyCounts();
22
+ for (const f of findings)
23
+ counts[f.severity]++;
24
+ return counts;
25
+ }
26
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,CAAC,MAAM,aAAa,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AA0UzF,MAAM,CAAC,MAAM,kBAAkB,GAAe;IAC5C,aAAa,EAAE,EAAE;IACjB,aAAa,EAAE,EAAE;IACjB,mBAAmB,EAAE,EAAE;IACvB,mBAAmB,EAAE,GAAG;IACxB,UAAU,EAAE,EAAE;IACd,gBAAgB,EAAE,GAAG;IACrB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,CAAC;IAChB,wBAAwB,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,UAAU,WAAW;IACzB,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAmB;IAC/C,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,QAAQ;QAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC/C,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,20 @@
1
+ export declare function isHttpUrl(value: string): boolean;
2
+ /** Resolve `href` against `base`, returning null for non-http(s) or unparsable values. */
3
+ export declare function resolveUrl(href: string, base: string): string | null;
4
+ /**
5
+ * Canonical form used for visited-sets and comparisons: lower-cased scheme/host,
6
+ * default ports dropped, fragment removed, tracking parameters removed.
7
+ * Path and remaining query are kept verbatim (trailing slashes matter to servers).
8
+ */
9
+ export declare function normalizeUrl(input: string): string | null;
10
+ export declare function hostOf(url: string): string | null;
11
+ export declare function stripWww(host: string): string;
12
+ /** Same site: identical host ignoring `www.`; optionally any subdomain of the start host. */
13
+ export declare function isSameSite(candidate: string, startHost: string, includeSubdomains?: boolean): boolean;
14
+ /** Heuristic: does the path look like it points at an HTML document (not an asset)? */
15
+ export declare function looksLikeHtml(url: string): boolean;
16
+ /** Compare two URLs by path only (used for static-directory audits where the host is synthetic). */
17
+ export declare function samePath(a: string, b: string): boolean;
18
+ /** `/dir/index.html` -> `/dir/`, `/page.html` -> `/page`, trailing slash dropped except root. */
19
+ export declare function normalizePath(pathname: string): string;
20
+ export declare function truncate(value: string, max?: number): string;