@houtini/seo-audit-console 0.2.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 (166) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +211 -0
  3. package/dist/audit/checks.d.ts +35 -0
  4. package/dist/audit/checks.d.ts.map +1 -0
  5. package/dist/audit/checks.js +1475 -0
  6. package/dist/audit/checks.js.map +1 -0
  7. package/dist/audit/drift.d.ts +40 -0
  8. package/dist/audit/drift.d.ts.map +1 -0
  9. package/dist/audit/drift.js +148 -0
  10. package/dist/audit/drift.js.map +1 -0
  11. package/dist/audit/engine.d.ts +33 -0
  12. package/dist/audit/engine.d.ts.map +1 -0
  13. package/dist/audit/engine.js +186 -0
  14. package/dist/audit/engine.js.map +1 -0
  15. package/dist/audit/opportunities.d.ts +23 -0
  16. package/dist/audit/opportunities.d.ts.map +1 -0
  17. package/dist/audit/opportunities.js +149 -0
  18. package/dist/audit/opportunities.js.map +1 -0
  19. package/dist/audit/report.d.ts +11 -0
  20. package/dist/audit/report.d.ts.map +1 -0
  21. package/dist/audit/report.js +59 -0
  22. package/dist/audit/report.js.map +1 -0
  23. package/dist/audit/schema-validate.d.ts +35 -0
  24. package/dist/audit/schema-validate.d.ts.map +1 -0
  25. package/dist/audit/schema-validate.js +293 -0
  26. package/dist/audit/schema-validate.js.map +1 -0
  27. package/dist/audit/templates.d.ts +43 -0
  28. package/dist/audit/templates.d.ts.map +1 -0
  29. package/dist/audit/templates.js +129 -0
  30. package/dist/audit/templates.js.map +1 -0
  31. package/dist/audit/topicGaps.d.ts +42 -0
  32. package/dist/audit/topicGaps.d.ts.map +1 -0
  33. package/dist/audit/topicGaps.js +181 -0
  34. package/dist/audit/topicGaps.js.map +1 -0
  35. package/dist/core/AuditDatabase.d.ts +33 -0
  36. package/dist/core/AuditDatabase.d.ts.map +1 -0
  37. package/dist/core/AuditDatabase.js +481 -0
  38. package/dist/core/AuditDatabase.js.map +1 -0
  39. package/dist/core/Backlinks.d.ts +33 -0
  40. package/dist/core/Backlinks.d.ts.map +1 -0
  41. package/dist/core/Backlinks.js +110 -0
  42. package/dist/core/Backlinks.js.map +1 -0
  43. package/dist/core/Crawler.d.ts +23 -0
  44. package/dist/core/Crawler.d.ts.map +1 -0
  45. package/dist/core/Crawler.js +588 -0
  46. package/dist/core/Crawler.js.map +1 -0
  47. package/dist/core/DataForSeoClient.d.ts +86 -0
  48. package/dist/core/DataForSeoClient.d.ts.map +1 -0
  49. package/dist/core/DataForSeoClient.js +232 -0
  50. package/dist/core/DataForSeoClient.js.map +1 -0
  51. package/dist/core/Entities.d.ts +23 -0
  52. package/dist/core/Entities.d.ts.map +1 -0
  53. package/dist/core/Entities.js +62 -0
  54. package/dist/core/Entities.js.map +1 -0
  55. package/dist/core/GscClient.d.ts +22 -0
  56. package/dist/core/GscClient.d.ts.map +1 -0
  57. package/dist/core/GscClient.js +93 -0
  58. package/dist/core/GscClient.js.map +1 -0
  59. package/dist/core/GscSync.d.ts +20 -0
  60. package/dist/core/GscSync.d.ts.map +1 -0
  61. package/dist/core/GscSync.js +133 -0
  62. package/dist/core/GscSync.js.map +1 -0
  63. package/dist/core/JobManager.d.ts +30 -0
  64. package/dist/core/JobManager.d.ts.map +1 -0
  65. package/dist/core/JobManager.js +68 -0
  66. package/dist/core/JobManager.js.map +1 -0
  67. package/dist/core/RankTracker.d.ts +25 -0
  68. package/dist/core/RankTracker.d.ts.map +1 -0
  69. package/dist/core/RankTracker.js +78 -0
  70. package/dist/core/RankTracker.js.map +1 -0
  71. package/dist/core/Refresh.d.ts +32 -0
  72. package/dist/core/Refresh.d.ts.map +1 -0
  73. package/dist/core/Refresh.js +73 -0
  74. package/dist/core/Refresh.js.map +1 -0
  75. package/dist/core/UrlInspector.d.ts +22 -0
  76. package/dist/core/UrlInspector.d.ts.map +1 -0
  77. package/dist/core/UrlInspector.js +92 -0
  78. package/dist/core/UrlInspector.js.map +1 -0
  79. package/dist/core/WikidataClient.d.ts +19 -0
  80. package/dist/core/WikidataClient.d.ts.map +1 -0
  81. package/dist/core/WikidataClient.js +59 -0
  82. package/dist/core/WikidataClient.js.map +1 -0
  83. package/dist/core/agentReadiness.d.ts +34 -0
  84. package/dist/core/agentReadiness.d.ts.map +1 -0
  85. package/dist/core/agentReadiness.js +119 -0
  86. package/dist/core/agentReadiness.js.map +1 -0
  87. package/dist/core/ctrModel.d.ts +2 -0
  88. package/dist/core/ctrModel.d.ts.map +1 -0
  89. package/dist/core/ctrModel.js +6 -0
  90. package/dist/core/ctrModel.js.map +1 -0
  91. package/dist/core/dashboardData.d.ts +312 -0
  92. package/dist/core/dashboardData.d.ts.map +1 -0
  93. package/dist/core/dashboardData.js +550 -0
  94. package/dist/core/dashboardData.js.map +1 -0
  95. package/dist/core/dataStorage.d.ts +42 -0
  96. package/dist/core/dataStorage.d.ts.map +1 -0
  97. package/dist/core/dataStorage.js +193 -0
  98. package/dist/core/dataStorage.js.map +1 -0
  99. package/dist/core/draftBrief.d.ts +27 -0
  100. package/dist/core/draftBrief.d.ts.map +1 -0
  101. package/dist/core/draftBrief.js +69 -0
  102. package/dist/core/draftBrief.js.map +1 -0
  103. package/dist/core/extract.d.ts +67 -0
  104. package/dist/core/extract.d.ts.map +1 -0
  105. package/dist/core/extract.js +262 -0
  106. package/dist/core/extract.js.map +1 -0
  107. package/dist/core/gscFreshness.d.ts +18 -0
  108. package/dist/core/gscFreshness.d.ts.map +1 -0
  109. package/dist/core/gscFreshness.js +32 -0
  110. package/dist/core/gscFreshness.js.map +1 -0
  111. package/dist/core/linkGraph.d.ts +19 -0
  112. package/dist/core/linkGraph.d.ts.map +1 -0
  113. package/dist/core/linkGraph.js +125 -0
  114. package/dist/core/linkGraph.js.map +1 -0
  115. package/dist/core/passageScore.d.ts +19 -0
  116. package/dist/core/passageScore.d.ts.map +1 -0
  117. package/dist/core/passageScore.js +59 -0
  118. package/dist/core/passageScore.js.map +1 -0
  119. package/dist/core/paths.d.ts +5 -0
  120. package/dist/core/paths.d.ts.map +1 -0
  121. package/dist/core/paths.js +15 -0
  122. package/dist/core/paths.js.map +1 -0
  123. package/dist/core/queryData.d.ts +35 -0
  124. package/dist/core/queryData.d.ts.map +1 -0
  125. package/dist/core/queryData.js +200 -0
  126. package/dist/core/queryData.js.map +1 -0
  127. package/dist/core/reranker.d.ts +8 -0
  128. package/dist/core/reranker.d.ts.map +1 -0
  129. package/dist/core/reranker.js +68 -0
  130. package/dist/core/reranker.js.map +1 -0
  131. package/dist/core/robots.d.ts +11 -0
  132. package/dist/core/robots.d.ts.map +1 -0
  133. package/dist/core/robots.js +76 -0
  134. package/dist/core/robots.js.map +1 -0
  135. package/dist/core/sitemap.d.ts +15 -0
  136. package/dist/core/sitemap.d.ts.map +1 -0
  137. package/dist/core/sitemap.js +100 -0
  138. package/dist/core/sitemap.js.map +1 -0
  139. package/dist/core/sql.d.ts +6 -0
  140. package/dist/core/sql.d.ts.map +1 -0
  141. package/dist/core/sql.js +6 -0
  142. package/dist/core/sql.js.map +1 -0
  143. package/dist/core/types.d.ts +22 -0
  144. package/dist/core/types.d.ts.map +1 -0
  145. package/dist/core/types.js +2 -0
  146. package/dist/core/types.js.map +1 -0
  147. package/dist/core/url-key.d.ts +39 -0
  148. package/dist/core/url-key.d.ts.map +1 -0
  149. package/dist/core/url-key.js +106 -0
  150. package/dist/core/url-key.js.map +1 -0
  151. package/dist/generators/index.d.ts +45 -0
  152. package/dist/generators/index.d.ts.map +1 -0
  153. package/dist/generators/index.js +184 -0
  154. package/dist/generators/index.js.map +1 -0
  155. package/dist/index.d.ts +3 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +10 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/server.d.ts +7 -0
  160. package/dist/server.d.ts.map +1 -0
  161. package/dist/server.js +1387 -0
  162. package/dist/server.js.map +1 -0
  163. package/dist/src/ui/dashboard.html +347 -0
  164. package/dist/src/ui/sync-progress.html +104 -0
  165. package/package.json +101 -0
  166. package/server.json +57 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/audit/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type Database from 'better-sqlite3';
2
+ export interface PageProposal {
3
+ headTerm: string;
4
+ queries: string[];
5
+ totalImpressions: number;
6
+ totalClicks: number;
7
+ bestPosition: number;
8
+ intent: string | null;
9
+ score: number;
10
+ nearestExistingPage: string | null;
11
+ }
12
+ interface Opts {
13
+ minImpressions?: number;
14
+ maxProposals?: number;
15
+ clusterThreshold?: number;
16
+ }
17
+ export declare function suggestPages(db: Database.Database, opts?: Opts): {
18
+ proposals: PageProposal[];
19
+ consideredQueries: number;
20
+ afterDedup: number;
21
+ };
22
+ export {};
23
+ //# sourceMappingURL=opportunities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opportunities.d.ts","sourceRoot":"","sources":["../../src/audit/opportunities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAyB3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,UAAU,IAAI;IAAG,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE;AAE5F,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,GAAE,IAAS,GAAG;IAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA6FjJ"}
@@ -0,0 +1,149 @@
1
+ import { gscFreshness } from '../core/gscFreshness.js';
2
+ /**
3
+ * New-page opportunity engine (Phase 6c). Grounded in REAL demand (GSC), with ruthless
4
+ * subtraction so we never propose a page that already exists. Computable from stored
5
+ * GSC + crawl data — no paid calls. (SERP-overlap clustering and search-volume/intent
6
+ * scoring are refinements layered on via DataForSEO when available.)
7
+ *
8
+ * Pipeline: demand universe (queries we get impressions for but rank 11+ → no winning page)
9
+ * → subtract (A: we already rank ≤10; B: a page already covers ≥70% of the query's words;
10
+ * C: one URL owns >80% of the query's impressions → that's an optimisation, not a new page)
11
+ * → cluster surviving queries lexically → score by impressions × intent → attach evidence.
12
+ */
13
+ const STOP = new Set(['the', 'a', 'an', 'and', 'or', 'of', 'for', 'to', 'in', 'on', 'with', 'your', 'you', 'is', 'are', 'best', 'how', 'what', 'vs', 'why', 'can', 'my', 'i', 'it']);
14
+ const tokenize = (s) => (s || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').split(/\s+/).filter(t => t.length >= 2 && !STOP.has(t));
15
+ const jaccard = (a, b) => {
16
+ if (!a.size || !b.size)
17
+ return 0;
18
+ let inter = 0;
19
+ for (const x of a)
20
+ if (b.has(x))
21
+ inter++;
22
+ return inter / (a.size + b.size - inter);
23
+ };
24
+ const INTENT_MULT = { transactional: 1, commercial: 0.8, informational: 0.3, navigational: 0.1 };
25
+ export function suggestPages(db, opts = {}) {
26
+ const minImpr = opts.minImpressions ?? 50;
27
+ const clusterT = opts.clusterThreshold ?? 0.5;
28
+ // Window off the FINALISED max date (raw MAX(date) includes 1–3 unfinalised, systematically
29
+ // low days that deflate impressions and can shift a query across the rank/dominance thresholds).
30
+ const maxDate = gscFreshness(db).effectiveMax;
31
+ if (!maxDate)
32
+ return { proposals: [], consideredQueries: 0, afterDedup: 0 };
33
+ const win = `date > date('${maxDate}', '-28 days') AND date <= '${maxDate}'`;
34
+ // Demand: per query × page (impression-weighted position).
35
+ const qp = db.prepare(`SELECT query, page_key, SUM(impressions) impr, SUM(clicks) clicks,
36
+ SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
37
+ FROM search_analytics WHERE query IS NOT NULL AND page_key IS NOT NULL AND ${win}
38
+ GROUP BY query, page_key`).all();
39
+ const byQuery = new Map();
40
+ for (const r of qp) {
41
+ let q = byQuery.get(r.query);
42
+ if (!q) {
43
+ q = { totalImpr: 0, totalClicks: 0, best: 999, topPageShare: 0, pages: [] };
44
+ byQuery.set(r.query, q);
45
+ }
46
+ q.totalImpr += r.impr;
47
+ q.totalClicks += r.clicks;
48
+ q.best = Math.min(q.best, r.pos);
49
+ q.pages.push({ key: r.page_key, impr: r.impr, pos: r.pos });
50
+ }
51
+ // Existing-page vocabulary (title + h1 + url slug) → inverted index for the 70% dedup.
52
+ const pages = db.prepare(`SELECT url_key, title, h1, url FROM pages WHERE status_code=200`).all();
53
+ const pageTokens = pages.map(p => {
54
+ const slug = (() => { try {
55
+ return decodeURIComponent(new URL(p.url).pathname).replace(/[-_/]+/g, ' ');
56
+ }
57
+ catch {
58
+ return '';
59
+ } })();
60
+ return { key: p.url_key, toks: new Set(tokenize(`${p.title ?? ''} ${p.h1 ?? ''} ${slug}`)) };
61
+ });
62
+ const invIndex = new Map();
63
+ pageTokens.forEach((pt, i) => { for (const t of pt.toks)
64
+ (invIndex.get(t) ?? invIndex.set(t, []).get(t)).push(i); });
65
+ // Persisted intent (optional) for scoring.
66
+ const intentOf = new Map();
67
+ try {
68
+ for (const r of db.prepare('SELECT keyword, intent FROM keyword_intent').all())
69
+ intentOf.set(r.keyword, r.intent);
70
+ }
71
+ catch { /* table may not exist on very old DBs */ }
72
+ // Subtraction.
73
+ const survivors = [];
74
+ for (const [query, q] of byQuery) {
75
+ if (q.totalImpr < minImpr)
76
+ continue;
77
+ if (q.best <= 10)
78
+ continue; // A: we already rank on page 1
79
+ // C: one URL owns >80% of impressions AND is a genuine contender (ranks ≤15) → optimise
80
+ // that page, don't spawn a new one. (Gated on rank so an incidental single-page mention at
81
+ // position 30 doesn't suppress a real gap — long-tail queries usually surface one page.)
82
+ const dominant = q.pages.reduce((a, b) => (b.impr > a.impr ? b : a));
83
+ if (dominant.impr / q.totalImpr > 0.8 && dominant.pos <= 15)
84
+ continue;
85
+ const qt = tokenize(query);
86
+ if (!qt.length)
87
+ continue;
88
+ // B: does any existing page already cover ≥70% of the query's words?
89
+ const hits = new Map();
90
+ for (const t of qt)
91
+ for (const i of invIndex.get(t) ?? [])
92
+ hits.set(i, (hits.get(i) ?? 0) + 1);
93
+ let maxCover = 0;
94
+ for (const [, n] of hits) {
95
+ const cover = n / qt.length;
96
+ if (cover > maxCover)
97
+ maxCover = cover;
98
+ }
99
+ if (maxCover >= 0.7)
100
+ continue; // page exists → optimisation, not a new page
101
+ survivors.push({ query, toks: new Set(qt), impr: q.totalImpr, clicks: q.totalClicks, best: q.best });
102
+ }
103
+ // Cluster survivors lexically (greedy Jaccard); head term = highest-impression query.
104
+ survivors.sort((a, b) => b.impr - a.impr);
105
+ const clusters = [];
106
+ for (const s of survivors) {
107
+ const c = clusters.find(c => jaccard(s.toks, c.toks) >= clusterT);
108
+ if (c) {
109
+ c.queries.push(s.query);
110
+ for (const t of s.toks)
111
+ c.toks.add(t);
112
+ c.impr += s.impr;
113
+ c.clicks += s.clicks;
114
+ c.best = Math.min(c.best, s.best);
115
+ }
116
+ else
117
+ clusters.push({ headTerm: s.query, toks: new Set(s.toks), queries: [s.query], impr: s.impr, clicks: s.clicks, best: s.best });
118
+ }
119
+ const proposals = clusters.map(c => {
120
+ const intent = intentOf.get(c.headTerm.toLowerCase()) ?? null;
121
+ const mult = intent ? (INTENT_MULT[intent] ?? 0.5) : 0.5;
122
+ // nearest existing page: best partial token coverage of the head term (where to add a link).
123
+ const ht = tokenize(c.headTerm);
124
+ let cover = 0, nearestKey = null;
125
+ const hits = new Map();
126
+ for (const t of ht)
127
+ for (const i of invIndex.get(t) ?? [])
128
+ hits.set(i, (hits.get(i) ?? 0) + 1);
129
+ for (const [i, n] of hits) {
130
+ const cv = n / Math.max(ht.length, 1);
131
+ if (cv > cover) {
132
+ cover = cv;
133
+ nearestKey = pageTokens[i].key;
134
+ }
135
+ }
136
+ return {
137
+ headTerm: c.headTerm,
138
+ queries: c.queries.slice(0, 20),
139
+ totalImpressions: c.impr,
140
+ totalClicks: c.clicks,
141
+ bestPosition: Math.round(c.best * 10) / 10,
142
+ intent,
143
+ score: Math.round(c.impr * mult),
144
+ nearestExistingPage: nearestKey,
145
+ };
146
+ }).sort((a, b) => b.score - a.score).slice(0, opts.maxProposals ?? 50);
147
+ return { proposals, consideredQueries: byQuery.size, afterDedup: survivors.length };
148
+ }
149
+ //# sourceMappingURL=opportunities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opportunities.js","sourceRoot":"","sources":["../../src/audit/opportunities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { AuditResult } from './engine.js';
2
+ /**
3
+ * Concise executive markdown summary of an audit run. Shared by the `run_audit` tool (returned
4
+ * as the model-facing text) and the local shakedown/CLI path so both emit identical reports.
5
+ */
6
+ export declare const SEV_ICON: Record<string, string>;
7
+ export declare const CAT_NAME: Record<string, string>;
8
+ export declare function buildAuditMarkdown(r: AuditResult & {
9
+ elapsedMs?: number;
10
+ }, siteUrl: string): string;
11
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/audit/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAA8D,CAAC;AAC3G,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI3C,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,WAAW,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAoCnG"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Concise executive markdown summary of an audit run. Shared by the `run_audit` tool (returned
3
+ * as the model-facing text) and the local shakedown/CLI path so both emit identical reports.
4
+ */
5
+ export const SEV_ICON = { crit: '🔴', high: '🟠', med: '🟡', low: '⚪', info: '·' };
6
+ export const CAT_NAME = {
7
+ integrity: 'Integrity', crawlability: 'Crawlability', indexation: 'Indexation', onpage: 'On-page',
8
+ content: 'Content', schema: 'Structured data', security: 'Security', 'war-stories': 'Edge cases',
9
+ performance: 'Performance (CWV)', merged: 'Search performance', agentic: 'AI readiness',
10
+ };
11
+ export function buildAuditMarkdown(r, siteUrl) {
12
+ const p = (s) => { try {
13
+ return JSON.parse(s || '{}');
14
+ }
15
+ catch {
16
+ return {};
17
+ } };
18
+ const sev = r.bySeverity ?? {};
19
+ const out = [
20
+ `# SEO audit — ${siteUrl.replace(/^sc-domain:/, '')}`,
21
+ `**${r.total} findings** · 🔴 ${sev.crit ?? 0} critical · 🟠 ${sev.high ?? 0} high · 🟡 ${sev.med ?? 0} medium · ⚪ ${sev.low ?? 0} low`,
22
+ ];
23
+ if (!r.integrityOk)
24
+ out.push('> ⚠ Crawl integrity not verified — treat findings as provisional.');
25
+ // Impact = priority normalised to the run's top finding (preserves magnitude: the #1 issue is
26
+ // 100, a 10x-smaller one is 10). Shown as a size + index instead of a clicks "forecast" — the
27
+ // raw expected-clicks figure reads as a promise, which it isn't. Real measured traffic stays.
28
+ const maxPrio = Math.max(1, ...(r.top ?? []).map((f) => f.priority || 0));
29
+ const sizeOf = (idx) => idx >= 50 ? 'XL' : idx >= 20 ? 'L' : idx >= 5 ? 'M' : 'S';
30
+ out.push('', '## Top priorities (impact vs effort)');
31
+ (r.top ?? []).slice(0, 12).forEach((f, i) => {
32
+ const rec = p(f.recommendation), traf = p(f.traffic_at_risk), eff = p(f.effort);
33
+ const pth = (f.url_key || '—').replace(/^https?:\/\/[^/]+/, '') || '/';
34
+ const tr = (traf.clicks || traf.impressions) ? ` · ${traf.clicks || 0} clicks / ${traf.impressions || 0} impr` : '';
35
+ const idx = Math.round((f.priority || 0) / maxPrio * 100);
36
+ const effort = eff.hours != null ? ` · ~${eff.hours}h` : '';
37
+ out.push(`${i + 1}. ${SEV_ICON[f.severity] ?? '·'} **${rec.title || f.check_id}** — \`${pth}\` · impact ${sizeOf(idx)} (${idx}/100)${effort}${tr}`);
38
+ if (rec.text)
39
+ out.push(` ${rec.text}`);
40
+ });
41
+ const byCat = {};
42
+ for (const c of r.byCheck ?? [])
43
+ (byCat[c.category] ??= []).push(`${c.checkId ?? c.check_id} (${c.count})`);
44
+ out.push('', '## All issues by category');
45
+ for (const [cat, checks] of Object.entries(byCat))
46
+ out.push(`- **${CAT_NAME[cat] ?? cat}** — ${checks.join(', ')}`);
47
+ const idx = r.indexability ?? {};
48
+ const notIndexable = Object.entries(idx).filter(([k]) => k !== 'indexable');
49
+ if (notIndexable.length) {
50
+ out.push('', '## Indexability (crawled URLs)', `- ✅ indexable: ${idx.indexable ?? 0}`);
51
+ for (const [reason, n] of notIndexable)
52
+ out.push(`- ✗ ${reason}: ${n}`);
53
+ }
54
+ if (r.elapsedMs != null)
55
+ out.push('', `_Audit computed in ${(r.elapsedMs / 1000).toFixed(1)}s._`);
56
+ out.push('', `_\`export_report siteUrl:"${siteUrl}"\` → a shareable interactive HTML dashboard._`);
57
+ return out.join('\n');
58
+ }
59
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/audit/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Structured-data validation. Google has NO public Rich-Results validation API and
3
+ * validator.schema.org has none either (it's a Google-run service; the /validate endpoint
4
+ * is undocumented + anti-scraped), so we validate captured JSON-LD locally against a
5
+ * maintained map. Grounded in research/11 and research/01 §5 (#109–123). Deterministic by
6
+ * design: we ONLY flag documented-required fields and unambiguous value errors —
7
+ * recommended-only fields, unknown properties, and unknown @types are deliberately NOT
8
+ * flagged (Google ignores them; flagging them is false-positive noise that erodes trust).
9
+ *
10
+ * Coverage (~30 @types):
11
+ * - Content: Article/BlogPosting/NewsArticle, FAQPage/QAPage, Review/AggregateRating,
12
+ * Movie/Book, VideoObject, Recipe, Course, SoftwareApplication, BreadcrumbList, WebSite, Organization.
13
+ * - Commerce: Product (name + one-of offers/review/aggregateRating).
14
+ * - Jobs: JobPosting (title/description/datePosted/hiringOrganization + one-of
15
+ * jobLocation/jobLocationType/applicantLocationRequirements — so remote jobs aren't flagged).
16
+ * - Datasets: Dataset (name, description).
17
+ * - Travel: Hotel/LodgingBusiness/Resort/BedAndBreakfast/Restaurant (name, address);
18
+ * TouristAttraction/TouristDestination/Trip/TouristTrip (name); LocalBusiness; Event.
19
+ * - ONE_OF groups: "at least one of" required-sets (Google treats some fields as alternatives).
20
+ * - Nested required (Product/SoftwareApplication.offers → price+priceCurrency;
21
+ * Review.reviewRating → ratingValue; JobPosting.jobLocation → address), only when present.
22
+ * - Value sanity: absolute URLs, ISO-8601 dates, ISO-4217 priceCurrency, bare-number price.
23
+ * - Type-level dedup so a complete + partial node of the same type doesn't false-flag.
24
+ * The `pages.json_ld` column is JSON.stringify(arrayOfRawBlockStrings).
25
+ */
26
+ export type SchemaIssueKind = 'parse' | 'context' | 'type' | 'required' | 'value' | 'forbidden';
27
+ export interface SchemaIssue {
28
+ kind: SchemaIssueKind;
29
+ type?: string;
30
+ detail: string;
31
+ fields?: string[];
32
+ }
33
+ /** Validate the raw `pages.json_ld` column. Returns all issues found across blocks. */
34
+ export declare function validateJsonLdColumn(col: string | null): SchemaIssue[];
35
+ //# sourceMappingURL=schema-validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-validate.d.ts","sourceRoot":"","sources":["../../src/audit/schema-validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAkEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AAChG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AA2ID,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,EAAE,CAoCtE"}
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Structured-data validation. Google has NO public Rich-Results validation API and
3
+ * validator.schema.org has none either (it's a Google-run service; the /validate endpoint
4
+ * is undocumented + anti-scraped), so we validate captured JSON-LD locally against a
5
+ * maintained map. Grounded in research/11 and research/01 §5 (#109–123). Deterministic by
6
+ * design: we ONLY flag documented-required fields and unambiguous value errors —
7
+ * recommended-only fields, unknown properties, and unknown @types are deliberately NOT
8
+ * flagged (Google ignores them; flagging them is false-positive noise that erodes trust).
9
+ *
10
+ * Coverage (~30 @types):
11
+ * - Content: Article/BlogPosting/NewsArticle, FAQPage/QAPage, Review/AggregateRating,
12
+ * Movie/Book, VideoObject, Recipe, Course, SoftwareApplication, BreadcrumbList, WebSite, Organization.
13
+ * - Commerce: Product (name + one-of offers/review/aggregateRating).
14
+ * - Jobs: JobPosting (title/description/datePosted/hiringOrganization + one-of
15
+ * jobLocation/jobLocationType/applicantLocationRequirements — so remote jobs aren't flagged).
16
+ * - Datasets: Dataset (name, description).
17
+ * - Travel: Hotel/LodgingBusiness/Resort/BedAndBreakfast/Restaurant (name, address);
18
+ * TouristAttraction/TouristDestination/Trip/TouristTrip (name); LocalBusiness; Event.
19
+ * - ONE_OF groups: "at least one of" required-sets (Google treats some fields as alternatives).
20
+ * - Nested required (Product/SoftwareApplication.offers → price+priceCurrency;
21
+ * Review.reviewRating → ratingValue; JobPosting.jobLocation → address), only when present.
22
+ * - Value sanity: absolute URLs, ISO-8601 dates, ISO-4217 priceCurrency, bare-number price.
23
+ * - Type-level dedup so a complete + partial node of the same type doesn't false-flag.
24
+ * The `pages.json_ld` column is JSON.stringify(arrayOfRawBlockStrings).
25
+ */
26
+ // Google "Required properties" per type (the fields Google actually rewards). Kept conservative
27
+ // on purpose — only fields Google documents as REQUIRED, and only for the standalone @type
28
+ // (nested objects like Product.review/aggregateRating aren't extracted as nodes, so requiring
29
+ // `author`/`ratingValue` here can't false-flag a nested review that inherits context).
30
+ const REQUIRED = {
31
+ Article: ['headline', 'author', 'datePublished', 'image'],
32
+ BlogPosting: ['headline', 'author', 'datePublished', 'image'],
33
+ NewsArticle: ['headline', 'author', 'datePublished', 'image'],
34
+ Product: ['name'], // + one-of(offers/review/aggregateRating) below
35
+ Organization: ['name', 'url', 'logo'],
36
+ BreadcrumbList: ['itemListElement'],
37
+ WebSite: ['url'],
38
+ Event: ['name', 'startDate', 'location'],
39
+ Recipe: ['name', 'image'],
40
+ VideoObject: ['name', 'thumbnailUrl', 'uploadDate'],
41
+ JobPosting: ['title', 'description', 'datePosted', 'hiringOrganization'], // + one-of(location…) below
42
+ LocalBusiness: ['name', 'address'],
43
+ Course: ['name', 'description', 'provider'],
44
+ FAQPage: ['mainEntity'],
45
+ QAPage: ['mainEntity'],
46
+ Review: ['author', 'reviewRating'],
47
+ AggregateRating: ['ratingValue'],
48
+ Movie: ['name'],
49
+ Book: ['name', 'author'],
50
+ SoftwareApplication: ['name'],
51
+ Dataset: ['name', 'description'],
52
+ // Travel — lodging/food are LocalBusiness subtypes (need name+address); places/trips need a name.
53
+ Hotel: ['name', 'address'],
54
+ LodgingBusiness: ['name', 'address'],
55
+ Resort: ['name', 'address'],
56
+ BedAndBreakfast: ['name', 'address'],
57
+ Restaurant: ['name', 'address'],
58
+ TouristAttraction: ['name'],
59
+ TouristDestination: ['name'],
60
+ Trip: ['name'],
61
+ TouristTrip: ['name'],
62
+ };
63
+ // One-of required: at least one of the group must be present. Google treats these as "any of"
64
+ // (a remote JobPosting uses jobLocationType instead of jobLocation; a Product can qualify via
65
+ // review/aggregateRating instead of offers) — so flatly requiring one member is a false positive.
66
+ const ONE_OF = {
67
+ Product: [['offers', 'review', 'aggregateRating']],
68
+ JobPosting: [['jobLocation', 'jobLocationType', 'applicantLocationRequirements']],
69
+ };
70
+ // Nested required: e.g. Product.offers must carry price + priceCurrency. Only checked when the
71
+ // parent field is present (so a SoftwareApplication relying on aggregateRating instead of offers
72
+ // isn't flagged for a missing offers.price, and a remote JobPosting isn't flagged for jobLocation).
73
+ const NESTED_REQUIRED = {
74
+ Product: { field: 'offers', sub: ['price', 'priceCurrency'] },
75
+ SoftwareApplication: { field: 'offers', sub: ['price', 'priceCurrency'] },
76
+ Review: { field: 'reviewRating', sub: ['ratingValue'] },
77
+ JobPosting: { field: 'jobLocation', sub: ['address'] },
78
+ };
79
+ // Schemas Google has restricted — eligible only in narrow contexts (research/01 #119).
80
+ const FORBIDDEN = new Set(['FAQPage', 'HowTo']);
81
+ const URL_FIELDS = /^(image|logo|thumbnailurl|contenturl|url)$/i;
82
+ const DATE_FIELDS = /^(datepublished|datemodified|startdate|enddate|uploaddate|dateposted|validfrom)$/i;
83
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/;
84
+ const CURRENCY_FIELD = /^pricecurrency$/i;
85
+ const ISO_4217 = /^[A-Z]{3}$/; // e.g. USD, GBP, AUD
86
+ const PRICE_FIELD = /^price$/i;
87
+ const PLAIN_NUMBER = /^\d+(\.\d+)?$/; // Google requires a bare number — no "$", commas, or "Free"
88
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
89
+ /** Normalise @type (may be a string or array) to the first concrete type string. */
90
+ function typeOf(node) {
91
+ const t = node['@type'];
92
+ if (typeof t === 'string')
93
+ return t;
94
+ if (Array.isArray(t) && typeof t[0] === 'string')
95
+ return t[0];
96
+ return null;
97
+ }
98
+ /** Flatten a parsed block into its constituent nodes (handles arrays + @graph). */
99
+ function nodesOf(parsed, out) {
100
+ if (Array.isArray(parsed)) {
101
+ for (const p of parsed)
102
+ nodesOf(p, out);
103
+ return;
104
+ }
105
+ if (!isObj(parsed))
106
+ return;
107
+ if (Array.isArray(parsed['@graph'])) {
108
+ nodesOf(parsed['@graph'], out);
109
+ return;
110
+ }
111
+ out.push(parsed);
112
+ }
113
+ function urlIsAbsolute(v) {
114
+ if (typeof v === 'string')
115
+ return /^https?:\/\//i.test(v);
116
+ if (isObj(v))
117
+ return urlIsAbsolute(v.url ?? v['@id']); // ImageObject etc.
118
+ if (Array.isArray(v))
119
+ return v.every(urlIsAbsolute);
120
+ return true; // not a URL-bearing shape we can judge → don't flag
121
+ }
122
+ // Value sanity on a node's present fields: absolute URLs (#120), ISO-8601 dates (#121),
123
+ // ISO-4217 currency, and bare-number prices. Run on the node AND on nested offer objects
124
+ // (price/priceCurrency live inside offers, not at the top level).
125
+ function checkValues(obj, type, issues) {
126
+ for (const [k, v] of Object.entries(obj)) {
127
+ if (URL_FIELDS.test(k) && v != null && !urlIsAbsolute(v)) {
128
+ issues.push({ kind: 'value', type, detail: `${k} is not an absolute URL.`, fields: [k] });
129
+ }
130
+ if (DATE_FIELDS.test(k) && typeof v === 'string' && !ISO_DATE.test(v)) {
131
+ issues.push({ kind: 'value', type, detail: `${k} is not ISO-8601 ("${v}").`, fields: [k] });
132
+ }
133
+ if (CURRENCY_FIELD.test(k) && typeof v === 'string' && !ISO_4217.test(v)) {
134
+ issues.push({ kind: 'value', type, detail: `priceCurrency "${v}" is not a 3-letter ISO-4217 code (e.g. USD).`, fields: ['priceCurrency'] });
135
+ }
136
+ if (PRICE_FIELD.test(k) && v != null && typeof v !== 'number' && !(typeof v === 'string' && PLAIN_NUMBER.test(v))) {
137
+ issues.push({ kind: 'value', type, detail: `price "${String(v)}" is not a plain number (no currency symbols or thousands separators).`, fields: ['price'] });
138
+ }
139
+ }
140
+ }
141
+ function validateNode(node, issues) {
142
+ const type = typeOf(node);
143
+ if (!type) {
144
+ issues.push({ kind: 'type', detail: 'Node has no @type.' });
145
+ return;
146
+ }
147
+ if (FORBIDDEN.has(type)) {
148
+ issues.push({ kind: 'forbidden', type, detail: `${type} rich results are restricted to narrow contexts — likely ineligible on this page.` });
149
+ }
150
+ const required = REQUIRED[type];
151
+ if (required) {
152
+ const missing = required.filter(f => node[f] === undefined || node[f] === null || node[f] === '');
153
+ if (missing.length)
154
+ issues.push({ kind: 'required', type, detail: `${type} missing required field(s): ${missing.join(', ')}.`, fields: missing });
155
+ const nested = NESTED_REQUIRED[type];
156
+ if (nested && node[nested.field] !== undefined) {
157
+ const offers = Array.isArray(node[nested.field]) ? node[nested.field] : [node[nested.field]];
158
+ for (const off of offers) {
159
+ if (!isObj(off))
160
+ continue;
161
+ const missSub = nested.sub.filter(f => off[f] === undefined || off[f] === null || off[f] === '');
162
+ if (missSub.length)
163
+ issues.push({ kind: 'required', type, detail: `${type}.${nested.field} missing: ${missSub.join(', ')}.`, fields: missSub.map(f => `${nested.field}.${f}`) });
164
+ checkValues(off, type, issues); // price/priceCurrency value sanity inside offers
165
+ }
166
+ }
167
+ }
168
+ for (const group of ONE_OF[type] ?? []) {
169
+ if (!group.some(f => node[f] !== undefined && node[f] !== null && node[f] !== '')) {
170
+ issues.push({ kind: 'required', type, detail: `${type} needs at least one of: ${group.join(', ')}.`, fields: group });
171
+ }
172
+ }
173
+ checkValues(node, type, issues);
174
+ }
175
+ /** Does this node fully satisfy its type's required (incl. nested) fields? */
176
+ function fullyComplete(node, type) {
177
+ const required = REQUIRED[type];
178
+ if (!required)
179
+ return false;
180
+ if (!required.every(f => node[f] !== undefined && node[f] !== null && node[f] !== ''))
181
+ return false;
182
+ for (const group of ONE_OF[type] ?? []) {
183
+ if (!group.some(f => node[f] !== undefined && node[f] !== null && node[f] !== ''))
184
+ return false;
185
+ }
186
+ const nested = NESTED_REQUIRED[type];
187
+ if (nested) {
188
+ const fv = node[nested.field];
189
+ if (fv === undefined || fv === null)
190
+ return false;
191
+ const offers = Array.isArray(fv) ? fv : [fv];
192
+ if (!offers.length)
193
+ return false;
194
+ for (const off of offers) {
195
+ if (!isObj(off) || !nested.sub.every(s => off[s] !== undefined && off[s] !== null && off[s] !== ''))
196
+ return false;
197
+ }
198
+ }
199
+ return true;
200
+ }
201
+ /** Coerce a schema numeric field (number or numeric string) to a number, else undefined. */
202
+ function asNum(v) {
203
+ if (typeof v === 'number')
204
+ return v;
205
+ if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v.trim()))
206
+ return parseFloat(v);
207
+ return undefined;
208
+ }
209
+ // Rating-scale sanity + CROSS-NODE consistency. A page's AggregateRating and its Review(s) must use
210
+ // the same scale, and ratingValue must sit within [worstRating, bestRating]. Catches the real bug a
211
+ // per-node check misses: an AggregateRating on a 0–5 scale (worstRating:0) alongside a Review added
212
+ // on the default 1–5 scale — Google reads them as contradictory. Defaults: worst 1, best 5.
213
+ function checkRatingScales(allNodes, issues) {
214
+ const descs = [];
215
+ for (const n of allNodes) {
216
+ const t = typeOf(n);
217
+ if (t === 'AggregateRating')
218
+ descs.push({ src: 'AggregateRating', value: asNum(n.ratingValue), best: asNum(n.bestRating), worst: asNum(n.worstRating) });
219
+ if (t === 'Review' && isObj(n.reviewRating)) {
220
+ const rr = n.reviewRating;
221
+ descs.push({ src: 'Review.reviewRating', value: asNum(rr.ratingValue), best: asNum(rr.bestRating), worst: asNum(rr.worstRating) });
222
+ }
223
+ }
224
+ if (!descs.length)
225
+ return;
226
+ for (const d of descs) {
227
+ const best = d.best ?? 5, worst = d.worst ?? 1;
228
+ if (d.best != null && d.worst != null && worst > best) {
229
+ issues.push({ kind: 'value', type: d.src, detail: `worstRating (${worst}) is greater than bestRating (${best}).`, fields: ['worstRating', 'bestRating'] });
230
+ }
231
+ if (d.value != null && (d.value < worst || d.value > best)) {
232
+ issues.push({ kind: 'value', type: d.src, detail: `ratingValue ${d.value} is outside its ${worst}–${best} scale.`, fields: ['ratingValue'] });
233
+ }
234
+ }
235
+ // Cross-node: only among nodes that explicitly declare a scale (an implicit 1–5 isn't a "choice").
236
+ const scales = new Set(descs.filter(d => d.best != null || d.worst != null).map(d => `${d.worst ?? 1}–${d.best ?? 5}`));
237
+ if (scales.size > 1) {
238
+ issues.push({ kind: 'value', detail: `Inconsistent rating scales across nodes (${[...scales].join(', ')}) — AggregateRating and Review must share the same worstRating/bestRating.`, fields: ['bestRating', 'worstRating'] });
239
+ }
240
+ }
241
+ /** Validate the raw `pages.json_ld` column. Returns all issues found across blocks. */
242
+ export function validateJsonLdColumn(col) {
243
+ if (!col)
244
+ return [];
245
+ const issues = [];
246
+ let blocks;
247
+ try {
248
+ blocks = JSON.parse(col);
249
+ }
250
+ catch {
251
+ return [{ kind: 'parse', detail: 'json_ld column is not valid JSON.' }];
252
+ }
253
+ const rawBlocks = Array.isArray(blocks) ? blocks.filter((b) => typeof b === 'string') : [];
254
+ const allNodes = [];
255
+ for (const raw of rawBlocks) {
256
+ let parsed;
257
+ try {
258
+ parsed = JSON.parse(raw);
259
+ }
260
+ catch {
261
+ issues.push({ kind: 'parse', detail: 'A JSON-LD block does not parse (check trailing commas / escaped quotes).' });
262
+ continue;
263
+ }
264
+ const nodes = [];
265
+ nodesOf(parsed, nodes);
266
+ allNodes.push(...nodes);
267
+ for (const node of nodes) {
268
+ const ctx = node['@context'];
269
+ const ctxOk = typeof ctx === 'string' ? /schema\.org/i.test(ctx) : Array.isArray(ctx) ? ctx.some(c => typeof c === 'string' && /schema\.org/i.test(c)) : isObj(ctx);
270
+ // @context lives on the top-level node; nested @graph nodes inherit it — only flag when absent everywhere.
271
+ if (ctx !== undefined && !ctxOk)
272
+ issues.push({ kind: 'context', detail: '@context is not https://schema.org.' });
273
+ validateNode(node, issues);
274
+ }
275
+ // A block whose top node carries no @context at all.
276
+ if (isObj(parsed) && parsed['@context'] === undefined && !Array.isArray(parsed['@graph'])) {
277
+ issues.push({ kind: 'context', detail: 'JSON-LD block has no @context.' });
278
+ }
279
+ }
280
+ // Type-level dedup: if the page has at least one FULLY-complete node of a type, don't flag a
281
+ // sibling incomplete node of that type for "missing required fields" — Google uses the valid
282
+ // one. (A common WP/RankMath pattern: a complete @graph Article + a second partial Article for
283
+ // entity SEO.) Only suppresses 'required'; parse/context/value/forbidden issues always stand.
284
+ checkRatingScales(allNodes, issues); // cross-node rating-scale consistency (page-level)
285
+ const satisfied = new Set();
286
+ for (const n of allNodes) {
287
+ const t = typeOf(n);
288
+ if (t && fullyComplete(n, t))
289
+ satisfied.add(t);
290
+ }
291
+ return satisfied.size ? issues.filter(i => !(i.kind === 'required' && i.type && satisfied.has(i.type))) : issues;
292
+ }
293
+ //# sourceMappingURL=schema-validate.js.map