@pseolint/core 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/dist/ai/prompt.d.ts +1 -1
- package/dist/ai/prompt.d.ts.map +1 -1
- package/dist/ai/prompt.js +13 -1
- package/dist/ai/prompt.js.map +1 -1
- package/dist/ai/triage.d.ts +15 -1
- package/dist/ai/triage.d.ts.map +1 -1
- package/dist/ai/triage.js +30 -0
- package/dist/ai/triage.js.map +1 -1
- package/dist/analytics-blocklist.d.ts +28 -0
- package/dist/analytics-blocklist.d.ts.map +1 -0
- package/dist/analytics-blocklist.js +129 -0
- package/dist/analytics-blocklist.js.map +1 -0
- package/dist/auditor.d.ts.map +1 -1
- package/dist/auditor.js +130 -46
- package/dist/auditor.js.map +1 -1
- package/dist/formatters/console.d.ts +9 -0
- package/dist/formatters/console.d.ts.map +1 -1
- package/dist/formatters/console.js +53 -0
- package/dist/formatters/console.js.map +1 -1
- package/dist/formatters/html.d.ts.map +1 -1
- package/dist/formatters/html.js +557 -144
- package/dist/formatters/html.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +14 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +130 -4
- package/dist/renderer.js.map +1 -1
- package/dist/rule-references.d.ts.map +1 -1
- package/dist/rule-references.js +9 -0
- package/dist/rule-references.js.map +1 -1
- package/dist/rules/aeo/answer-first.d.ts +18 -0
- package/dist/rules/aeo/answer-first.d.ts.map +1 -0
- package/dist/rules/aeo/answer-first.js +191 -0
- package/dist/rules/aeo/answer-first.js.map +1 -0
- package/dist/rules/aeo/citable-facts.d.ts +9 -0
- package/dist/rules/aeo/citable-facts.d.ts.map +1 -0
- package/dist/rules/aeo/citable-facts.js +90 -0
- package/dist/rules/aeo/citable-facts.js.map +1 -0
- package/dist/rules/aeo/content-modularity.d.ts +11 -0
- package/dist/rules/aeo/content-modularity.d.ts.map +1 -0
- package/dist/rules/aeo/content-modularity.js +107 -0
- package/dist/rules/aeo/content-modularity.js.map +1 -0
- package/dist/rules/aeo/crawler-access.d.ts +25 -0
- package/dist/rules/aeo/crawler-access.d.ts.map +1 -0
- package/dist/rules/aeo/crawler-access.js +116 -0
- package/dist/rules/aeo/crawler-access.js.map +1 -0
- package/dist/rules/aeo/faq-coverage.d.ts +9 -0
- package/dist/rules/aeo/faq-coverage.d.ts.map +1 -0
- package/dist/rules/aeo/faq-coverage.js +71 -0
- package/dist/rules/aeo/faq-coverage.js.map +1 -0
- package/dist/rules/aeo/freshness-signals.d.ts +9 -0
- package/dist/rules/aeo/freshness-signals.d.ts.map +1 -0
- package/dist/rules/aeo/freshness-signals.js +109 -0
- package/dist/rules/aeo/freshness-signals.js.map +1 -0
- package/dist/rules/aeo/llms-txt.d.ts +24 -0
- package/dist/rules/aeo/llms-txt.d.ts.map +1 -0
- package/dist/rules/aeo/llms-txt.js +93 -0
- package/dist/rules/aeo/llms-txt.js.map +1 -0
- package/dist/rules/aeo/non-replicable-value.d.ts +9 -0
- package/dist/rules/aeo/non-replicable-value.d.ts.map +1 -0
- package/dist/rules/aeo/non-replicable-value.js +95 -0
- package/dist/rules/aeo/non-replicable-value.js.map +1 -0
- package/dist/rules/aeo/summary-bait.d.ts +20 -0
- package/dist/rules/aeo/summary-bait.d.ts.map +1 -0
- package/dist/rules/aeo/summary-bait.js +147 -0
- package/dist/rules/aeo/summary-bait.js.map +1 -0
- package/dist/rules/scope.d.ts +12 -0
- package/dist/rules/scope.d.ts.map +1 -0
- package/dist/rules/scope.js +67 -0
- package/dist/rules/scope.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/formatters/html.js
CHANGED
|
@@ -1,24 +1,59 @@
|
|
|
1
1
|
const SEVERITY_ORDER = ["critical", "error", "warning", "info"];
|
|
2
|
-
|
|
2
|
+
const SEVERITY_LABEL = {
|
|
3
|
+
critical: "Critical",
|
|
4
|
+
error: "Error",
|
|
5
|
+
warning: "Warning",
|
|
6
|
+
info: "Info",
|
|
7
|
+
};
|
|
8
|
+
const SEVERITY_WEIGHT = {
|
|
9
|
+
critical: 100,
|
|
10
|
+
error: 50,
|
|
11
|
+
warning: 10,
|
|
12
|
+
info: 1,
|
|
13
|
+
};
|
|
14
|
+
function severityTone(severity) {
|
|
3
15
|
switch (severity) {
|
|
4
16
|
case "critical":
|
|
5
|
-
return "#dc2626";
|
|
6
17
|
case "error":
|
|
7
|
-
return "
|
|
18
|
+
return "destructive";
|
|
8
19
|
case "warning":
|
|
9
|
-
return "
|
|
20
|
+
return "warning";
|
|
10
21
|
case "info":
|
|
11
|
-
return "
|
|
22
|
+
return "muted";
|
|
12
23
|
}
|
|
13
24
|
}
|
|
14
|
-
function
|
|
25
|
+
function scoreTone(score) {
|
|
26
|
+
if (score <= 40)
|
|
27
|
+
return "success";
|
|
28
|
+
if (score <= 69)
|
|
29
|
+
return "warning";
|
|
30
|
+
return "destructive";
|
|
31
|
+
}
|
|
32
|
+
function scoreVerdict(score) {
|
|
15
33
|
if (score <= 20)
|
|
16
|
-
return "
|
|
34
|
+
return "Clean run";
|
|
17
35
|
if (score <= 40)
|
|
18
|
-
return "
|
|
19
|
-
if (score <=
|
|
20
|
-
return "
|
|
21
|
-
|
|
36
|
+
return "Low risk";
|
|
37
|
+
if (score <= 69)
|
|
38
|
+
return "Watch list";
|
|
39
|
+
if (score <= 84)
|
|
40
|
+
return "Elevated risk";
|
|
41
|
+
return "Doorway garden";
|
|
42
|
+
}
|
|
43
|
+
function categoryTone(pct) {
|
|
44
|
+
if (pct <= 40)
|
|
45
|
+
return "success";
|
|
46
|
+
if (pct <= 69)
|
|
47
|
+
return "warning";
|
|
48
|
+
return "destructive";
|
|
49
|
+
}
|
|
50
|
+
function effortLabel(effort) {
|
|
51
|
+
switch (effort) {
|
|
52
|
+
case "quick": return "quick fix";
|
|
53
|
+
case "moderate": return "moderate";
|
|
54
|
+
case "structural": return "structural";
|
|
55
|
+
default: return effort;
|
|
56
|
+
}
|
|
22
57
|
}
|
|
23
58
|
function escapeHtml(text) {
|
|
24
59
|
return text
|
|
@@ -27,14 +62,6 @@ function escapeHtml(text) {
|
|
|
27
62
|
.replace(/>/g, ">")
|
|
28
63
|
.replace(/"/g, """);
|
|
29
64
|
}
|
|
30
|
-
function effortColor(effort) {
|
|
31
|
-
switch (effort) {
|
|
32
|
-
case "quick": return "#16a34a";
|
|
33
|
-
case "moderate": return "#ca8a04";
|
|
34
|
-
case "structural": return "#dc2626";
|
|
35
|
-
default: return "#64748b";
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
65
|
function shortenUrl(url) {
|
|
39
66
|
try {
|
|
40
67
|
return new URL(url).pathname;
|
|
@@ -43,150 +70,536 @@ function shortenUrl(url) {
|
|
|
43
70
|
return url;
|
|
44
71
|
}
|
|
45
72
|
}
|
|
73
|
+
function categoryLabel(name) {
|
|
74
|
+
const map = {
|
|
75
|
+
spam: "Spam signals",
|
|
76
|
+
content: "Content quality",
|
|
77
|
+
links: "Link graph",
|
|
78
|
+
tech: "Technical SEO",
|
|
79
|
+
data: "Data binding",
|
|
80
|
+
schema: "Structured data",
|
|
81
|
+
cannibal: "Cannibalisation",
|
|
82
|
+
aeo: "AEO readiness",
|
|
83
|
+
};
|
|
84
|
+
return map[name] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* AEO-specific score band label. Low score = AI-Ready (site is citable by
|
|
88
|
+
* LLMs in AI Overviews / answer engines); high score = Ghost (not discoverable).
|
|
89
|
+
*/
|
|
90
|
+
function aeoScoreLabel(score) {
|
|
91
|
+
if (score <= 20)
|
|
92
|
+
return "AI-Ready";
|
|
93
|
+
if (score <= 40)
|
|
94
|
+
return "Partial";
|
|
95
|
+
if (score <= 60)
|
|
96
|
+
return "Vulnerable";
|
|
97
|
+
if (score <= 80)
|
|
98
|
+
return "Invisible";
|
|
99
|
+
return "Ghost";
|
|
100
|
+
}
|
|
101
|
+
/** Group findings by ruleId. Preserves encounter order per bucket. */
|
|
102
|
+
function groupByRule(findings) {
|
|
103
|
+
const m = new Map();
|
|
104
|
+
for (const f of findings) {
|
|
105
|
+
const bucket = m.get(f.ruleId);
|
|
106
|
+
if (bucket)
|
|
107
|
+
bucket.push(f);
|
|
108
|
+
else
|
|
109
|
+
m.set(f.ruleId, [f]);
|
|
110
|
+
}
|
|
111
|
+
return m;
|
|
112
|
+
}
|
|
113
|
+
function rankByImpact(findings) {
|
|
114
|
+
const byRule = groupByRule(findings);
|
|
115
|
+
const impacts = [];
|
|
116
|
+
for (const [ruleId, items] of byRule) {
|
|
117
|
+
const sev = items[0].severity;
|
|
118
|
+
impacts.push({
|
|
119
|
+
ruleId,
|
|
120
|
+
severity: sev,
|
|
121
|
+
pageCount: items.length,
|
|
122
|
+
representative: items[0],
|
|
123
|
+
impact: SEVERITY_WEIGHT[sev] * items.length,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
impacts.sort((a, b) => {
|
|
127
|
+
if (b.impact !== a.impact)
|
|
128
|
+
return b.impact - a.impact;
|
|
129
|
+
const sa = SEVERITY_ORDER.indexOf(a.severity);
|
|
130
|
+
const sb = SEVERITY_ORDER.indexOf(b.severity);
|
|
131
|
+
if (sa !== sb)
|
|
132
|
+
return sa - sb;
|
|
133
|
+
if (b.pageCount !== a.pageCount)
|
|
134
|
+
return b.pageCount - a.pageCount;
|
|
135
|
+
return a.ruleId.localeCompare(b.ruleId);
|
|
136
|
+
});
|
|
137
|
+
return impacts;
|
|
138
|
+
}
|
|
46
139
|
function renderTriageHtml(triage) {
|
|
47
140
|
const sorted = triage.rootCauses.slice().sort((a, b) => a.fixOrder - b.fixOrder);
|
|
48
|
-
const cost = triage.estimatedCostUsd !== undefined ?
|
|
49
|
-
const cacheLabel = triage.cacheHit ? "cached" : "
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
141
|
+
const cost = triage.estimatedCostUsd !== undefined ? ` · est $${triage.estimatedCostUsd.toFixed(2)}` : "";
|
|
142
|
+
const cacheLabel = triage.cacheHit ? "cached" : "fresh";
|
|
143
|
+
const tokens = `${triage.tokenUsage.input.toLocaleString()} in / ${triage.tokenUsage.output.toLocaleString()} out`;
|
|
144
|
+
const causes = sorted.map((c) => `
|
|
145
|
+
<li class="cause">
|
|
146
|
+
<div class="cause-head">
|
|
147
|
+
<span class="cause-order">${c.fixOrder}</span>
|
|
148
|
+
<h3>${escapeHtml(c.label)}</h3>
|
|
149
|
+
<span class="sev sev-${severityTone(c.severity)}">${escapeHtml(c.severity)}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<p class="cause-meta">${c.findingsCount} findings · ${c.affectedRuleIds.map(escapeHtml).join(", ")}</p>
|
|
152
|
+
<p class="cause-body">${escapeHtml(c.rationale)}</p>
|
|
153
|
+
</li>`).join("");
|
|
154
|
+
return `
|
|
155
|
+
<section class="ai-triage">
|
|
156
|
+
<header class="section-head">
|
|
157
|
+
<span class="eyebrow">AI Triage</span>
|
|
158
|
+
<span class="meta-mono">${escapeHtml(triage.modelUsed)} · ${cacheLabel} · ${tokens}${cost}</span>
|
|
159
|
+
</header>
|
|
160
|
+
${triage.narrative ? `<p class="narrative">${escapeHtml(triage.narrative)}</p>` : ""}
|
|
161
|
+
<ol class="causes">${causes}</ol>
|
|
162
|
+
</section>`;
|
|
163
|
+
}
|
|
164
|
+
function renderTopFixes(impacts) {
|
|
165
|
+
if (impacts.length === 0)
|
|
166
|
+
return "";
|
|
167
|
+
const top = impacts.slice(0, 5);
|
|
168
|
+
const rows = top.map((imp, idx) => {
|
|
169
|
+
const r = imp.representative;
|
|
170
|
+
const effortPill = r.effort
|
|
171
|
+
? `<span class="effort-pill effort-${escapeHtml(r.effort)}">${escapeHtml(effortLabel(r.effort))}</span>`
|
|
172
|
+
: "";
|
|
173
|
+
const pagesLabel = imp.pageCount === 1 ? "1 page" : `${imp.pageCount} pages`;
|
|
174
|
+
return `<li class="top-fix">
|
|
175
|
+
<span class="top-fix-rank">${idx + 1}</span>
|
|
176
|
+
<div class="top-fix-body">
|
|
177
|
+
<div class="top-fix-head">
|
|
178
|
+
<code class="rule-id">${escapeHtml(imp.ruleId)}</code>
|
|
179
|
+
<span class="sev sev-${severityTone(imp.severity)}">${escapeHtml(imp.severity)}</span>
|
|
180
|
+
${effortPill}
|
|
181
|
+
<span class="top-fix-pages mono">${pagesLabel}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<p class="top-fix-msg">${escapeHtml(r.message)}</p>
|
|
184
|
+
</div>
|
|
185
|
+
</li>`;
|
|
186
|
+
}).join("");
|
|
187
|
+
return `
|
|
188
|
+
<section class="top-fixes">
|
|
189
|
+
<header class="section-head">
|
|
190
|
+
<span class="eyebrow">Top fixes by impact</span>
|
|
191
|
+
<span class="meta-mono">severity × pages affected</span>
|
|
192
|
+
</header>
|
|
193
|
+
<ol class="top-fix-list">${rows}</ol>
|
|
64
194
|
</section>`;
|
|
65
195
|
}
|
|
196
|
+
function renderRuleCard(ruleId, items, sev) {
|
|
197
|
+
const representative = items[0];
|
|
198
|
+
const effortPill = representative.effort
|
|
199
|
+
? ` <span class="effort-pill effort-${escapeHtml(representative.effort)}">${escapeHtml(effortLabel(representative.effort))}</span>`
|
|
200
|
+
: "";
|
|
201
|
+
const pagesLabel = items.length === 1 ? "1 page" : `${items.length} pages`;
|
|
202
|
+
// Sample + full URL list — only include findings that have a pageUrl.
|
|
203
|
+
const urls = items.map((i) => i.pageUrl).filter((u) => !!u);
|
|
204
|
+
let pageList = "";
|
|
205
|
+
if (urls.length > 0) {
|
|
206
|
+
const preview = urls.slice(0, 3);
|
|
207
|
+
const rest = urls.slice(3);
|
|
208
|
+
const previewHtml = `<ul class="page-preview">${preview.map((u) => `<li class="mono">${escapeHtml(shortenUrl(u))}</li>`).join("")}</ul>`;
|
|
209
|
+
const restHtml = rest.length > 0
|
|
210
|
+
? `<details class="page-extra">
|
|
211
|
+
<summary>${rest.length} more ${rest.length === 1 ? "page" : "pages"}</summary>
|
|
212
|
+
<ul class="page-list">${rest.map((u) => `<li class="mono">${escapeHtml(shortenUrl(u))}</li>`).join("")}</ul>
|
|
213
|
+
</details>`
|
|
214
|
+
: "";
|
|
215
|
+
pageList = previewHtml + restHtml;
|
|
216
|
+
}
|
|
217
|
+
// Cluster context (if any finding has it, show the first one — site-wide rules only fire once).
|
|
218
|
+
let cluster = "";
|
|
219
|
+
const withCluster = items.find((i) => i.context?.type === "cluster");
|
|
220
|
+
if (withCluster && withCluster.context?.type === "cluster") {
|
|
221
|
+
const ctx = withCluster.context;
|
|
222
|
+
const [minSim, maxSim] = ctx.similarityRange;
|
|
223
|
+
const worstPairsHtml = ctx.worstPairs
|
|
224
|
+
.map(p => `<li><span class="mono">${escapeHtml(shortenUrl(p.left))}</span> <span class="arrow">↔</span> <span class="mono">${escapeHtml(shortenUrl(p.right))}</span> <span class="sim">${(p.similarity * 100).toFixed(1)}%</span></li>`)
|
|
225
|
+
.join("");
|
|
226
|
+
const membersHtml = ctx.members
|
|
227
|
+
.map(m => `<li class="mono">${escapeHtml(shortenUrl(m))}</li>`)
|
|
228
|
+
.join("");
|
|
229
|
+
cluster = `
|
|
230
|
+
<details>
|
|
231
|
+
<summary>${ctx.clusterSize} pages in cluster · ${(minSim * 100).toFixed(0)}–${(maxSim * 100).toFixed(0)}% similar</summary>
|
|
232
|
+
<div class="cluster-body">
|
|
233
|
+
<p class="cluster-label">Worst pairs</p>
|
|
234
|
+
<ul class="pair-list">${worstPairsHtml}</ul>
|
|
235
|
+
<p class="cluster-label">All members</p>
|
|
236
|
+
<ul class="member-list">${membersHtml}</ul>
|
|
237
|
+
</div>
|
|
238
|
+
</details>`;
|
|
239
|
+
}
|
|
240
|
+
const fix = representative.fix ? `<div class="fix"><span class="fix-label">Fix</span>${escapeHtml(representative.fix)}</div>` : "";
|
|
241
|
+
const ref = representative.ref ? `<a href="${escapeHtml(representative.ref)}" class="ref" target="_blank" rel="noopener">ref ↗</a>` : "";
|
|
242
|
+
return `<li class="finding finding-${severityTone(sev)}">
|
|
243
|
+
<div class="finding-head">
|
|
244
|
+
<code class="rule-id">${escapeHtml(ruleId)}</code>${effortPill}
|
|
245
|
+
<span class="finding-pages mono">${pagesLabel}</span>
|
|
246
|
+
</div>
|
|
247
|
+
<p class="finding-msg">${escapeHtml(representative.message)}</p>
|
|
248
|
+
${pageList}
|
|
249
|
+
${cluster}
|
|
250
|
+
${fix}
|
|
251
|
+
${ref}
|
|
252
|
+
</li>`;
|
|
253
|
+
}
|
|
66
254
|
export function formatHtml(summary) {
|
|
67
255
|
const grouped = new Map();
|
|
68
|
-
for (const sev of SEVERITY_ORDER)
|
|
256
|
+
for (const sev of SEVERITY_ORDER)
|
|
69
257
|
grouped.set(sev, []);
|
|
70
|
-
|
|
71
|
-
for (const f of summary.findings) {
|
|
258
|
+
for (const f of summary.findings)
|
|
72
259
|
grouped.get(f.severity).push(f);
|
|
73
|
-
|
|
260
|
+
const counts = {
|
|
261
|
+
critical: grouped.get("critical").length,
|
|
262
|
+
error: grouped.get("error").length,
|
|
263
|
+
warning: grouped.get("warning").length,
|
|
264
|
+
info: grouped.get("info").length,
|
|
265
|
+
};
|
|
266
|
+
const totalErrors = counts.critical + counts.error;
|
|
267
|
+
const impacts = rankByImpact(summary.findings);
|
|
268
|
+
const topFixesHtml = renderTopFixes(impacts);
|
|
74
269
|
const categoryRows = Object.entries(summary.categoryScores)
|
|
75
270
|
.map(([name, value]) => {
|
|
76
|
-
const label = name.charAt(0).toUpperCase() + name.slice(1);
|
|
77
271
|
const pct = value;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<td>${pct}</td>
|
|
272
|
+
const clean = Math.max(0, 100 - pct);
|
|
273
|
+
const tone = categoryTone(pct);
|
|
274
|
+
return `<tr>
|
|
275
|
+
<td class="cat-name">${escapeHtml(categoryLabel(name))}</td>
|
|
276
|
+
<td class="cat-bar"><div class="bar-bg"><div class="bar-fill bar-${tone}" style="width:${clean}%"></div></div></td>
|
|
277
|
+
<td class="cat-val mono tabular">${pct}</td>
|
|
84
278
|
</tr>`;
|
|
85
279
|
})
|
|
86
|
-
.join("
|
|
280
|
+
.join("");
|
|
87
281
|
const findingsSections = SEVERITY_ORDER.map((sev) => {
|
|
88
282
|
const items = grouped.get(sev);
|
|
89
283
|
if (items.length === 0)
|
|
90
284
|
return "";
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (item.context?.type === "cluster") {
|
|
99
|
-
const ctx = item.context;
|
|
100
|
-
const [minSim, maxSim] = ctx.similarityRange;
|
|
101
|
-
const worstPairsHtml = ctx.worstPairs
|
|
102
|
-
.map(p => `<li>${escapeHtml(shortenUrl(p.left))} ↔ ${escapeHtml(shortenUrl(p.right))} (${(p.similarity * 100).toFixed(1)}%)</li>`)
|
|
103
|
-
.join("\n");
|
|
104
|
-
const membersHtml = ctx.members
|
|
105
|
-
.map(m => `<li>${escapeHtml(shortenUrl(m))}</li>`)
|
|
106
|
-
.join("\n");
|
|
107
|
-
li += `
|
|
108
|
-
<details>
|
|
109
|
-
<summary>${ctx.clusterSize} pages in cluster (${(minSim * 100).toFixed(0)}–${(maxSim * 100).toFixed(0)}% similar)</summary>
|
|
110
|
-
<div class="cluster-details">
|
|
111
|
-
<strong>Worst pairs:</strong>
|
|
112
|
-
<ul>${worstPairsHtml}</ul>
|
|
113
|
-
<strong>All members:</strong>
|
|
114
|
-
<ul class="member-list">${membersHtml}</ul>
|
|
115
|
-
</div>
|
|
116
|
-
</details>`;
|
|
117
|
-
}
|
|
118
|
-
if (item.fix) {
|
|
119
|
-
li += `<div class="fix">Fix: ${escapeHtml(item.fix)}</div>`;
|
|
120
|
-
}
|
|
121
|
-
if (item.ref) {
|
|
122
|
-
li += ` <a href="${escapeHtml(item.ref)}" class="ref" target="_blank">Ref</a>`;
|
|
123
|
-
}
|
|
124
|
-
li += `</li>`;
|
|
125
|
-
return li;
|
|
285
|
+
const byRule = groupByRule(items);
|
|
286
|
+
// Preserve rule-level ordering by rank (severity × pageCount) within the severity group.
|
|
287
|
+
const orderedRuleIds = Array.from(byRule.entries())
|
|
288
|
+
.sort((a, b) => {
|
|
289
|
+
if (b[1].length !== a[1].length)
|
|
290
|
+
return b[1].length - a[1].length;
|
|
291
|
+
return a[0].localeCompare(b[0]);
|
|
126
292
|
})
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.bar-bg{background:#e2e8f0;border-radius:4px;height:14px;width:100%}
|
|
150
|
-
.bar-fill{height:100%;border-radius:4px;transition:width .3s}
|
|
151
|
-
ul{list-style:disc;padding-left:1.5rem;margin-bottom:.5rem}
|
|
152
|
-
li{margin:.2rem 0}
|
|
153
|
-
.fix{color:#64748b;font-size:.9em;margin-top:.2rem}
|
|
154
|
-
.ref{color:#2563eb;font-size:.85em}
|
|
155
|
-
.effort-pill{display:inline-block;padding:.1rem .4rem;border-radius:9999px;color:white;font-size:.75em;font-weight:600;vertical-align:middle}
|
|
156
|
-
.template-banner{background:#fef3c7;border:1px solid #f59e0b;border-radius:6px;padding:.5rem 1rem;margin:.5rem 0;color:#92400e;font-size:.9em}
|
|
157
|
-
details{margin:.25rem 0}
|
|
158
|
-
summary{cursor:pointer;color:#2563eb;font-size:.9em}
|
|
159
|
-
.cluster-details{padding:.5rem;background:#f1f5f9;border-radius:4px;margin:.25rem 0;font-size:.85em}
|
|
160
|
-
.cluster-details ul{margin:.25rem 0}
|
|
161
|
-
.member-list{max-height:200px;overflow-y:auto}
|
|
162
|
-
</style>
|
|
163
|
-
</head>
|
|
164
|
-
<body>
|
|
165
|
-
<h1>pSEOlint Audit Report</h1>
|
|
166
|
-
<p class="meta">Pages analysed: ${summary.pageCount}</p>
|
|
167
|
-
<p class="score" style="color:${scoreColor(summary.score)}">SpamBrain Risk Score: ${summary.score}/100</p>
|
|
168
|
-
${summary.templateDetected ? `<p class="template-banner">Template-generated content detected. Fix suggestions are tailored for template authors.</p>` : ""}
|
|
169
|
-
<h2>Category Scores</h2>
|
|
170
|
-
<table>
|
|
171
|
-
<thead><tr><th>Category</th><th>Bar</th><th>Score</th></tr></thead>
|
|
172
|
-
<tbody>${categoryRows}</tbody>
|
|
173
|
-
</table>
|
|
174
|
-
|
|
175
|
-
${summary.groupScores && summary.groupPageCounts ? `
|
|
176
|
-
<h2>Group Scores</h2>
|
|
177
|
-
<table>
|
|
178
|
-
<thead><tr><th>Group</th><th>Score</th><th>Pages</th></tr></thead>
|
|
179
|
-
<tbody>${Object.entries(summary.groupScores).map(([name, value]) => {
|
|
293
|
+
.map(([ruleId]) => ruleId);
|
|
294
|
+
const itemsHtml = orderedRuleIds.map((rid) => renderRuleCard(rid, byRule.get(rid), sev)).join("");
|
|
295
|
+
const ruleCount = byRule.size;
|
|
296
|
+
return `
|
|
297
|
+
<div class="sev-group">
|
|
298
|
+
<h3 class="sev-heading sev-${severityTone(sev)}">
|
|
299
|
+
<span class="sev-dot"></span>${SEVERITY_LABEL[sev]}
|
|
300
|
+
<span class="sev-count">${ruleCount} ${ruleCount === 1 ? "rule" : "rules"} · ${items.length} ${items.length === 1 ? "finding" : "findings"}</span>
|
|
301
|
+
</h3>
|
|
302
|
+
<ul class="finding-list">${itemsHtml}</ul>
|
|
303
|
+
</div>`;
|
|
304
|
+
}).join("");
|
|
305
|
+
const tone = scoreTone(summary.score);
|
|
306
|
+
const verdict = scoreVerdict(summary.score);
|
|
307
|
+
const groupScoresHtml = summary.groupScores && summary.groupPageCounts ? `
|
|
308
|
+
<section class="card">
|
|
309
|
+
<header class="section-head">
|
|
310
|
+
<span class="eyebrow">Group scores</span>
|
|
311
|
+
</header>
|
|
312
|
+
<table class="data-table">
|
|
313
|
+
<thead><tr><th>Group</th><th>Score</th><th>Pages</th></tr></thead>
|
|
314
|
+
<tbody>${Object.entries(summary.groupScores).map(([name, value]) => {
|
|
180
315
|
const count = summary.groupPageCounts[name] ?? 0;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
</
|
|
316
|
+
const gTone = categoryTone(value);
|
|
317
|
+
return `<tr>
|
|
318
|
+
<td>${escapeHtml(name)}</td>
|
|
319
|
+
<td class="mono tabular tone-${gTone}">${value}</td>
|
|
320
|
+
<td class="mono tabular muted">${count}</td>
|
|
321
|
+
</tr>`;
|
|
322
|
+
}).join("")}</tbody>
|
|
323
|
+
</table>
|
|
324
|
+
</section>` : "";
|
|
325
|
+
return `<!DOCTYPE html>
|
|
326
|
+
<html lang="en">
|
|
327
|
+
<head>
|
|
328
|
+
<meta charset="utf-8">
|
|
329
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
330
|
+
<title>pseolint · audit report</title>
|
|
331
|
+
<style>
|
|
332
|
+
:root{
|
|
333
|
+
--bg:#16181c; --fg:#eef2f6; --muted:#9aa4ae; --muted-2:#6a7380;
|
|
334
|
+
--card:#1a1d22; --card-2:#1f2329; --border:#262a30; --border-strong:#3a3f47;
|
|
335
|
+
--success:#39d19f; --warning:#fbb838; --destructive:#e94b4b; --primary:#39d19f;
|
|
336
|
+
--r:18px; --r-lg:28px;
|
|
337
|
+
}
|
|
338
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
339
|
+
html{color-scheme:dark}
|
|
340
|
+
body{
|
|
341
|
+
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
342
|
+
background:var(--bg); color:var(--fg);
|
|
343
|
+
font-feature-settings:"rlig" 1,"calt" 1;
|
|
344
|
+
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
|
345
|
+
line-height:1.5;
|
|
346
|
+
}
|
|
347
|
+
main{max-width:960px;margin:0 auto;padding:56px 24px 80px}
|
|
348
|
+
.mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
349
|
+
.tabular{font-variant-numeric:tabular-nums}
|
|
350
|
+
.display{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-weight:400;letter-spacing:-0.01em}
|
|
351
|
+
.muted{color:var(--muted)}
|
|
352
|
+
.meta-mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;color:var(--muted);font-size:12px}
|
|
353
|
+
.eyebrow{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
354
|
+
|
|
355
|
+
.status-row{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:11px;letter-spacing:0.14em;text-transform:uppercase}
|
|
356
|
+
.status-dot{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--success)}
|
|
357
|
+
.status-dot.tone-warning{background:var(--warning)} .status-dot.tone-destructive{background:var(--destructive)}
|
|
358
|
+
|
|
359
|
+
h1.title{margin-top:12px;font-size:clamp(32px,5vw,56px);line-height:1;letter-spacing:-0.01em}
|
|
360
|
+
.title-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px;margin-top:12px}
|
|
361
|
+
.src-link{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;text-decoration:none}
|
|
362
|
+
.src-link:hover{color:var(--fg)}
|
|
363
|
+
.lead{margin-top:12px;color:var(--muted);font-size:14px;max-width:640px}
|
|
364
|
+
|
|
365
|
+
.card{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
366
|
+
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
367
|
+
.hero{display:grid;grid-template-columns:auto 1fr;gap:36px;align-items:center}
|
|
368
|
+
@media (max-width:640px){.hero{grid-template-columns:1fr;gap:24px}}
|
|
369
|
+
.score-block{display:flex;flex-direction:column;align-items:flex-start;min-width:180px}
|
|
370
|
+
.score{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-weight:400;
|
|
371
|
+
font-size:128px;line-height:0.9;font-variant-numeric:tabular-nums;letter-spacing:-0.02em}
|
|
372
|
+
.tone-success{color:var(--success)} .tone-warning{color:var(--warning)} .tone-destructive{color:var(--destructive)}
|
|
373
|
+
.score-label{margin-top:6px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted)}
|
|
374
|
+
.verdict{margin-top:14px;display:inline-flex;align-items:center;gap:8px;
|
|
375
|
+
padding:5px 10px;border:1px solid var(--border-strong);border-radius:999px;
|
|
376
|
+
background:var(--card-2);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
377
|
+
font-size:11px;color:var(--muted)}
|
|
378
|
+
.verdict-dot{width:5px;height:5px;border-radius:999px;background:var(--success)}
|
|
379
|
+
|
|
380
|
+
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px 28px;margin-top:8px}
|
|
381
|
+
@media (max-width:640px){.stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
382
|
+
.stat-label{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted)}
|
|
383
|
+
.stat-val{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
384
|
+
font-size:20px;font-variant-numeric:tabular-nums;margin-top:2px}
|
|
385
|
+
|
|
386
|
+
.template-banner{margin-top:20px;padding:14px 18px;border:1px solid color-mix(in oklab,var(--warning) 40%,transparent);
|
|
387
|
+
border-radius:var(--r);background:color-mix(in oklab,var(--warning) 8%,transparent);color:var(--fg);font-size:13px}
|
|
388
|
+
.template-banner strong{color:var(--warning)}
|
|
389
|
+
|
|
390
|
+
.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:18px}
|
|
391
|
+
.data-table{width:100%;border-collapse:collapse}
|
|
392
|
+
.data-table th,.data-table td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);font-size:14px}
|
|
393
|
+
.data-table th{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
394
|
+
.data-table tr:last-child td{border-bottom:0}
|
|
395
|
+
.cat-name{width:40%}
|
|
396
|
+
.cat-bar{width:auto}
|
|
397
|
+
.cat-val{width:56px;text-align:right}
|
|
398
|
+
.bar-bg{background:var(--border);border-radius:999px;height:8px;width:100%;overflow:hidden}
|
|
399
|
+
.bar-fill{height:100%;border-radius:999px;transition:width .4s ease}
|
|
400
|
+
.bar-success{background:var(--success)} .bar-warning{background:var(--warning)} .bar-destructive{background:var(--destructive)}
|
|
401
|
+
|
|
402
|
+
.aeo-callout{margin-top:20px;padding:16px 20px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
403
|
+
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
404
|
+
.aeo-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:10px}
|
|
405
|
+
.aeo-body{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center}
|
|
406
|
+
@media (max-width:640px){.aeo-body{grid-template-columns:1fr;gap:8px}}
|
|
407
|
+
.aeo-label{font-family:"Instrument Serif","Times New Roman",Georgia,serif;font-style:italic;font-size:28px;line-height:1;letter-spacing:-0.01em}
|
|
408
|
+
.aeo-bar{display:block;height:8px;background:var(--border);border-radius:999px;overflow:hidden}
|
|
409
|
+
.aeo-bar .bar-fill{height:100%;display:block;border-radius:999px}
|
|
410
|
+
.aeo-meta{font-size:11px;color:var(--muted)}
|
|
411
|
+
|
|
412
|
+
.top-fixes{margin-top:28px;padding:24px 28px 28px;background:color-mix(in oklab,var(--primary) 6%,var(--card) 94%);
|
|
413
|
+
border:1px solid color-mix(in oklab,var(--primary) 25%,var(--border));border-radius:var(--r-lg)}
|
|
414
|
+
.top-fix-list{list-style:none;display:flex;flex-direction:column;gap:10px}
|
|
415
|
+
.top-fix{display:grid;grid-template-columns:32px 1fr;gap:12px;align-items:start;padding:12px 14px;
|
|
416
|
+
background:var(--card);border:1px solid var(--border);border-radius:14px}
|
|
417
|
+
.top-fix-rank{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:999px;
|
|
418
|
+
background:color-mix(in oklab,var(--primary) 22%,transparent);color:var(--primary);
|
|
419
|
+
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;font-weight:700;margin-top:1px}
|
|
420
|
+
.top-fix-body{min-width:0}
|
|
421
|
+
.top-fix-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|
422
|
+
.top-fix-pages{color:var(--muted);font-size:11px;margin-left:auto}
|
|
423
|
+
.top-fix-msg{color:var(--fg);font-size:14px}
|
|
424
|
+
|
|
425
|
+
.findings-head{display:flex;align-items:baseline;justify-content:space-between;margin:56px 0 18px}
|
|
426
|
+
.findings-head h2{font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
427
|
+
.findings-head .meta{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;color:var(--muted)}
|
|
428
|
+
|
|
429
|
+
.sev-group{margin-top:28px}
|
|
430
|
+
.sev-group:first-child{margin-top:0}
|
|
431
|
+
.sev-heading{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;margin-bottom:10px}
|
|
432
|
+
.sev-dot{display:inline-block;width:8px;height:8px;border-radius:999px;background:currentColor}
|
|
433
|
+
.sev-count{margin-left:auto;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
434
|
+
font-size:12px;color:var(--muted);font-weight:400}
|
|
435
|
+
.sev-destructive{color:var(--destructive)} .sev-warning{color:var(--warning)} .sev-muted{color:var(--muted)}
|
|
436
|
+
.sev-heading.sev-muted{color:var(--muted-2)}
|
|
437
|
+
|
|
438
|
+
.finding-list{list-style:none;column-count:2;column-gap:10px;column-fill:balance}
|
|
439
|
+
@media (max-width:720px){.finding-list{column-count:1}}
|
|
440
|
+
.finding{position:relative;padding:14px 16px 14px 18px;background:var(--card);
|
|
441
|
+
border:1px solid var(--border);border-radius:var(--r);overflow:hidden;
|
|
442
|
+
break-inside:avoid;-webkit-column-break-inside:avoid;page-break-inside:avoid;
|
|
443
|
+
margin-bottom:10px;display:inline-block;width:100%}
|
|
444
|
+
.finding::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--border-strong)}
|
|
445
|
+
.finding-destructive::before{background:var(--destructive)}
|
|
446
|
+
.finding-warning::before{background:var(--warning)}
|
|
447
|
+
.finding-muted::before{background:var(--muted-2)}
|
|
448
|
+
.finding-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|
449
|
+
.rule-id{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12px;
|
|
450
|
+
color:var(--fg);background:var(--card-2);padding:2px 8px;border-radius:6px;border:1px solid var(--border)}
|
|
451
|
+
.finding-pages{color:var(--muted);font-size:11px;margin-left:auto}
|
|
452
|
+
.finding-msg{color:var(--fg);font-size:14px}
|
|
453
|
+
.page-preview{list-style:none;display:flex;flex-direction:column;gap:2px;margin-top:8px;font-size:12px;color:var(--muted)}
|
|
454
|
+
.page-extra{margin-top:6px}
|
|
455
|
+
.page-extra>summary{cursor:pointer;color:var(--muted);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
|
|
456
|
+
font-size:12px;list-style:none;padding:4px 0}
|
|
457
|
+
.page-extra>summary::-webkit-details-marker{display:none}
|
|
458
|
+
.page-extra>summary::before{content:"▸ ";color:var(--muted-2)}
|
|
459
|
+
.page-extra[open]>summary::before{content:"▾ "}
|
|
460
|
+
.page-list{list-style:none;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--muted);
|
|
461
|
+
max-height:240px;overflow-y:auto;padding:6px 0 0}
|
|
462
|
+
|
|
463
|
+
.effort-pill{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;
|
|
464
|
+
font-size:10px;font-weight:600;letter-spacing:0.04em;text-transform:lowercase;
|
|
465
|
+
border:1px solid transparent}
|
|
466
|
+
.effort-quick{background:color-mix(in oklab,var(--success) 14%,transparent);
|
|
467
|
+
color:var(--success);border-color:color-mix(in oklab,var(--success) 35%,transparent)}
|
|
468
|
+
.effort-moderate{background:color-mix(in oklab,var(--warning) 14%,transparent);
|
|
469
|
+
color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
470
|
+
.effort-structural{background:color-mix(in oklab,var(--destructive) 14%,transparent);
|
|
471
|
+
color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
472
|
+
|
|
473
|
+
.sev{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:10px;
|
|
474
|
+
font-weight:600;text-transform:lowercase;letter-spacing:0.04em;border:1px solid transparent}
|
|
475
|
+
.sev-destructive.sev,.sev.sev-destructive{background:color-mix(in oklab,var(--destructive) 14%,transparent);color:var(--destructive);border-color:color-mix(in oklab,var(--destructive) 35%,transparent)}
|
|
476
|
+
.sev-warning.sev,.sev.sev-warning{background:color-mix(in oklab,var(--warning) 14%,transparent);color:var(--warning);border-color:color-mix(in oklab,var(--warning) 35%,transparent)}
|
|
477
|
+
.sev-muted.sev,.sev.sev-muted{background:var(--card-2);color:var(--muted);border-color:var(--border-strong)}
|
|
478
|
+
|
|
479
|
+
.fix{margin-top:8px;padding:10px 12px;background:var(--card-2);border-radius:10px;
|
|
480
|
+
color:var(--muted);font-size:13px;line-height:1.55}
|
|
481
|
+
.fix-label{display:inline-block;margin-right:8px;padding:1px 6px;border-radius:4px;
|
|
482
|
+
background:color-mix(in oklab,var(--primary) 18%,transparent);color:var(--primary);
|
|
483
|
+
font-size:10px;letter-spacing:0.08em;text-transform:uppercase;font-weight:700;vertical-align:1px}
|
|
484
|
+
.ref{display:inline-block;margin-top:8px;color:var(--primary);font-size:12px;text-decoration:none}
|
|
485
|
+
.ref:hover{text-decoration:underline}
|
|
486
|
+
|
|
487
|
+
.finding details{margin-top:10px;border:1px solid var(--border);border-radius:12px;background:var(--card-2)}
|
|
488
|
+
.finding details.page-extra{border:0;background:transparent;margin-top:6px}
|
|
489
|
+
.finding details>summary{cursor:pointer;padding:10px 14px;color:var(--muted);font-size:12px;
|
|
490
|
+
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;list-style:none}
|
|
491
|
+
.finding details.page-extra>summary{padding:4px 0}
|
|
492
|
+
.finding details>summary::-webkit-details-marker{display:none}
|
|
493
|
+
.finding details>summary::before{content:"▸ ";color:var(--muted-2)}
|
|
494
|
+
.finding details[open]>summary::before{content:"▾ "}
|
|
495
|
+
.cluster-body{padding:0 14px 14px;border-top:1px solid var(--border)}
|
|
496
|
+
.cluster-label{margin:12px 0 6px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
|
|
497
|
+
.pair-list,.member-list{list-style:none;display:flex;flex-direction:column;gap:4px;font-size:12px}
|
|
498
|
+
.member-list{max-height:220px;overflow-y:auto;padding-right:4px}
|
|
499
|
+
.arrow{color:var(--muted-2)}
|
|
500
|
+
.sim{color:var(--warning);font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace}
|
|
501
|
+
|
|
502
|
+
.ai-triage{margin-top:28px;padding:28px;background:color-mix(in oklab,var(--card) 85%,transparent);
|
|
503
|
+
border:1px solid var(--border);border-radius:var(--r-lg)}
|
|
504
|
+
.ai-triage .narrative{color:var(--fg);font-size:15px;line-height:1.6;margin-bottom:18px}
|
|
505
|
+
.causes{list-style:none;display:flex;flex-direction:column;gap:12px}
|
|
506
|
+
.cause{padding:14px 16px;background:var(--card);border:1px solid var(--border);border-radius:14px}
|
|
507
|
+
.cause-head{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
508
|
+
.cause-order{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:999px;
|
|
509
|
+
background:color-mix(in oklab,var(--primary) 20%,transparent);color:var(--primary);
|
|
510
|
+
font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:11px;font-weight:700}
|
|
511
|
+
.cause h3{font-size:15px;font-weight:600}
|
|
512
|
+
.cause-meta{color:var(--muted);font-size:12px;font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;margin-bottom:6px}
|
|
513
|
+
.cause-body{color:var(--fg);font-size:14px;line-height:1.55}
|
|
514
|
+
|
|
515
|
+
.footer-note{margin-top:56px;padding:20px 22px;border:1px solid var(--border);border-radius:var(--r-lg);
|
|
516
|
+
background:color-mix(in oklab,var(--card) 60%,transparent);color:var(--muted);font-size:12px;line-height:1.6}
|
|
517
|
+
.footer-note strong{color:var(--fg);font-weight:600}
|
|
518
|
+
|
|
519
|
+
::selection{background:color-mix(in oklab,var(--primary) 75%,transparent);color:#0b1a14}
|
|
520
|
+
::-webkit-scrollbar{width:8px;height:8px}
|
|
521
|
+
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:999px}
|
|
522
|
+
</style>
|
|
523
|
+
</head>
|
|
524
|
+
<body>
|
|
525
|
+
<main>
|
|
526
|
+
<div class="status-row">
|
|
527
|
+
<span class="status-dot tone-${tone}"></span>
|
|
528
|
+
pseolint · audit complete
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<div class="title-row">
|
|
532
|
+
<h1 class="title display">Audit report</h1>
|
|
533
|
+
<span class="src-link">${summary.pageCount} pages · ${summary.findings.length} findings</span>
|
|
534
|
+
</div>
|
|
535
|
+
<p class="lead">Scored against 40+ rules covering Google's public SpamBrain guidance, programmatic-SEO research, and Answer Engine Optimization (AEO) — how citable your pages are to LLMs in AI Overviews. Risk score — lower is safer.</p>
|
|
536
|
+
|
|
537
|
+
<section class="card hero">
|
|
538
|
+
<div class="score-block">
|
|
539
|
+
<span class="score tone-${tone}">${summary.score}</span>
|
|
540
|
+
<span class="score-label">Risk score · /100</span>
|
|
541
|
+
<span class="verdict"><span class="verdict-dot tone-${tone}" style="background:var(--${tone})"></span>${escapeHtml(verdict)}</span>
|
|
542
|
+
</div>
|
|
543
|
+
<dl class="stats">
|
|
544
|
+
<div><dt class="stat-label">Pages</dt><dd class="stat-val">${summary.pageCount}</dd></div>
|
|
545
|
+
<div><dt class="stat-label">Errors</dt><dd class="stat-val tone-destructive">${totalErrors}</dd></div>
|
|
546
|
+
<div><dt class="stat-label">Warnings</dt><dd class="stat-val tone-warning">${counts.warning}</dd></div>
|
|
547
|
+
<div><dt class="stat-label">Info</dt><dd class="stat-val muted">${counts.info}</dd></div>
|
|
548
|
+
</dl>
|
|
549
|
+
</section>
|
|
550
|
+
|
|
551
|
+
${summary.templateDetected ? `<div class="template-banner"><strong>Template-generated content detected.</strong> Fix suggestions are tailored for template authors — one change can fix hundreds of pages.</div>` : ""}
|
|
552
|
+
|
|
553
|
+
${(() => {
|
|
554
|
+
const aeoScore = summary.categoryScores?.aeo;
|
|
555
|
+
if (aeoScore === undefined)
|
|
556
|
+
return "";
|
|
557
|
+
const aeoTone = categoryTone(aeoScore);
|
|
558
|
+
const label = aeoScoreLabel(aeoScore);
|
|
559
|
+
const invert = 100 - aeoScore;
|
|
560
|
+
return `
|
|
561
|
+
<section class="aeo-callout">
|
|
562
|
+
<div class="aeo-head">
|
|
563
|
+
<span class="eyebrow">AEO readiness</span>
|
|
564
|
+
<span class="meta-mono">answer-engine citability</span>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="aeo-body">
|
|
567
|
+
<span class="aeo-label tone-${aeoTone}">${escapeHtml(label)}</span>
|
|
568
|
+
<span class="aeo-bar"><span class="bar-fill bar-${aeoTone}" style="width:${invert}%"></span></span>
|
|
569
|
+
<span class="aeo-meta mono">${invert}/100 ready · ${aeoScore}/100 risk</span>
|
|
570
|
+
</div>
|
|
571
|
+
</section>`;
|
|
572
|
+
})()}
|
|
573
|
+
|
|
574
|
+
${topFixesHtml}
|
|
575
|
+
|
|
576
|
+
<section class="card">
|
|
577
|
+
<header class="section-head">
|
|
578
|
+
<span class="eyebrow">Category scores</span>
|
|
579
|
+
<span class="meta-mono">bar = how clean · number = risk (lower is better)</span>
|
|
580
|
+
</header>
|
|
581
|
+
<table class="data-table">
|
|
582
|
+
<tbody>${categoryRows}</tbody>
|
|
583
|
+
</table>
|
|
584
|
+
</section>
|
|
585
|
+
|
|
586
|
+
${groupScoresHtml}
|
|
587
|
+
|
|
588
|
+
${summary.triage ? renderTriageHtml(summary.triage) : ""}
|
|
589
|
+
|
|
590
|
+
<div class="findings-head">
|
|
591
|
+
<h2>Findings · ${summary.findings.length}</h2>
|
|
592
|
+
<span class="meta">grouped by rule · sampled ${summary.pageCount} page${summary.pageCount === 1 ? "" : "s"}</span>
|
|
593
|
+
</div>
|
|
594
|
+
${findingsSections}
|
|
595
|
+
|
|
596
|
+
<section class="footer-note">
|
|
597
|
+
<strong>About this report.</strong> Score is a structured heuristic, not a verdict from Google. Categories are weighted equally. Severities escalate: info → warning → error → critical. Effort tags (<span class="effort-pill effort-quick">quick fix</span> <span class="effort-pill effort-moderate">moderate</span> <span class="effort-pill effort-structural">structural</span>) estimate the change cost per finding. "Top fixes by impact" ranks by severity × pages affected — the same heuristic Pro uses for its fix queue (which also factors Search Console impressions once connected).
|
|
598
|
+
<br><br>
|
|
599
|
+
<strong>Analytics-safe.</strong> Rendered audits intercept outbound beacons to 40+ telemetry endpoints (GA, PostHog, Mixpanel, Segment, Hotjar, Sentry, Cloudflare/Vercel Insights, etc.) so your dashboards stay untouched — no fake sessions, no polluted funnels.
|
|
600
|
+
</section>
|
|
601
|
+
</main>
|
|
602
|
+
</body>
|
|
190
603
|
</html>`;
|
|
191
604
|
}
|
|
192
605
|
//# sourceMappingURL=html.js.map
|