@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.
Files changed (68) hide show
  1. package/README.md +192 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.mjs +3780 -0
  4. package/dist/cli.mjs.map +1 -0
  5. package/dist/crawler/index.d.ts +88 -0
  6. package/dist/crawler/index.mjs +610 -0
  7. package/dist/crawler/index.mjs.map +1 -0
  8. package/dist/google-console/index.d.ts +95 -0
  9. package/dist/google-console/index.mjs +539 -0
  10. package/dist/google-console/index.mjs.map +1 -0
  11. package/dist/index.d.ts +285 -0
  12. package/dist/index.mjs +3236 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/link-checker/index.d.ts +76 -0
  15. package/dist/link-checker/index.mjs +326 -0
  16. package/dist/link-checker/index.mjs.map +1 -0
  17. package/dist/markdown-report-B3QdDzxE.d.ts +193 -0
  18. package/dist/reports/index.d.ts +24 -0
  19. package/dist/reports/index.mjs +836 -0
  20. package/dist/reports/index.mjs.map +1 -0
  21. package/dist/routes/index.d.ts +69 -0
  22. package/dist/routes/index.mjs +372 -0
  23. package/dist/routes/index.mjs.map +1 -0
  24. package/dist/scanner-Cz4Th2Pt.d.ts +60 -0
  25. package/dist/types/index.d.ts +144 -0
  26. package/dist/types/index.mjs +3 -0
  27. package/dist/types/index.mjs.map +1 -0
  28. package/package.json +114 -0
  29. package/src/analyzer.ts +256 -0
  30. package/src/cli/commands/audit.ts +260 -0
  31. package/src/cli/commands/content.ts +180 -0
  32. package/src/cli/commands/crawl.ts +32 -0
  33. package/src/cli/commands/index.ts +12 -0
  34. package/src/cli/commands/inspect.ts +60 -0
  35. package/src/cli/commands/links.ts +41 -0
  36. package/src/cli/commands/robots.ts +36 -0
  37. package/src/cli/commands/routes.ts +126 -0
  38. package/src/cli/commands/sitemap.ts +48 -0
  39. package/src/cli/index.ts +149 -0
  40. package/src/cli/types.ts +40 -0
  41. package/src/config.ts +207 -0
  42. package/src/content/index.ts +51 -0
  43. package/src/content/link-checker.ts +182 -0
  44. package/src/content/link-fixer.ts +188 -0
  45. package/src/content/scanner.ts +200 -0
  46. package/src/content/sitemap-generator.ts +321 -0
  47. package/src/content/types.ts +140 -0
  48. package/src/crawler/crawler.ts +425 -0
  49. package/src/crawler/index.ts +10 -0
  50. package/src/crawler/robots-parser.ts +171 -0
  51. package/src/crawler/sitemap-validator.ts +204 -0
  52. package/src/google-console/analyzer.ts +317 -0
  53. package/src/google-console/auth.ts +100 -0
  54. package/src/google-console/client.ts +281 -0
  55. package/src/google-console/index.ts +9 -0
  56. package/src/index.ts +144 -0
  57. package/src/link-checker/index.ts +461 -0
  58. package/src/reports/claude-context.ts +149 -0
  59. package/src/reports/generator.ts +244 -0
  60. package/src/reports/index.ts +27 -0
  61. package/src/reports/json-report.ts +320 -0
  62. package/src/reports/markdown-report.ts +246 -0
  63. package/src/reports/split-report.ts +252 -0
  64. package/src/routes/analyzer.ts +324 -0
  65. package/src/routes/index.ts +25 -0
  66. package/src/routes/scanner.ts +298 -0
  67. package/src/types/index.ts +222 -0
  68. package/src/utils/index.ts +154 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @djangocfg/seo - Markdown Report Generator
3
+ * Generate human-readable Markdown reports
4
+ */
5
+
6
+ import type { SeoReport, SeoIssue, IssueSeverity, IssueCategory } from '../types/index.js';
7
+
8
+ export interface MarkdownReportOptions {
9
+ includeRawIssues?: boolean;
10
+ includeUrls?: boolean;
11
+ maxUrlsPerIssue?: number;
12
+ }
13
+
14
+ /**
15
+ * Generate a Markdown report from SEO data
16
+ */
17
+ export function generateMarkdownReport(
18
+ report: SeoReport,
19
+ options: MarkdownReportOptions = {}
20
+ ): string {
21
+ const { includeRawIssues = true, includeUrls = true, maxUrlsPerIssue = 10 } = options;
22
+
23
+ const lines: string[] = [];
24
+
25
+ // Header
26
+ lines.push(`# SEO Analysis Report`);
27
+ lines.push('');
28
+ lines.push(`**Site:** ${report.siteUrl}`);
29
+ lines.push(`**Generated:** ${new Date(report.generatedAt).toLocaleString()}`);
30
+ lines.push(`**Report ID:** ${report.id}`);
31
+ lines.push('');
32
+
33
+ // Summary
34
+ lines.push('## Summary');
35
+ lines.push('');
36
+ lines.push(`| Metric | Value |`);
37
+ lines.push(`|--------|-------|`);
38
+ lines.push(`| Health Score | ${getHealthScoreEmoji(report.summary.healthScore)} **${report.summary.healthScore}/100** |`);
39
+ lines.push(`| Total URLs | ${report.summary.totalUrls} |`);
40
+ lines.push(`| Indexed URLs | ${report.summary.indexedUrls} |`);
41
+ lines.push(`| Not Indexed | ${report.summary.notIndexedUrls} |`);
42
+ lines.push('');
43
+
44
+ // Issues by Severity
45
+ lines.push('### Issues by Severity');
46
+ lines.push('');
47
+ const severities: IssueSeverity[] = ['critical', 'error', 'warning', 'info'];
48
+ for (const severity of severities) {
49
+ const count = report.summary.issuesBySeverity[severity] || 0;
50
+ lines.push(`- ${getSeverityEmoji(severity)} **${capitalize(severity)}:** ${count}`);
51
+ }
52
+ lines.push('');
53
+
54
+ // Issues by Category
55
+ lines.push('### Issues by Category');
56
+ lines.push('');
57
+ const categories = Object.entries(report.summary.issuesByCategory).sort(
58
+ ([, a], [, b]) => b - a
59
+ );
60
+ for (const [category, count] of categories) {
61
+ lines.push(`- ${getCategoryEmoji(category as IssueCategory)} **${formatCategory(category)}:** ${count}`);
62
+ }
63
+ lines.push('');
64
+
65
+ // Recommendations
66
+ lines.push('## Prioritized Recommendations');
67
+ lines.push('');
68
+
69
+ for (const rec of report.recommendations) {
70
+ lines.push(`### ${getPriorityEmoji(rec.priority)} Priority ${rec.priority}: ${rec.title}`);
71
+ lines.push('');
72
+ lines.push(`**Category:** ${formatCategory(rec.category)}`);
73
+ lines.push(`**Impact:** ${capitalize(rec.estimatedImpact)}`);
74
+ lines.push(`**Affected URLs:** ${rec.affectedUrls.length}`);
75
+ lines.push('');
76
+ lines.push(`${rec.description}`);
77
+ lines.push('');
78
+ lines.push('**Action Items:**');
79
+ for (const action of rec.actionItems) {
80
+ lines.push(`- ${action}`);
81
+ }
82
+ lines.push('');
83
+
84
+ if (includeUrls && rec.affectedUrls.length > 0) {
85
+ const urlsToShow = rec.affectedUrls.slice(0, maxUrlsPerIssue);
86
+ lines.push('<details>');
87
+ lines.push(`<summary>Affected URLs (${rec.affectedUrls.length})</summary>`);
88
+ lines.push('');
89
+ for (const url of urlsToShow) {
90
+ lines.push(`- ${url}`);
91
+ }
92
+ if (rec.affectedUrls.length > maxUrlsPerIssue) {
93
+ lines.push(`- ... and ${rec.affectedUrls.length - maxUrlsPerIssue} more`);
94
+ }
95
+ lines.push('</details>');
96
+ lines.push('');
97
+ }
98
+
99
+ lines.push('---');
100
+ lines.push('');
101
+ }
102
+
103
+ // Detailed Issues
104
+ if (includeRawIssues) {
105
+ lines.push('## All Issues');
106
+ lines.push('');
107
+
108
+ const issuesByCategory = groupBy(report.issues, 'category');
109
+
110
+ for (const [category, issues] of Object.entries(issuesByCategory)) {
111
+ lines.push(`### ${getCategoryEmoji(category as IssueCategory)} ${formatCategory(category)}`);
112
+ lines.push('');
113
+
114
+ for (const issue of issues) {
115
+ lines.push(
116
+ `#### ${getSeverityEmoji(issue.severity)} ${issue.title}`
117
+ );
118
+ lines.push('');
119
+ lines.push(`**URL:** \`${issue.url}\``);
120
+ lines.push(`**Severity:** ${capitalize(issue.severity)}`);
121
+ lines.push('');
122
+ lines.push(issue.description);
123
+ lines.push('');
124
+ lines.push(`**Recommendation:** ${issue.recommendation}`);
125
+ lines.push('');
126
+ }
127
+ }
128
+ }
129
+
130
+ // Footer
131
+ lines.push('---');
132
+ lines.push('');
133
+ lines.push('*Report generated by [@djangocfg/seo](https://djangocfg.com)*');
134
+ lines.push('');
135
+ lines.push('> This report is designed to be processed by AI assistants for automated SEO improvements.');
136
+
137
+ return lines.join('\n');
138
+ }
139
+
140
+ /**
141
+ * Generate a concise summary for AI consumption
142
+ */
143
+ export function generateAiSummary(report: SeoReport): string {
144
+ const lines: string[] = [];
145
+
146
+ lines.push('# SEO Report Summary for AI Processing');
147
+ lines.push('');
148
+ lines.push('## Context');
149
+ lines.push(`Site: ${report.siteUrl}`);
150
+ lines.push(`Health Score: ${report.summary.healthScore}/100`);
151
+ lines.push(`Critical Issues: ${report.summary.issuesBySeverity.critical || 0}`);
152
+ lines.push(`Errors: ${report.summary.issuesBySeverity.error || 0}`);
153
+ lines.push(`Warnings: ${report.summary.issuesBySeverity.warning || 0}`);
154
+ lines.push('');
155
+ lines.push('## Top Priority Actions');
156
+ lines.push('');
157
+
158
+ const topRecommendations = report.recommendations.slice(0, 5);
159
+ for (let i = 0; i < topRecommendations.length; i++) {
160
+ const rec = topRecommendations[i];
161
+ if (!rec) continue;
162
+ lines.push(`${i + 1}. **${rec.title}** (${rec.affectedUrls.length} URLs)`);
163
+ lines.push(` - ${rec.actionItems[0]}`);
164
+ }
165
+
166
+ lines.push('');
167
+ lines.push('## Issue Categories');
168
+ lines.push('');
169
+
170
+ const sortedCategories = Object.entries(report.summary.issuesByCategory)
171
+ .sort(([, a], [, b]) => b - a);
172
+
173
+ for (const [category, count] of sortedCategories) {
174
+ lines.push(`- ${formatCategory(category)}: ${count} issues`);
175
+ }
176
+
177
+ return lines.join('\n');
178
+ }
179
+
180
+ // Helper functions
181
+ function getSeverityEmoji(severity: IssueSeverity): string {
182
+ const emojis: Record<IssueSeverity, string> = {
183
+ critical: '🔴',
184
+ error: '🟠',
185
+ warning: '🟡',
186
+ info: '🔵',
187
+ };
188
+ return emojis[severity];
189
+ }
190
+
191
+ function getCategoryEmoji(category: IssueCategory): string {
192
+ const emojis: Record<IssueCategory, string> = {
193
+ indexing: '📑',
194
+ crawling: '🕷️',
195
+ content: '📝',
196
+ technical: '⚙️',
197
+ mobile: '📱',
198
+ performance: '⚡',
199
+ 'structured-data': '🏷️',
200
+ security: '🔒',
201
+ };
202
+ return emojis[category] || '📋';
203
+ }
204
+
205
+ function getPriorityEmoji(priority: number): string {
206
+ const emojis: Record<number, string> = {
207
+ 1: '🚨',
208
+ 2: '⚠️',
209
+ 3: '📌',
210
+ 4: '💡',
211
+ 5: 'ℹ️',
212
+ };
213
+ return emojis[priority] || '📋';
214
+ }
215
+
216
+ function getHealthScoreEmoji(score: number): string {
217
+ if (score >= 90) return '🟢';
218
+ if (score >= 70) return '🟡';
219
+ if (score >= 50) return '🟠';
220
+ return '🔴';
221
+ }
222
+
223
+ function capitalize(str: string): string {
224
+ return str.charAt(0).toUpperCase() + str.slice(1);
225
+ }
226
+
227
+ function formatCategory(category: string): string {
228
+ return category
229
+ .split('-')
230
+ .map(capitalize)
231
+ .join(' ');
232
+ }
233
+
234
+ function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
235
+ return array.reduce(
236
+ (acc, item) => {
237
+ const groupKey = String(item[key]);
238
+ if (!acc[groupKey]) {
239
+ acc[groupKey] = [];
240
+ }
241
+ acc[groupKey].push(item);
242
+ return acc;
243
+ },
244
+ {} as Record<string, T[]>
245
+ );
246
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @djangocfg/seo - Split Report Generator
3
+ * Generates AI-friendly split reports (max 1000 lines each)
4
+ */
5
+
6
+ import { writeFileSync, mkdirSync, existsSync, rmSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import type { SeoReport, SeoIssue, IssueCategory, IssueSeverity } from '../types/index.js';
9
+
10
+ const MAX_LINES = 1000;
11
+
12
+ export interface SplitReportOptions {
13
+ outputDir: string;
14
+ /** Clear output directory before generating */
15
+ clearOutputDir?: boolean;
16
+ }
17
+
18
+ export interface SplitReportResult {
19
+ indexFile: string;
20
+ categoryFiles: string[];
21
+ totalFiles: number;
22
+ }
23
+
24
+ /**
25
+ * Generate split reports for AI processing
26
+ */
27
+ export function generateSplitReports(
28
+ report: SeoReport,
29
+ options: SplitReportOptions
30
+ ): SplitReportResult {
31
+ const { outputDir, clearOutputDir = true } = options;
32
+
33
+ // Clear output directory
34
+ if (clearOutputDir && existsSync(outputDir)) {
35
+ const files = readdirSync(outputDir);
36
+ for (const file of files) {
37
+ if (file.startsWith('seo-') && file.endsWith('.md')) {
38
+ rmSync(join(outputDir, file), { force: true });
39
+ }
40
+ }
41
+ }
42
+
43
+ // Ensure output directory exists
44
+ if (!existsSync(outputDir)) {
45
+ mkdirSync(outputDir, { recursive: true });
46
+ }
47
+
48
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
49
+ const siteName = new URL(report.siteUrl).hostname.replace(/\./g, '-');
50
+ const prefix = `seo-${siteName}-${timestamp}`;
51
+
52
+ const categoryFiles: string[] = [];
53
+
54
+ // Group issues by category
55
+ const issuesByCategory = groupIssuesByCategory(report.issues);
56
+ const categories = Object.keys(issuesByCategory) as IssueCategory[];
57
+
58
+ // Generate category files
59
+ for (const category of categories) {
60
+ const issues = issuesByCategory[category] || [];
61
+ if (issues.length === 0) continue;
62
+
63
+ const chunks = splitIntoChunks(issues, category);
64
+
65
+ for (let i = 0; i < chunks.length; i++) {
66
+ const suffix = chunks.length > 1 ? `-${i + 1}` : '';
67
+ const filename = `${prefix}-${category}${suffix}.md`;
68
+ const filepath = join(outputDir, filename);
69
+
70
+ const chunk = chunks[i];
71
+ if (!chunk) continue;
72
+ const content = generateCategoryFile(report.siteUrl, category, chunk, {
73
+ part: chunks.length > 1 ? i + 1 : undefined,
74
+ totalParts: chunks.length > 1 ? chunks.length : undefined,
75
+ });
76
+
77
+ writeFileSync(filepath, content, 'utf-8');
78
+ categoryFiles.push(filename);
79
+ }
80
+ }
81
+
82
+ // Generate index file
83
+ const indexFilename = `${prefix}-index.md`;
84
+ const indexFilepath = join(outputDir, indexFilename);
85
+ const indexContent = generateIndexFile(report, categoryFiles);
86
+ writeFileSync(indexFilepath, indexContent, 'utf-8');
87
+
88
+ return {
89
+ indexFile: indexFilename,
90
+ categoryFiles,
91
+ totalFiles: categoryFiles.length + 1,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Generate compact index file
97
+ */
98
+ function generateIndexFile(report: SeoReport, categoryFiles: string[]): string {
99
+ const lines: string[] = [];
100
+
101
+ lines.push('# SEO Report Index');
102
+ lines.push('');
103
+ lines.push(`Site: ${report.siteUrl}`);
104
+ lines.push(`Score: ${report.summary.healthScore}/100`);
105
+ lines.push(`Date: ${report.generatedAt.slice(0, 10)}`);
106
+ lines.push('');
107
+
108
+ // Severity counts
109
+ lines.push('## Issues');
110
+ lines.push('');
111
+ lines.push('| Severity | Count |');
112
+ lines.push('|----------|-------|');
113
+ const severities: IssueSeverity[] = ['critical', 'error', 'warning', 'info'];
114
+ for (const sev of severities) {
115
+ const count = report.summary.issuesBySeverity[sev] || 0;
116
+ if (count > 0) {
117
+ lines.push(`| ${sev} | ${count} |`);
118
+ }
119
+ }
120
+ lines.push('');
121
+
122
+ // Top 10 recommendations (compact)
123
+ lines.push('## Actions');
124
+ lines.push('');
125
+ const topRecs = report.recommendations.slice(0, 10);
126
+ for (let i = 0; i < topRecs.length; i++) {
127
+ const rec = topRecs[i];
128
+ if (!rec) continue;
129
+ lines.push(`${i + 1}. **${rec.title}** (${rec.affectedUrls.length})`);
130
+ lines.push(` ${rec.actionItems[0]}`);
131
+ }
132
+ lines.push('');
133
+
134
+ // File index
135
+ lines.push('## Files');
136
+ lines.push('');
137
+ for (const file of categoryFiles) {
138
+ lines.push(`- [${file}](./${file})`);
139
+ }
140
+
141
+ return lines.join('\n');
142
+ }
143
+
144
+ /**
145
+ * Generate compact category file
146
+ */
147
+ function generateCategoryFile(
148
+ siteUrl: string,
149
+ category: IssueCategory,
150
+ issues: SeoIssue[],
151
+ opts: { part?: number; totalParts?: number }
152
+ ): string {
153
+ const lines: string[] = [];
154
+ const partStr = opts.part ? ` (Part ${opts.part}/${opts.totalParts})` : '';
155
+
156
+ lines.push(`# ${formatCategory(category)}${partStr}`);
157
+ lines.push('');
158
+ lines.push(`Site: ${siteUrl}`);
159
+ lines.push(`Issues: ${issues.length}`);
160
+ lines.push('');
161
+
162
+ // Group by title for compact display
163
+ const byTitle = new Map<string, SeoIssue[]>();
164
+ for (const issue of issues) {
165
+ const group = byTitle.get(issue.title) || [];
166
+ group.push(issue);
167
+ byTitle.set(issue.title, group);
168
+ }
169
+
170
+ for (const [title, groupIssues] of byTitle) {
171
+ const first = groupIssues[0];
172
+ if (!first) continue;
173
+
174
+ lines.push(`## ${title}`);
175
+ lines.push('');
176
+ lines.push(`Severity: ${first.severity}`);
177
+ lines.push(`Count: ${groupIssues.length}`);
178
+ lines.push('');
179
+ lines.push(`> ${first.recommendation}`);
180
+ lines.push('');
181
+
182
+ // Compact URL list
183
+ lines.push('URLs:');
184
+ for (const issue of groupIssues.slice(0, 20)) {
185
+ lines.push(`- ${issue.url}`);
186
+ }
187
+ if (groupIssues.length > 20) {
188
+ lines.push(`- ... +${groupIssues.length - 20} more`);
189
+ }
190
+ lines.push('');
191
+ }
192
+
193
+ return lines.join('\n');
194
+ }
195
+
196
+ /**
197
+ * Split issues into chunks that fit within MAX_LINES
198
+ */
199
+ function splitIntoChunks(issues: SeoIssue[], category: IssueCategory): SeoIssue[][] {
200
+ // Estimate lines per issue group (title + severity + recommendation + URLs)
201
+ const byTitle = new Map<string, SeoIssue[]>();
202
+ for (const issue of issues) {
203
+ const group = byTitle.get(issue.title) || [];
204
+ group.push(issue);
205
+ byTitle.set(issue.title, group);
206
+ }
207
+
208
+ const chunks: SeoIssue[][] = [];
209
+ let currentChunk: SeoIssue[] = [];
210
+ let currentLines = 10; // Header overhead
211
+
212
+ for (const [, groupIssues] of byTitle) {
213
+ // Estimate: 6 lines header + min(20, count) URLs + 2 spacing
214
+ const urlCount = Math.min(20, groupIssues.length);
215
+ const groupLines = 8 + urlCount;
216
+
217
+ if (currentLines + groupLines > MAX_LINES && currentChunk.length > 0) {
218
+ chunks.push(currentChunk);
219
+ currentChunk = [];
220
+ currentLines = 10;
221
+ }
222
+
223
+ currentChunk.push(...groupIssues);
224
+ currentLines += groupLines;
225
+ }
226
+
227
+ if (currentChunk.length > 0) {
228
+ chunks.push(currentChunk);
229
+ }
230
+
231
+ return chunks.length > 0 ? chunks : [[]];
232
+ }
233
+
234
+ function groupIssuesByCategory(issues: SeoIssue[]): Record<IssueCategory, SeoIssue[]> {
235
+ const result: Partial<Record<IssueCategory, SeoIssue[]>> = {};
236
+
237
+ for (const issue of issues) {
238
+ if (!result[issue.category]) {
239
+ result[issue.category] = [];
240
+ }
241
+ result[issue.category]!.push(issue);
242
+ }
243
+
244
+ return result as Record<IssueCategory, SeoIssue[]>;
245
+ }
246
+
247
+ function formatCategory(category: string): string {
248
+ return category
249
+ .split('-')
250
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1))
251
+ .join(' ');
252
+ }