@djangocfg/seo 2.1.50
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 +192 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +3780 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/crawler/index.d.ts +88 -0
- package/dist/crawler/index.mjs +610 -0
- package/dist/crawler/index.mjs.map +1 -0
- package/dist/google-console/index.d.ts +95 -0
- package/dist/google-console/index.mjs +539 -0
- package/dist/google-console/index.mjs.map +1 -0
- package/dist/index.d.ts +285 -0
- package/dist/index.mjs +3236 -0
- package/dist/index.mjs.map +1 -0
- package/dist/link-checker/index.d.ts +76 -0
- package/dist/link-checker/index.mjs +326 -0
- package/dist/link-checker/index.mjs.map +1 -0
- package/dist/markdown-report-B3QdDzxE.d.ts +193 -0
- package/dist/reports/index.d.ts +24 -0
- package/dist/reports/index.mjs +836 -0
- package/dist/reports/index.mjs.map +1 -0
- package/dist/routes/index.d.ts +69 -0
- package/dist/routes/index.mjs +372 -0
- package/dist/routes/index.mjs.map +1 -0
- package/dist/scanner-Cz4Th2Pt.d.ts +60 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +114 -0
- package/src/analyzer.ts +256 -0
- package/src/cli/commands/audit.ts +260 -0
- package/src/cli/commands/content.ts +180 -0
- package/src/cli/commands/crawl.ts +32 -0
- package/src/cli/commands/index.ts +12 -0
- package/src/cli/commands/inspect.ts +60 -0
- package/src/cli/commands/links.ts +41 -0
- package/src/cli/commands/robots.ts +36 -0
- package/src/cli/commands/routes.ts +126 -0
- package/src/cli/commands/sitemap.ts +48 -0
- package/src/cli/index.ts +149 -0
- package/src/cli/types.ts +40 -0
- package/src/config.ts +207 -0
- package/src/content/index.ts +51 -0
- package/src/content/link-checker.ts +182 -0
- package/src/content/link-fixer.ts +188 -0
- package/src/content/scanner.ts +200 -0
- package/src/content/sitemap-generator.ts +321 -0
- package/src/content/types.ts +140 -0
- package/src/crawler/crawler.ts +425 -0
- package/src/crawler/index.ts +10 -0
- package/src/crawler/robots-parser.ts +171 -0
- package/src/crawler/sitemap-validator.ts +204 -0
- package/src/google-console/analyzer.ts +317 -0
- package/src/google-console/auth.ts +100 -0
- package/src/google-console/client.ts +281 -0
- package/src/google-console/index.ts +9 -0
- package/src/index.ts +144 -0
- package/src/link-checker/index.ts +461 -0
- package/src/reports/claude-context.ts +149 -0
- package/src/reports/generator.ts +244 -0
- package/src/reports/index.ts +27 -0
- package/src/reports/json-report.ts +320 -0
- package/src/reports/markdown-report.ts +246 -0
- package/src/reports/split-report.ts +252 -0
- package/src/routes/analyzer.ts +324 -0
- package/src/routes/index.ts +25 -0
- package/src/routes/scanner.ts +298 -0
- package/src/types/index.ts +222 -0
- package/src/utils/index.ts +154 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { existsSync, readdirSync, rmSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import consola from 'consola';
|
|
4
|
+
|
|
5
|
+
// src/reports/generator.ts
|
|
6
|
+
|
|
7
|
+
// src/reports/json-report.ts
|
|
8
|
+
function generateJsonReport(siteUrl, data, options = {}) {
|
|
9
|
+
const { issues, urlInspections = [], crawlResults = [] } = data;
|
|
10
|
+
const maxUrlsPerIssue = options.maxUrlsPerIssue ?? 10;
|
|
11
|
+
const limitedIssues = limitIssuesByTitle(issues, maxUrlsPerIssue);
|
|
12
|
+
const report = {
|
|
13
|
+
id: generateReportId(),
|
|
14
|
+
siteUrl,
|
|
15
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16
|
+
summary: generateSummary(issues, urlInspections, crawlResults),
|
|
17
|
+
// Use original for accurate counts
|
|
18
|
+
issues: sortIssues(limitedIssues),
|
|
19
|
+
urlInspections: options.includeRawData ? urlInspections.slice(0, 100) : [],
|
|
20
|
+
crawlResults: options.includeRawData ? crawlResults.slice(0, 100) : [],
|
|
21
|
+
recommendations: generateRecommendations(issues, maxUrlsPerIssue)
|
|
22
|
+
};
|
|
23
|
+
return report;
|
|
24
|
+
}
|
|
25
|
+
function limitIssuesByTitle(issues, maxUrls) {
|
|
26
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const issue of issues) {
|
|
28
|
+
const existing = byTitle.get(issue.title) || [];
|
|
29
|
+
existing.push(issue);
|
|
30
|
+
byTitle.set(issue.title, existing);
|
|
31
|
+
}
|
|
32
|
+
const limited = [];
|
|
33
|
+
for (const [, group] of byTitle) {
|
|
34
|
+
const sorted = group.sort((a, b) => {
|
|
35
|
+
const severityOrder = { critical: 0, error: 1, warning: 2, info: 3 };
|
|
36
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
37
|
+
});
|
|
38
|
+
limited.push(...sorted.slice(0, maxUrls));
|
|
39
|
+
}
|
|
40
|
+
return limited;
|
|
41
|
+
}
|
|
42
|
+
function generateSummary(issues, urlInspections, crawlResults) {
|
|
43
|
+
const totalUrls = Math.max(
|
|
44
|
+
urlInspections.length,
|
|
45
|
+
crawlResults.length,
|
|
46
|
+
new Set(issues.map((i) => i.url)).size
|
|
47
|
+
);
|
|
48
|
+
const indexedUrls = urlInspections.filter(
|
|
49
|
+
(r) => r.indexStatusResult.coverageState === "SUBMITTED_AND_INDEXED"
|
|
50
|
+
).length;
|
|
51
|
+
const notIndexedUrls = urlInspections.filter(
|
|
52
|
+
(r) => r.indexStatusResult.coverageState === "NOT_INDEXED" || r.indexStatusResult.coverageState === "CRAWLED_CURRENTLY_NOT_INDEXED" || r.indexStatusResult.coverageState === "DISCOVERED_CURRENTLY_NOT_INDEXED"
|
|
53
|
+
).length;
|
|
54
|
+
const issuesByCategory = issues.reduce(
|
|
55
|
+
(acc, issue) => {
|
|
56
|
+
acc[issue.category] = (acc[issue.category] || 0) + 1;
|
|
57
|
+
return acc;
|
|
58
|
+
},
|
|
59
|
+
{}
|
|
60
|
+
);
|
|
61
|
+
const issuesBySeverity = issues.reduce(
|
|
62
|
+
(acc, issue) => {
|
|
63
|
+
acc[issue.severity] = (acc[issue.severity] || 0) + 1;
|
|
64
|
+
return acc;
|
|
65
|
+
},
|
|
66
|
+
{}
|
|
67
|
+
);
|
|
68
|
+
const healthScore = calculateHealthScore(issues, totalUrls);
|
|
69
|
+
return {
|
|
70
|
+
totalUrls,
|
|
71
|
+
indexedUrls,
|
|
72
|
+
notIndexedUrls,
|
|
73
|
+
issuesByCategory,
|
|
74
|
+
issuesBySeverity,
|
|
75
|
+
healthScore
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function calculateHealthScore(issues, totalUrls) {
|
|
79
|
+
if (totalUrls === 0) return 100;
|
|
80
|
+
const severityWeights = {
|
|
81
|
+
critical: 10,
|
|
82
|
+
error: 5,
|
|
83
|
+
warning: 2,
|
|
84
|
+
info: 0.5
|
|
85
|
+
};
|
|
86
|
+
const totalPenalty = issues.reduce(
|
|
87
|
+
(sum, issue) => sum + severityWeights[issue.severity],
|
|
88
|
+
0
|
|
89
|
+
);
|
|
90
|
+
const maxPenalty = totalUrls * 20;
|
|
91
|
+
const penaltyRatio = Math.min(totalPenalty / maxPenalty, 1);
|
|
92
|
+
return Math.round((1 - penaltyRatio) * 100);
|
|
93
|
+
}
|
|
94
|
+
function generateRecommendations(issues, maxUrls = 10) {
|
|
95
|
+
const recommendations = [];
|
|
96
|
+
const issueGroups = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const issue of issues) {
|
|
98
|
+
const key = `${issue.category}:${issue.title}`;
|
|
99
|
+
if (!issueGroups.has(key)) {
|
|
100
|
+
issueGroups.set(key, []);
|
|
101
|
+
}
|
|
102
|
+
issueGroups.get(key).push(issue);
|
|
103
|
+
}
|
|
104
|
+
for (const [, groupedIssues] of issueGroups) {
|
|
105
|
+
const firstIssue = groupedIssues[0];
|
|
106
|
+
if (!firstIssue) continue;
|
|
107
|
+
const severity = firstIssue.severity;
|
|
108
|
+
const priority = severity === "critical" ? 1 : severity === "error" ? 2 : severity === "warning" ? 3 : 4;
|
|
109
|
+
const impact = priority <= 2 ? "high" : priority === 3 ? "medium" : "low";
|
|
110
|
+
const allUrls = groupedIssues.map((i) => i.url);
|
|
111
|
+
const totalCount = allUrls.length;
|
|
112
|
+
const limitedUrls = allUrls.slice(0, maxUrls);
|
|
113
|
+
recommendations.push({
|
|
114
|
+
priority,
|
|
115
|
+
category: firstIssue.category,
|
|
116
|
+
title: firstIssue.title,
|
|
117
|
+
description: totalCount > maxUrls ? `${firstIssue.description} (showing ${maxUrls} of ${totalCount} URLs)` : firstIssue.description,
|
|
118
|
+
affectedUrls: limitedUrls,
|
|
119
|
+
estimatedImpact: impact,
|
|
120
|
+
actionItems: [firstIssue.recommendation]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return recommendations.sort((a, b) => a.priority - b.priority);
|
|
124
|
+
}
|
|
125
|
+
function sortIssues(issues) {
|
|
126
|
+
const severityOrder = {
|
|
127
|
+
critical: 0,
|
|
128
|
+
error: 1,
|
|
129
|
+
warning: 2,
|
|
130
|
+
info: 3
|
|
131
|
+
};
|
|
132
|
+
return [...issues].sort((a, b) => {
|
|
133
|
+
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
134
|
+
if (severityDiff !== 0) return severityDiff;
|
|
135
|
+
return a.category.localeCompare(b.category);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function generateReportId() {
|
|
139
|
+
const timestamp = Date.now().toString(36);
|
|
140
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
141
|
+
return `seo-report-${timestamp}-${random}`;
|
|
142
|
+
}
|
|
143
|
+
function exportJsonReport(report, pretty = true) {
|
|
144
|
+
return JSON.stringify(report, null, pretty ? 2 : 0);
|
|
145
|
+
}
|
|
146
|
+
var AI_REPORT_SCHEMA = {
|
|
147
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
148
|
+
title: "SEO Report",
|
|
149
|
+
description: "AI-friendly SEO analysis report with issues and recommendations",
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
id: { type: "string", description: "Unique report identifier" },
|
|
153
|
+
siteUrl: { type: "string", description: "Analyzed site URL" },
|
|
154
|
+
generatedAt: { type: "string", format: "date-time" },
|
|
155
|
+
summary: {
|
|
156
|
+
type: "object",
|
|
157
|
+
description: "Quick overview of SEO health",
|
|
158
|
+
properties: {
|
|
159
|
+
totalUrls: { type: "number" },
|
|
160
|
+
indexedUrls: { type: "number" },
|
|
161
|
+
notIndexedUrls: { type: "number" },
|
|
162
|
+
healthScore: {
|
|
163
|
+
type: "number",
|
|
164
|
+
minimum: 0,
|
|
165
|
+
maximum: 100,
|
|
166
|
+
description: "0-100 score, higher is better"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
issues: {
|
|
171
|
+
type: "array",
|
|
172
|
+
description: "List of detected SEO issues sorted by severity",
|
|
173
|
+
items: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
severity: {
|
|
177
|
+
type: "string",
|
|
178
|
+
enum: ["critical", "error", "warning", "info"]
|
|
179
|
+
},
|
|
180
|
+
category: {
|
|
181
|
+
type: "string",
|
|
182
|
+
enum: [
|
|
183
|
+
"indexing",
|
|
184
|
+
"crawling",
|
|
185
|
+
"content",
|
|
186
|
+
"technical",
|
|
187
|
+
"mobile",
|
|
188
|
+
"performance",
|
|
189
|
+
"structured-data",
|
|
190
|
+
"security"
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
title: { type: "string" },
|
|
194
|
+
description: { type: "string" },
|
|
195
|
+
recommendation: { type: "string" },
|
|
196
|
+
url: { type: "string" }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
recommendations: {
|
|
201
|
+
type: "array",
|
|
202
|
+
description: "Prioritized action items",
|
|
203
|
+
items: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
priority: { type: "number", minimum: 1, maximum: 5 },
|
|
207
|
+
title: { type: "string" },
|
|
208
|
+
affectedUrls: { type: "array", items: { type: "string" } },
|
|
209
|
+
actionItems: { type: "array", items: { type: "string" } }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/reports/markdown-report.ts
|
|
217
|
+
function generateMarkdownReport(report, options = {}) {
|
|
218
|
+
const { includeRawIssues = true, includeUrls = true, maxUrlsPerIssue = 10 } = options;
|
|
219
|
+
const lines = [];
|
|
220
|
+
lines.push(`# SEO Analysis Report`);
|
|
221
|
+
lines.push("");
|
|
222
|
+
lines.push(`**Site:** ${report.siteUrl}`);
|
|
223
|
+
lines.push(`**Generated:** ${new Date(report.generatedAt).toLocaleString()}`);
|
|
224
|
+
lines.push(`**Report ID:** ${report.id}`);
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push("## Summary");
|
|
227
|
+
lines.push("");
|
|
228
|
+
lines.push(`| Metric | Value |`);
|
|
229
|
+
lines.push(`|--------|-------|`);
|
|
230
|
+
lines.push(`| Health Score | ${getHealthScoreEmoji(report.summary.healthScore)} **${report.summary.healthScore}/100** |`);
|
|
231
|
+
lines.push(`| Total URLs | ${report.summary.totalUrls} |`);
|
|
232
|
+
lines.push(`| Indexed URLs | ${report.summary.indexedUrls} |`);
|
|
233
|
+
lines.push(`| Not Indexed | ${report.summary.notIndexedUrls} |`);
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push("### Issues by Severity");
|
|
236
|
+
lines.push("");
|
|
237
|
+
const severities = ["critical", "error", "warning", "info"];
|
|
238
|
+
for (const severity of severities) {
|
|
239
|
+
const count = report.summary.issuesBySeverity[severity] || 0;
|
|
240
|
+
lines.push(`- ${getSeverityEmoji(severity)} **${capitalize(severity)}:** ${count}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push("");
|
|
243
|
+
lines.push("### Issues by Category");
|
|
244
|
+
lines.push("");
|
|
245
|
+
const categories = Object.entries(report.summary.issuesByCategory).sort(
|
|
246
|
+
([, a], [, b]) => b - a
|
|
247
|
+
);
|
|
248
|
+
for (const [category, count] of categories) {
|
|
249
|
+
lines.push(`- ${getCategoryEmoji(category)} **${formatCategory(category)}:** ${count}`);
|
|
250
|
+
}
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push("## Prioritized Recommendations");
|
|
253
|
+
lines.push("");
|
|
254
|
+
for (const rec of report.recommendations) {
|
|
255
|
+
lines.push(`### ${getPriorityEmoji(rec.priority)} Priority ${rec.priority}: ${rec.title}`);
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push(`**Category:** ${formatCategory(rec.category)}`);
|
|
258
|
+
lines.push(`**Impact:** ${capitalize(rec.estimatedImpact)}`);
|
|
259
|
+
lines.push(`**Affected URLs:** ${rec.affectedUrls.length}`);
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push(`${rec.description}`);
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("**Action Items:**");
|
|
264
|
+
for (const action of rec.actionItems) {
|
|
265
|
+
lines.push(`- ${action}`);
|
|
266
|
+
}
|
|
267
|
+
lines.push("");
|
|
268
|
+
if (includeUrls && rec.affectedUrls.length > 0) {
|
|
269
|
+
const urlsToShow = rec.affectedUrls.slice(0, maxUrlsPerIssue);
|
|
270
|
+
lines.push("<details>");
|
|
271
|
+
lines.push(`<summary>Affected URLs (${rec.affectedUrls.length})</summary>`);
|
|
272
|
+
lines.push("");
|
|
273
|
+
for (const url of urlsToShow) {
|
|
274
|
+
lines.push(`- ${url}`);
|
|
275
|
+
}
|
|
276
|
+
if (rec.affectedUrls.length > maxUrlsPerIssue) {
|
|
277
|
+
lines.push(`- ... and ${rec.affectedUrls.length - maxUrlsPerIssue} more`);
|
|
278
|
+
}
|
|
279
|
+
lines.push("</details>");
|
|
280
|
+
lines.push("");
|
|
281
|
+
}
|
|
282
|
+
lines.push("---");
|
|
283
|
+
lines.push("");
|
|
284
|
+
}
|
|
285
|
+
if (includeRawIssues) {
|
|
286
|
+
lines.push("## All Issues");
|
|
287
|
+
lines.push("");
|
|
288
|
+
const issuesByCategory = groupBy(report.issues, "category");
|
|
289
|
+
for (const [category, issues] of Object.entries(issuesByCategory)) {
|
|
290
|
+
lines.push(`### ${getCategoryEmoji(category)} ${formatCategory(category)}`);
|
|
291
|
+
lines.push("");
|
|
292
|
+
for (const issue of issues) {
|
|
293
|
+
lines.push(
|
|
294
|
+
`#### ${getSeverityEmoji(issue.severity)} ${issue.title}`
|
|
295
|
+
);
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push(`**URL:** \`${issue.url}\``);
|
|
298
|
+
lines.push(`**Severity:** ${capitalize(issue.severity)}`);
|
|
299
|
+
lines.push("");
|
|
300
|
+
lines.push(issue.description);
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(`**Recommendation:** ${issue.recommendation}`);
|
|
303
|
+
lines.push("");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
lines.push("---");
|
|
308
|
+
lines.push("");
|
|
309
|
+
lines.push("*Report generated by [@djangocfg/seo](https://djangocfg.com)*");
|
|
310
|
+
lines.push("");
|
|
311
|
+
lines.push("> This report is designed to be processed by AI assistants for automated SEO improvements.");
|
|
312
|
+
return lines.join("\n");
|
|
313
|
+
}
|
|
314
|
+
function generateAiSummary(report) {
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push("# SEO Report Summary for AI Processing");
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push("## Context");
|
|
319
|
+
lines.push(`Site: ${report.siteUrl}`);
|
|
320
|
+
lines.push(`Health Score: ${report.summary.healthScore}/100`);
|
|
321
|
+
lines.push(`Critical Issues: ${report.summary.issuesBySeverity.critical || 0}`);
|
|
322
|
+
lines.push(`Errors: ${report.summary.issuesBySeverity.error || 0}`);
|
|
323
|
+
lines.push(`Warnings: ${report.summary.issuesBySeverity.warning || 0}`);
|
|
324
|
+
lines.push("");
|
|
325
|
+
lines.push("## Top Priority Actions");
|
|
326
|
+
lines.push("");
|
|
327
|
+
const topRecommendations = report.recommendations.slice(0, 5);
|
|
328
|
+
for (let i = 0; i < topRecommendations.length; i++) {
|
|
329
|
+
const rec = topRecommendations[i];
|
|
330
|
+
if (!rec) continue;
|
|
331
|
+
lines.push(`${i + 1}. **${rec.title}** (${rec.affectedUrls.length} URLs)`);
|
|
332
|
+
lines.push(` - ${rec.actionItems[0]}`);
|
|
333
|
+
}
|
|
334
|
+
lines.push("");
|
|
335
|
+
lines.push("## Issue Categories");
|
|
336
|
+
lines.push("");
|
|
337
|
+
const sortedCategories = Object.entries(report.summary.issuesByCategory).sort(([, a], [, b]) => b - a);
|
|
338
|
+
for (const [category, count] of sortedCategories) {
|
|
339
|
+
lines.push(`- ${formatCategory(category)}: ${count} issues`);
|
|
340
|
+
}
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
function getSeverityEmoji(severity) {
|
|
344
|
+
const emojis = {
|
|
345
|
+
critical: "\u{1F534}",
|
|
346
|
+
error: "\u{1F7E0}",
|
|
347
|
+
warning: "\u{1F7E1}",
|
|
348
|
+
info: "\u{1F535}"
|
|
349
|
+
};
|
|
350
|
+
return emojis[severity];
|
|
351
|
+
}
|
|
352
|
+
function getCategoryEmoji(category) {
|
|
353
|
+
const emojis = {
|
|
354
|
+
indexing: "\u{1F4D1}",
|
|
355
|
+
crawling: "\u{1F577}\uFE0F",
|
|
356
|
+
content: "\u{1F4DD}",
|
|
357
|
+
technical: "\u2699\uFE0F",
|
|
358
|
+
mobile: "\u{1F4F1}",
|
|
359
|
+
performance: "\u26A1",
|
|
360
|
+
"structured-data": "\u{1F3F7}\uFE0F",
|
|
361
|
+
security: "\u{1F512}"
|
|
362
|
+
};
|
|
363
|
+
return emojis[category] || "\u{1F4CB}";
|
|
364
|
+
}
|
|
365
|
+
function getPriorityEmoji(priority) {
|
|
366
|
+
const emojis = {
|
|
367
|
+
1: "\u{1F6A8}",
|
|
368
|
+
2: "\u26A0\uFE0F",
|
|
369
|
+
3: "\u{1F4CC}",
|
|
370
|
+
4: "\u{1F4A1}",
|
|
371
|
+
5: "\u2139\uFE0F"
|
|
372
|
+
};
|
|
373
|
+
return emojis[priority] || "\u{1F4CB}";
|
|
374
|
+
}
|
|
375
|
+
function getHealthScoreEmoji(score) {
|
|
376
|
+
if (score >= 90) return "\u{1F7E2}";
|
|
377
|
+
if (score >= 70) return "\u{1F7E1}";
|
|
378
|
+
if (score >= 50) return "\u{1F7E0}";
|
|
379
|
+
return "\u{1F534}";
|
|
380
|
+
}
|
|
381
|
+
function capitalize(str) {
|
|
382
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
383
|
+
}
|
|
384
|
+
function formatCategory(category) {
|
|
385
|
+
return category.split("-").map(capitalize).join(" ");
|
|
386
|
+
}
|
|
387
|
+
function groupBy(array, key) {
|
|
388
|
+
return array.reduce(
|
|
389
|
+
(acc, item) => {
|
|
390
|
+
const groupKey = String(item[key]);
|
|
391
|
+
if (!acc[groupKey]) {
|
|
392
|
+
acc[groupKey] = [];
|
|
393
|
+
}
|
|
394
|
+
acc[groupKey].push(item);
|
|
395
|
+
return acc;
|
|
396
|
+
},
|
|
397
|
+
{}
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
var MAX_LINES = 1e3;
|
|
401
|
+
function generateSplitReports(report, options) {
|
|
402
|
+
const { outputDir, clearOutputDir = true } = options;
|
|
403
|
+
if (clearOutputDir && existsSync(outputDir)) {
|
|
404
|
+
const files = readdirSync(outputDir);
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
if (file.startsWith("seo-") && file.endsWith(".md")) {
|
|
407
|
+
rmSync(join(outputDir, file), { force: true });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!existsSync(outputDir)) {
|
|
412
|
+
mkdirSync(outputDir, { recursive: true });
|
|
413
|
+
}
|
|
414
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
415
|
+
const siteName = new URL(report.siteUrl).hostname.replace(/\./g, "-");
|
|
416
|
+
const prefix = `seo-${siteName}-${timestamp}`;
|
|
417
|
+
const categoryFiles = [];
|
|
418
|
+
const issuesByCategory = groupIssuesByCategory(report.issues);
|
|
419
|
+
const categories = Object.keys(issuesByCategory);
|
|
420
|
+
for (const category of categories) {
|
|
421
|
+
const issues = issuesByCategory[category] || [];
|
|
422
|
+
if (issues.length === 0) continue;
|
|
423
|
+
const chunks = splitIntoChunks(issues);
|
|
424
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
425
|
+
const suffix = chunks.length > 1 ? `-${i + 1}` : "";
|
|
426
|
+
const filename = `${prefix}-${category}${suffix}.md`;
|
|
427
|
+
const filepath = join(outputDir, filename);
|
|
428
|
+
const chunk = chunks[i];
|
|
429
|
+
if (!chunk) continue;
|
|
430
|
+
const content = generateCategoryFile(report.siteUrl, category, chunk, {
|
|
431
|
+
part: chunks.length > 1 ? i + 1 : void 0,
|
|
432
|
+
totalParts: chunks.length > 1 ? chunks.length : void 0
|
|
433
|
+
});
|
|
434
|
+
writeFileSync(filepath, content, "utf-8");
|
|
435
|
+
categoryFiles.push(filename);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const indexFilename = `${prefix}-index.md`;
|
|
439
|
+
const indexFilepath = join(outputDir, indexFilename);
|
|
440
|
+
const indexContent = generateIndexFile(report, categoryFiles);
|
|
441
|
+
writeFileSync(indexFilepath, indexContent, "utf-8");
|
|
442
|
+
return {
|
|
443
|
+
indexFile: indexFilename,
|
|
444
|
+
categoryFiles,
|
|
445
|
+
totalFiles: categoryFiles.length + 1
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function generateIndexFile(report, categoryFiles) {
|
|
449
|
+
const lines = [];
|
|
450
|
+
lines.push("# SEO Report Index");
|
|
451
|
+
lines.push("");
|
|
452
|
+
lines.push(`Site: ${report.siteUrl}`);
|
|
453
|
+
lines.push(`Score: ${report.summary.healthScore}/100`);
|
|
454
|
+
lines.push(`Date: ${report.generatedAt.slice(0, 10)}`);
|
|
455
|
+
lines.push("");
|
|
456
|
+
lines.push("## Issues");
|
|
457
|
+
lines.push("");
|
|
458
|
+
lines.push("| Severity | Count |");
|
|
459
|
+
lines.push("|----------|-------|");
|
|
460
|
+
const severities = ["critical", "error", "warning", "info"];
|
|
461
|
+
for (const sev of severities) {
|
|
462
|
+
const count = report.summary.issuesBySeverity[sev] || 0;
|
|
463
|
+
if (count > 0) {
|
|
464
|
+
lines.push(`| ${sev} | ${count} |`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
lines.push("");
|
|
468
|
+
lines.push("## Actions");
|
|
469
|
+
lines.push("");
|
|
470
|
+
const topRecs = report.recommendations.slice(0, 10);
|
|
471
|
+
for (let i = 0; i < topRecs.length; i++) {
|
|
472
|
+
const rec = topRecs[i];
|
|
473
|
+
if (!rec) continue;
|
|
474
|
+
lines.push(`${i + 1}. **${rec.title}** (${rec.affectedUrls.length})`);
|
|
475
|
+
lines.push(` ${rec.actionItems[0]}`);
|
|
476
|
+
}
|
|
477
|
+
lines.push("");
|
|
478
|
+
lines.push("## Files");
|
|
479
|
+
lines.push("");
|
|
480
|
+
for (const file of categoryFiles) {
|
|
481
|
+
lines.push(`- [${file}](./${file})`);
|
|
482
|
+
}
|
|
483
|
+
return lines.join("\n");
|
|
484
|
+
}
|
|
485
|
+
function generateCategoryFile(siteUrl, category, issues, opts) {
|
|
486
|
+
const lines = [];
|
|
487
|
+
const partStr = opts.part ? ` (Part ${opts.part}/${opts.totalParts})` : "";
|
|
488
|
+
lines.push(`# ${formatCategory2(category)}${partStr}`);
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push(`Site: ${siteUrl}`);
|
|
491
|
+
lines.push(`Issues: ${issues.length}`);
|
|
492
|
+
lines.push("");
|
|
493
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const issue of issues) {
|
|
495
|
+
const group = byTitle.get(issue.title) || [];
|
|
496
|
+
group.push(issue);
|
|
497
|
+
byTitle.set(issue.title, group);
|
|
498
|
+
}
|
|
499
|
+
for (const [title, groupIssues] of byTitle) {
|
|
500
|
+
const first = groupIssues[0];
|
|
501
|
+
if (!first) continue;
|
|
502
|
+
lines.push(`## ${title}`);
|
|
503
|
+
lines.push("");
|
|
504
|
+
lines.push(`Severity: ${first.severity}`);
|
|
505
|
+
lines.push(`Count: ${groupIssues.length}`);
|
|
506
|
+
lines.push("");
|
|
507
|
+
lines.push(`> ${first.recommendation}`);
|
|
508
|
+
lines.push("");
|
|
509
|
+
lines.push("URLs:");
|
|
510
|
+
for (const issue of groupIssues.slice(0, 20)) {
|
|
511
|
+
lines.push(`- ${issue.url}`);
|
|
512
|
+
}
|
|
513
|
+
if (groupIssues.length > 20) {
|
|
514
|
+
lines.push(`- ... +${groupIssues.length - 20} more`);
|
|
515
|
+
}
|
|
516
|
+
lines.push("");
|
|
517
|
+
}
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
function splitIntoChunks(issues, category) {
|
|
521
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const issue of issues) {
|
|
523
|
+
const group = byTitle.get(issue.title) || [];
|
|
524
|
+
group.push(issue);
|
|
525
|
+
byTitle.set(issue.title, group);
|
|
526
|
+
}
|
|
527
|
+
const chunks = [];
|
|
528
|
+
let currentChunk = [];
|
|
529
|
+
let currentLines = 10;
|
|
530
|
+
for (const [, groupIssues] of byTitle) {
|
|
531
|
+
const urlCount = Math.min(20, groupIssues.length);
|
|
532
|
+
const groupLines = 8 + urlCount;
|
|
533
|
+
if (currentLines + groupLines > MAX_LINES && currentChunk.length > 0) {
|
|
534
|
+
chunks.push(currentChunk);
|
|
535
|
+
currentChunk = [];
|
|
536
|
+
currentLines = 10;
|
|
537
|
+
}
|
|
538
|
+
currentChunk.push(...groupIssues);
|
|
539
|
+
currentLines += groupLines;
|
|
540
|
+
}
|
|
541
|
+
if (currentChunk.length > 0) {
|
|
542
|
+
chunks.push(currentChunk);
|
|
543
|
+
}
|
|
544
|
+
return chunks.length > 0 ? chunks : [[]];
|
|
545
|
+
}
|
|
546
|
+
function groupIssuesByCategory(issues) {
|
|
547
|
+
const result = {};
|
|
548
|
+
for (const issue of issues) {
|
|
549
|
+
if (!result[issue.category]) {
|
|
550
|
+
result[issue.category] = [];
|
|
551
|
+
}
|
|
552
|
+
result[issue.category].push(issue);
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
function formatCategory2(category) {
|
|
557
|
+
return category.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/reports/claude-context.ts
|
|
561
|
+
function generateClaudeContext(report) {
|
|
562
|
+
const lines = [];
|
|
563
|
+
lines.push("# @djangocfg/seo");
|
|
564
|
+
lines.push("");
|
|
565
|
+
lines.push("SEO audit toolkit. Generates AI-optimized split reports (max 1000 lines each).");
|
|
566
|
+
lines.push("");
|
|
567
|
+
lines.push("## Commands");
|
|
568
|
+
lines.push("");
|
|
569
|
+
lines.push("```bash");
|
|
570
|
+
lines.push("# Audit (HTTP-based, crawls live site)");
|
|
571
|
+
lines.push("pnpm seo:audit # Full audit (split reports)");
|
|
572
|
+
lines.push("pnpm seo:audit --env dev # Audit local dev");
|
|
573
|
+
lines.push("pnpm seo:audit --format all # All formats");
|
|
574
|
+
lines.push("");
|
|
575
|
+
lines.push("# Content (file-based, scans MDX/content/)");
|
|
576
|
+
lines.push("pnpm exec djangocfg-seo content check # Check MDX links");
|
|
577
|
+
lines.push("pnpm exec djangocfg-seo content fix # Show fixable links");
|
|
578
|
+
lines.push("pnpm exec djangocfg-seo content fix --fix # Apply fixes");
|
|
579
|
+
lines.push("pnpm exec djangocfg-seo content sitemap # Generate sitemap.ts");
|
|
580
|
+
lines.push("```");
|
|
581
|
+
lines.push("");
|
|
582
|
+
lines.push("## Options");
|
|
583
|
+
lines.push("");
|
|
584
|
+
lines.push("- `--env, -e` - prod (default) or dev");
|
|
585
|
+
lines.push("- `--site, -s` - Site URL (overrides env)");
|
|
586
|
+
lines.push("- `--output, -o` - Output directory");
|
|
587
|
+
lines.push("- `--format, -f` - split (default), json, markdown, ai-summary, all");
|
|
588
|
+
lines.push("- `--max-pages` - Max pages (default: 100)");
|
|
589
|
+
lines.push("- `--service-account` - Google service account JSON path");
|
|
590
|
+
lines.push("- `--content-dir` - Content directory (default: content/)");
|
|
591
|
+
lines.push("- `--base-path` - Base URL path for docs (default: /docs)");
|
|
592
|
+
lines.push("");
|
|
593
|
+
lines.push("## Reports");
|
|
594
|
+
lines.push("");
|
|
595
|
+
lines.push("- `seo-*-index.md` - Summary + links to categories");
|
|
596
|
+
lines.push("- `seo-*-technical.md` - Broken links, sitemap issues");
|
|
597
|
+
lines.push("- `seo-*-content.md` - H1, meta, title issues");
|
|
598
|
+
lines.push("- `seo-*-performance.md` - Load time, TTFB issues");
|
|
599
|
+
lines.push("- `seo-ai-summary-*.md` - Quick overview");
|
|
600
|
+
lines.push("");
|
|
601
|
+
lines.push("## Issue Severity");
|
|
602
|
+
lines.push("");
|
|
603
|
+
lines.push("- **critical** - Blocks indexing (fix immediately)");
|
|
604
|
+
lines.push("- **error** - SEO problems (high priority)");
|
|
605
|
+
lines.push("- **warning** - Recommendations (medium priority)");
|
|
606
|
+
lines.push("- **info** - Best practices (low priority)");
|
|
607
|
+
lines.push("");
|
|
608
|
+
lines.push("## Issue Categories");
|
|
609
|
+
lines.push("");
|
|
610
|
+
lines.push("- **technical** - Broken links, sitemap, robots.txt");
|
|
611
|
+
lines.push("- **content** - Missing H1, meta description, title");
|
|
612
|
+
lines.push("- **indexing** - Not indexed, crawl errors from GSC");
|
|
613
|
+
lines.push("- **performance** - Slow load time (>3s), high TTFB (>800ms)");
|
|
614
|
+
lines.push("");
|
|
615
|
+
lines.push("## Routes Scanner");
|
|
616
|
+
lines.push("");
|
|
617
|
+
lines.push("Scans Next.js App Router `app/` directory. Handles:");
|
|
618
|
+
lines.push("- Route groups `(group)` - ignored in URL");
|
|
619
|
+
lines.push("- Dynamic `[slug]` - shown as `:slug`");
|
|
620
|
+
lines.push("- Catch-all `[...slug]` - shown as `:...slug`");
|
|
621
|
+
lines.push("- Parallel `@folder` - skipped");
|
|
622
|
+
lines.push("- Private `_folder` - skipped");
|
|
623
|
+
lines.push("");
|
|
624
|
+
lines.push("## Link Guidelines");
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push("### Nextra/MDX Projects (content/)");
|
|
627
|
+
lines.push("");
|
|
628
|
+
lines.push("For non-index files (e.g., `overview.mdx`):");
|
|
629
|
+
lines.push("- **Sibling file**: `../sibling` (one level up)");
|
|
630
|
+
lines.push("- **Other section**: `/docs/full/path` (absolute)");
|
|
631
|
+
lines.push("- **AVOID**: `./sibling` (browser adds filename to path!)");
|
|
632
|
+
lines.push("- **AVOID**: `../../deep/path` (hard to maintain)");
|
|
633
|
+
lines.push("");
|
|
634
|
+
lines.push("For index files (e.g., `index.mdx`):");
|
|
635
|
+
lines.push("- **Child file**: `./child` works correctly");
|
|
636
|
+
lines.push("- **Sibling folder**: `../sibling/` or absolute");
|
|
637
|
+
lines.push("");
|
|
638
|
+
lines.push("### Next.js App Router Projects");
|
|
639
|
+
lines.push("");
|
|
640
|
+
lines.push("Use declarative routes from `_routes/`:");
|
|
641
|
+
lines.push("```typescript");
|
|
642
|
+
lines.push('import { routes } from "@/app/_routes";');
|
|
643
|
+
lines.push("<Link href={routes.dashboard.machines}>Machines</Link>");
|
|
644
|
+
lines.push("```");
|
|
645
|
+
lines.push("");
|
|
646
|
+
lines.push("Benefits: type-safe, refactor-friendly, centralized.");
|
|
647
|
+
lines.push("");
|
|
648
|
+
lines.push("---");
|
|
649
|
+
lines.push("");
|
|
650
|
+
lines.push("## Current Audit");
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(`Site: ${report.siteUrl}`);
|
|
653
|
+
lines.push(`Score: ${report.summary.healthScore}/100`);
|
|
654
|
+
lines.push(`Date: ${report.generatedAt.slice(0, 10)}`);
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("### Issues");
|
|
657
|
+
lines.push("");
|
|
658
|
+
const { critical = 0, error = 0, warning = 0, info = 0 } = report.summary.issuesBySeverity;
|
|
659
|
+
if (critical > 0) lines.push(`- Critical: ${critical}`);
|
|
660
|
+
if (error > 0) lines.push(`- Error: ${error}`);
|
|
661
|
+
if (warning > 0) lines.push(`- Warning: ${warning}`);
|
|
662
|
+
if (info > 0) lines.push(`- Info: ${info}`);
|
|
663
|
+
lines.push("");
|
|
664
|
+
lines.push("### Top Actions");
|
|
665
|
+
lines.push("");
|
|
666
|
+
const topRecs = report.recommendations.slice(0, 5);
|
|
667
|
+
for (let i = 0; i < topRecs.length; i++) {
|
|
668
|
+
const rec = topRecs[i];
|
|
669
|
+
if (!rec) continue;
|
|
670
|
+
lines.push(`${i + 1}. **${rec.title}** (${rec.affectedUrls.length} URLs)`);
|
|
671
|
+
}
|
|
672
|
+
lines.push("");
|
|
673
|
+
lines.push("### Report Files");
|
|
674
|
+
lines.push("");
|
|
675
|
+
lines.push("See split reports in this directory:");
|
|
676
|
+
lines.push("- `seo-*-index.md` - Start here");
|
|
677
|
+
lines.push("- `seo-*-technical.md` - Technical issues");
|
|
678
|
+
lines.push("- `seo-*-content.md` - Content issues");
|
|
679
|
+
lines.push("- `seo-*-performance.md` - Performance issues");
|
|
680
|
+
lines.push("");
|
|
681
|
+
return lines.join("\n");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/reports/generator.ts
|
|
685
|
+
async function generateAndSaveReports(siteUrl, data, options) {
|
|
686
|
+
const {
|
|
687
|
+
outputDir,
|
|
688
|
+
formats,
|
|
689
|
+
includeRawData = false,
|
|
690
|
+
timestamp = true,
|
|
691
|
+
clearOutputDir = true,
|
|
692
|
+
maxUrlsPerIssue = 10
|
|
693
|
+
} = options;
|
|
694
|
+
if (clearOutputDir && existsSync(outputDir)) {
|
|
695
|
+
try {
|
|
696
|
+
const files = readdirSync(outputDir);
|
|
697
|
+
for (const file of files) {
|
|
698
|
+
if (file.startsWith("seo-")) {
|
|
699
|
+
rmSync(join(outputDir, file), { force: true });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!existsSync(outputDir)) {
|
|
706
|
+
mkdirSync(outputDir, { recursive: true });
|
|
707
|
+
}
|
|
708
|
+
const report = generateJsonReport(siteUrl, data, { includeRawData, maxUrlsPerIssue });
|
|
709
|
+
const result = {
|
|
710
|
+
report,
|
|
711
|
+
files: {}
|
|
712
|
+
};
|
|
713
|
+
const ts = timestamp ? `-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}` : "";
|
|
714
|
+
const siteName = new URL(siteUrl).hostname.replace(/\./g, "-");
|
|
715
|
+
if (formats.includes("json")) {
|
|
716
|
+
const filename = `seo-report-${siteName}${ts}.json`;
|
|
717
|
+
const filepath = join(outputDir, filename);
|
|
718
|
+
const content = exportJsonReport(report, true);
|
|
719
|
+
writeFileSync(filepath, content, "utf-8");
|
|
720
|
+
result.files.json = filepath;
|
|
721
|
+
consola.success(`JSON report saved: ${filepath}`);
|
|
722
|
+
}
|
|
723
|
+
if (formats.includes("markdown")) {
|
|
724
|
+
const filename = `seo-report-${siteName}${ts}.md`;
|
|
725
|
+
const filepath = join(outputDir, filename);
|
|
726
|
+
const content = generateMarkdownReport(report, {
|
|
727
|
+
includeRawIssues: true,
|
|
728
|
+
includeUrls: true
|
|
729
|
+
});
|
|
730
|
+
writeFileSync(filepath, content, "utf-8");
|
|
731
|
+
result.files.markdown = filepath;
|
|
732
|
+
consola.success(`Markdown report saved: ${filepath}`);
|
|
733
|
+
}
|
|
734
|
+
if (formats.includes("ai-summary")) {
|
|
735
|
+
const filename = `seo-ai-summary-${siteName}${ts}.md`;
|
|
736
|
+
const filepath = join(outputDir, filename);
|
|
737
|
+
const content = generateAiSummary(report);
|
|
738
|
+
writeFileSync(filepath, content, "utf-8");
|
|
739
|
+
result.files.aiSummary = filepath;
|
|
740
|
+
consola.success(`AI summary saved: ${filepath}`);
|
|
741
|
+
}
|
|
742
|
+
if (formats.includes("split")) {
|
|
743
|
+
const splitResult = generateSplitReports(report, {
|
|
744
|
+
outputDir,
|
|
745
|
+
clearOutputDir: false
|
|
746
|
+
// Already cleared above
|
|
747
|
+
});
|
|
748
|
+
result.files.split = {
|
|
749
|
+
index: join(outputDir, splitResult.indexFile),
|
|
750
|
+
categories: splitResult.categoryFiles.map((f) => join(outputDir, f))
|
|
751
|
+
};
|
|
752
|
+
consola.success(`Split reports saved: ${splitResult.totalFiles} files (index + ${splitResult.categoryFiles.length} categories)`);
|
|
753
|
+
}
|
|
754
|
+
const claudeContent = generateClaudeContext(report);
|
|
755
|
+
const claudeFilepath = join(outputDir, "CLAUDE.md");
|
|
756
|
+
writeFileSync(claudeFilepath, claudeContent, "utf-8");
|
|
757
|
+
consola.success(`AI context saved: ${claudeFilepath}`);
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
function printReportSummary(report) {
|
|
761
|
+
consola.box(
|
|
762
|
+
`SEO Report: ${report.siteUrl}
|
|
763
|
+
Health Score: ${report.summary.healthScore}/100
|
|
764
|
+
Total URLs: ${report.summary.totalUrls}
|
|
765
|
+
Indexed: ${report.summary.indexedUrls} | Not Indexed: ${report.summary.notIndexedUrls}
|
|
766
|
+
Issues: ${report.issues.length}`
|
|
767
|
+
);
|
|
768
|
+
if (report.summary.issuesBySeverity.critical) {
|
|
769
|
+
consola.error(`Critical issues: ${report.summary.issuesBySeverity.critical}`);
|
|
770
|
+
}
|
|
771
|
+
if (report.summary.issuesBySeverity.error) {
|
|
772
|
+
consola.warn(`Errors: ${report.summary.issuesBySeverity.error}`);
|
|
773
|
+
}
|
|
774
|
+
if (report.summary.issuesBySeverity.warning) {
|
|
775
|
+
consola.info(`Warnings: ${report.summary.issuesBySeverity.warning}`);
|
|
776
|
+
}
|
|
777
|
+
consola.log("");
|
|
778
|
+
consola.info("Top recommendations:");
|
|
779
|
+
for (const rec of report.recommendations.slice(0, 3)) {
|
|
780
|
+
consola.log(` ${rec.priority}. ${rec.title} (${rec.affectedUrls.length} URLs)`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function mergeReports(reports) {
|
|
784
|
+
if (reports.length === 0) {
|
|
785
|
+
throw new Error("Cannot merge empty reports array");
|
|
786
|
+
}
|
|
787
|
+
const merged = {
|
|
788
|
+
id: `merged-${Date.now().toString(36)}`,
|
|
789
|
+
siteUrl: reports.map((r) => r.siteUrl).join(", "),
|
|
790
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
791
|
+
summary: {
|
|
792
|
+
totalUrls: reports.reduce((sum, r) => sum + r.summary.totalUrls, 0),
|
|
793
|
+
indexedUrls: reports.reduce((sum, r) => sum + r.summary.indexedUrls, 0),
|
|
794
|
+
notIndexedUrls: reports.reduce((sum, r) => sum + r.summary.notIndexedUrls, 0),
|
|
795
|
+
issuesByCategory: {},
|
|
796
|
+
issuesBySeverity: {},
|
|
797
|
+
healthScore: Math.round(
|
|
798
|
+
reports.reduce((sum, r) => sum + r.summary.healthScore, 0) / reports.length
|
|
799
|
+
)
|
|
800
|
+
},
|
|
801
|
+
issues: reports.flatMap((r) => r.issues),
|
|
802
|
+
urlInspections: reports.flatMap((r) => r.urlInspections),
|
|
803
|
+
crawlResults: reports.flatMap((r) => r.crawlResults),
|
|
804
|
+
recommendations: []
|
|
805
|
+
};
|
|
806
|
+
for (const report of reports) {
|
|
807
|
+
for (const [category, count] of Object.entries(report.summary.issuesByCategory)) {
|
|
808
|
+
const cat = category;
|
|
809
|
+
merged.summary.issuesByCategory[cat] = (merged.summary.issuesByCategory[cat] || 0) + count;
|
|
810
|
+
}
|
|
811
|
+
for (const [severity, count] of Object.entries(report.summary.issuesBySeverity)) {
|
|
812
|
+
const sev = severity;
|
|
813
|
+
merged.summary.issuesBySeverity[sev] = (merged.summary.issuesBySeverity[sev] || 0) + count;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const recMap = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const report of reports) {
|
|
818
|
+
for (const rec of report.recommendations) {
|
|
819
|
+
const key = `${rec.category}:${rec.title}`;
|
|
820
|
+
if (recMap.has(key)) {
|
|
821
|
+
const existing = recMap.get(key);
|
|
822
|
+
existing.affectedUrls.push(...rec.affectedUrls);
|
|
823
|
+
} else {
|
|
824
|
+
recMap.set(key, { ...rec, affectedUrls: [...rec.affectedUrls] });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
merged.recommendations = Array.from(recMap.values()).sort(
|
|
829
|
+
(a, b) => a.priority - b.priority
|
|
830
|
+
);
|
|
831
|
+
return merged;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export { AI_REPORT_SCHEMA, exportJsonReport, generateAiSummary, generateAndSaveReports, generateJsonReport, generateMarkdownReport, generateSplitReports, mergeReports, printReportSummary };
|
|
835
|
+
//# sourceMappingURL=index.mjs.map
|
|
836
|
+
//# sourceMappingURL=index.mjs.map
|