@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,76 @@
1
+ #!/usr/bin/env node
2
+ import { SeoIssue } from '../types/index.js';
3
+
4
+ /**
5
+ * @djangocfg/seo - Link Checker
6
+ *
7
+ * Smart link checker using linkinator with proper error handling,
8
+ * timeout management, and filtering of problematic URLs.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { checkLinks } from '@djangocfg/seo/link-checker';
13
+ *
14
+ * const result = await checkLinks({
15
+ * url: 'https://example.com',
16
+ * timeout: 60000,
17
+ * });
18
+ * ```
19
+ *
20
+ * @example CLI
21
+ * ```bash
22
+ * djangocfg-seo links --site https://example.com
23
+ * # or
24
+ * djangocfg-seo links # Interactive mode
25
+ * ```
26
+ */
27
+
28
+ interface CheckLinksOptions {
29
+ /** Base URL to check (defaults to NEXT_PUBLIC_SITE_URL env variable) */
30
+ url?: string;
31
+ /** Timeout in milliseconds (default: 60000) */
32
+ timeout?: number;
33
+ /** URLs to skip (regex pattern) */
34
+ skipPattern?: string;
35
+ /** Show only broken links (default: true) */
36
+ showOnlyBroken?: boolean;
37
+ /** Maximum number of concurrent requests (default: 50) */
38
+ concurrency?: number;
39
+ /** Output file path for report (optional) */
40
+ outputFile?: string;
41
+ /** Report format: 'json', 'markdown', 'text' (default: 'text') */
42
+ reportFormat?: 'json' | 'markdown' | 'text';
43
+ /** Verbose logging (default: true) */
44
+ verbose?: boolean;
45
+ }
46
+ interface BrokenLink {
47
+ url: string;
48
+ status: number | string;
49
+ reason?: string;
50
+ isExternal: boolean;
51
+ sourceUrl?: string;
52
+ }
53
+ interface CheckLinksResult {
54
+ success: boolean;
55
+ broken: number;
56
+ total: number;
57
+ errors: BrokenLink[];
58
+ /** Internal broken links (same domain) */
59
+ internalErrors: BrokenLink[];
60
+ /** External broken links (different domain) */
61
+ externalErrors: BrokenLink[];
62
+ url: string;
63
+ timestamp: string;
64
+ duration?: number;
65
+ }
66
+ /**
67
+ * Check all links on a website
68
+ */
69
+ declare function checkLinks(options: CheckLinksOptions): Promise<CheckLinksResult>;
70
+ /**
71
+ * Convert link check results to SEO issues
72
+ * Separates internal (critical) from external (warning) issues
73
+ */
74
+ declare function linkResultsToSeoIssues(result: CheckLinksResult): SeoIssue[];
75
+
76
+ export { type BrokenLink, type CheckLinksOptions, type CheckLinksResult, checkLinks, linkResultsToSeoIssues };
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from 'fs/promises';
3
+ import * as linkinator from 'linkinator';
4
+ import { dirname } from 'path';
5
+ import chalk from 'chalk';
6
+
7
+ var DEFAULT_SKIP_PATTERN = [
8
+ "github.com",
9
+ "twitter.com",
10
+ "linkedin.com",
11
+ "x.com",
12
+ "127.0.0.1",
13
+ "localhost:[0-9]+",
14
+ "api\\.localhost",
15
+ "demo\\.localhost",
16
+ "cdn-cgi",
17
+ // Cloudflare email protection
18
+ "mailto:",
19
+ // Email links
20
+ "tel:",
21
+ // Phone links
22
+ "javascript:"
23
+ // JavaScript links
24
+ ].join("|");
25
+ function getSiteUrl(options) {
26
+ if (options.url) {
27
+ return options.url;
28
+ }
29
+ const envUrl = process.env.NEXT_PUBLIC_SITE_URL || process.env.SITE_URL || process.env.BASE_URL;
30
+ if (envUrl) {
31
+ return envUrl;
32
+ }
33
+ throw new Error(
34
+ "URL is required. Provide it via options.url or set NEXT_PUBLIC_SITE_URL environment variable."
35
+ );
36
+ }
37
+ function isExternalUrl(linkUrl, baseUrl) {
38
+ try {
39
+ const link = new URL(linkUrl);
40
+ const base = new URL(baseUrl);
41
+ return link.hostname !== base.hostname;
42
+ } catch {
43
+ return true;
44
+ }
45
+ }
46
+ async function checkLinks(options) {
47
+ const url = getSiteUrl(options);
48
+ const {
49
+ timeout = 6e4,
50
+ skipPattern = DEFAULT_SKIP_PATTERN,
51
+ showOnlyBroken = true,
52
+ concurrency = 50,
53
+ outputFile,
54
+ reportFormat = "text",
55
+ verbose = true
56
+ } = options;
57
+ const startTime = Date.now();
58
+ if (verbose) {
59
+ console.log(chalk.cyan(`
60
+ \u{1F50D} Starting link check for: ${chalk.bold(url)}`));
61
+ console.log(chalk.dim(` Timeout: ${timeout}ms | Concurrency: ${concurrency}`));
62
+ console.log("");
63
+ }
64
+ const skipRegex = new RegExp(skipPattern);
65
+ const checkOptions = {
66
+ path: url,
67
+ recurse: true,
68
+ timeout,
69
+ concurrency,
70
+ linksToSkip: (link) => {
71
+ return Promise.resolve(skipRegex.test(link));
72
+ }
73
+ };
74
+ const broken = [];
75
+ const internalErrors = [];
76
+ const externalErrors = [];
77
+ let total = 0;
78
+ try {
79
+ const results = await linkinator.check(checkOptions);
80
+ for (const result2 of results.links) {
81
+ total++;
82
+ const status = result2.status || 0;
83
+ const isExternal = isExternalUrl(result2.url, url);
84
+ if (status < 200 || status >= 400 || result2.state === "BROKEN") {
85
+ const statusValue = status || "TIMEOUT";
86
+ if (statusValue === "TIMEOUT" && isExternal) {
87
+ continue;
88
+ }
89
+ const brokenLink = {
90
+ url: result2.url,
91
+ status: statusValue,
92
+ reason: result2.state === "BROKEN" ? "BROKEN" : void 0,
93
+ isExternal,
94
+ sourceUrl: result2.parent
95
+ };
96
+ broken.push(brokenLink);
97
+ if (isExternal) {
98
+ externalErrors.push(brokenLink);
99
+ } else {
100
+ internalErrors.push(brokenLink);
101
+ }
102
+ }
103
+ }
104
+ const success = internalErrors.length === 0;
105
+ if (!showOnlyBroken || broken.length > 0) {
106
+ if (success && externalErrors.length === 0) {
107
+ console.log(`\u2705 All links are valid!`);
108
+ console.log(` Checked ${total} links.`);
109
+ } else {
110
+ if (internalErrors.length > 0) {
111
+ console.log(chalk.red(`\u274C Found ${internalErrors.length} broken internal links:`));
112
+ for (const { url: linkUrl, status, reason } of internalErrors.slice(0, 20)) {
113
+ console.log(` [${status}] ${linkUrl}${reason ? ` (${reason})` : ""}`);
114
+ }
115
+ if (internalErrors.length > 20) {
116
+ console.log(chalk.dim(` ... and ${internalErrors.length - 20} more`));
117
+ }
118
+ }
119
+ if (externalErrors.length > 0) {
120
+ console.log("");
121
+ console.log(chalk.yellow(`\u26A0\uFE0F Found ${externalErrors.length} broken external links:`));
122
+ for (const { url: linkUrl, status } of externalErrors.slice(0, 10)) {
123
+ console.log(` [${status}] ${linkUrl}`);
124
+ }
125
+ if (externalErrors.length > 10) {
126
+ console.log(chalk.dim(` ... and ${externalErrors.length - 10} more`));
127
+ }
128
+ }
129
+ }
130
+ }
131
+ const duration = Date.now() - startTime;
132
+ const result = {
133
+ success,
134
+ broken: broken.length,
135
+ total,
136
+ errors: broken,
137
+ internalErrors,
138
+ externalErrors,
139
+ url,
140
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
141
+ duration
142
+ };
143
+ if (outputFile) {
144
+ await saveReport(result, outputFile, reportFormat);
145
+ console.log(chalk.green(`
146
+ \u{1F4C4} Report saved to: ${chalk.cyan(outputFile)}`));
147
+ }
148
+ return result;
149
+ } catch (error) {
150
+ const errorMessage = error instanceof Error ? error.message : String(error);
151
+ const errorName = error instanceof Error ? error.name : "UnknownError";
152
+ if (errorMessage.includes("timeout") || errorMessage.includes("TimeoutError") || errorName === "TimeoutError" || errorMessage.includes("aborted")) {
153
+ console.warn(chalk.yellow(`\u26A0\uFE0F Some links timed out after ${timeout}ms`));
154
+ console.warn(chalk.dim(` This is normal for slow or protected URLs.`));
155
+ if (total > 0) {
156
+ console.warn(chalk.dim(` Checked ${total} links before timeout.`));
157
+ }
158
+ if (broken.length > 0) {
159
+ console.log(chalk.red(`
160
+ \u274C Found ${broken.length} broken links:`));
161
+ for (const { url: url2, status, reason } of broken) {
162
+ const statusColor = typeof status === "number" && status >= 500 ? chalk.red : chalk.yellow;
163
+ console.log(
164
+ ` ${statusColor(`[${status}]`)} ${chalk.cyan(url2)}${reason ? chalk.dim(` (${reason})`) : ""}`
165
+ );
166
+ }
167
+ }
168
+ } else {
169
+ console.error(chalk.red(`\u274C Error checking links: ${errorMessage}`));
170
+ }
171
+ const duration = Date.now() - startTime;
172
+ const result = {
173
+ success: internalErrors.length === 0 && total > 0,
174
+ broken: broken.length,
175
+ total,
176
+ errors: broken,
177
+ internalErrors,
178
+ externalErrors,
179
+ url,
180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
181
+ duration
182
+ };
183
+ if (outputFile) {
184
+ try {
185
+ await saveReport(result, outputFile, reportFormat);
186
+ console.log(chalk.green(`
187
+ \u{1F4C4} Report saved to: ${chalk.cyan(outputFile)}`));
188
+ } catch (saveError) {
189
+ console.warn(
190
+ chalk.yellow(
191
+ `
192
+ \u26A0\uFE0F Failed to save report: ${saveError instanceof Error ? saveError.message : String(saveError)}`
193
+ )
194
+ );
195
+ }
196
+ }
197
+ return result;
198
+ }
199
+ }
200
+ function linkResultsToSeoIssues(result) {
201
+ const issues = [];
202
+ for (const error of result.internalErrors) {
203
+ issues.push({
204
+ id: `broken-internal-link-${hash(error.url)}`,
205
+ url: error.url,
206
+ category: "technical",
207
+ severity: typeof error.status === "number" && error.status >= 500 ? "critical" : "error",
208
+ title: `Broken internal link: ${error.status}`,
209
+ description: `Internal link returned ${error.status} status${error.reason ? ` (${error.reason})` : ""}.`,
210
+ recommendation: "Fix the internal link. This affects user experience and SEO.",
211
+ detectedAt: result.timestamp,
212
+ metadata: {
213
+ status: error.status,
214
+ reason: error.reason,
215
+ sourceUrl: error.sourceUrl || result.url,
216
+ isExternal: false
217
+ }
218
+ });
219
+ }
220
+ for (const error of result.externalErrors) {
221
+ issues.push({
222
+ id: `broken-external-link-${hash(error.url)}`,
223
+ url: error.url,
224
+ category: "technical",
225
+ severity: "warning",
226
+ title: `Broken external link: ${error.status}`,
227
+ description: `External link returned ${error.status} status.`,
228
+ recommendation: "Consider removing or updating the external link.",
229
+ detectedAt: result.timestamp,
230
+ metadata: {
231
+ status: error.status,
232
+ reason: error.reason,
233
+ sourceUrl: error.sourceUrl || result.url,
234
+ isExternal: true
235
+ }
236
+ });
237
+ }
238
+ return issues;
239
+ }
240
+ async function saveReport(result, filePath, format) {
241
+ const dir = dirname(filePath);
242
+ if (dir !== ".") {
243
+ await mkdir(dir, { recursive: true });
244
+ }
245
+ let content;
246
+ switch (format) {
247
+ case "json":
248
+ content = JSON.stringify(result, null, 2);
249
+ break;
250
+ case "markdown":
251
+ content = generateMarkdownReport(result);
252
+ break;
253
+ case "text":
254
+ default:
255
+ content = generateTextReport(result);
256
+ break;
257
+ }
258
+ await writeFile(filePath, content, "utf-8");
259
+ }
260
+ function generateMarkdownReport(result) {
261
+ const lines = [];
262
+ lines.push("# Link Check Report");
263
+ lines.push("");
264
+ lines.push(`**URL:** ${result.url}`);
265
+ lines.push(`**Timestamp:** ${result.timestamp}`);
266
+ if (result.duration) {
267
+ lines.push(`**Duration:** ${(result.duration / 1e3).toFixed(2)}s`);
268
+ }
269
+ lines.push("");
270
+ lines.push(
271
+ `**Status:** ${result.success ? "\u2705 All links valid" : "\u274C Broken links found"}`
272
+ );
273
+ lines.push(`**Total links:** ${result.total}`);
274
+ lines.push(`**Broken links:** ${result.broken}`);
275
+ lines.push("");
276
+ if (result.errors.length > 0) {
277
+ lines.push("## Broken Links");
278
+ lines.push("");
279
+ lines.push("| Status | URL | Reason |");
280
+ lines.push("|--------|-----|--------|");
281
+ for (const { url, status, reason } of result.errors) {
282
+ lines.push(`| ${status} | ${url} | ${reason || "-"} |`);
283
+ }
284
+ lines.push("");
285
+ }
286
+ return lines.join("\n");
287
+ }
288
+ function generateTextReport(result) {
289
+ const lines = [];
290
+ lines.push("Link Check Report");
291
+ lines.push("=".repeat(50));
292
+ lines.push(`URL: ${result.url}`);
293
+ lines.push(`Timestamp: ${result.timestamp}`);
294
+ if (result.duration) {
295
+ lines.push(`Duration: ${(result.duration / 1e3).toFixed(2)}s`);
296
+ }
297
+ lines.push("");
298
+ lines.push(
299
+ `Status: ${result.success ? "\u2705 All links valid" : "\u274C Broken links found"}`
300
+ );
301
+ lines.push(`Total links: ${result.total}`);
302
+ lines.push(`Broken links: ${result.broken}`);
303
+ lines.push("");
304
+ if (result.errors.length > 0) {
305
+ lines.push("Broken Links:");
306
+ lines.push("-".repeat(50));
307
+ for (const { url, status, reason } of result.errors) {
308
+ lines.push(`[${status}] ${url}${reason ? ` (${reason})` : ""}`);
309
+ }
310
+ lines.push("");
311
+ }
312
+ return lines.join("\n");
313
+ }
314
+ function hash(str) {
315
+ let h = 0;
316
+ for (let i = 0; i < str.length; i++) {
317
+ const char = str.charCodeAt(i);
318
+ h = (h << 5) - h + char;
319
+ h = h & h;
320
+ }
321
+ return Math.abs(h).toString(36);
322
+ }
323
+
324
+ export { checkLinks, linkResultsToSeoIssues };
325
+ //# sourceMappingURL=index.mjs.map
326
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/link-checker/index.ts"],"names":["result","url"],"mappings":"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","file":"index.mjs","sourcesContent":["#!/usr/bin/env node\n\n/**\n * @djangocfg/seo - Link Checker\n *\n * Smart link checker using linkinator with proper error handling,\n * timeout management, and filtering of problematic URLs.\n *\n * @example\n * ```typescript\n * import { checkLinks } from '@djangocfg/seo/link-checker';\n *\n * const result = await checkLinks({\n * url: 'https://example.com',\n * timeout: 60000,\n * });\n * ```\n *\n * @example CLI\n * ```bash\n * djangocfg-seo links --site https://example.com\n * # or\n * djangocfg-seo links # Interactive mode\n * ```\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport * as linkinator from 'linkinator';\nimport { dirname } from 'node:path';\nimport chalk from 'chalk';\nimport type { CheckOptions } from 'linkinator';\nimport type { SeoIssue } from '../types/index.js';\n\nexport interface CheckLinksOptions {\n /** Base URL to check (defaults to NEXT_PUBLIC_SITE_URL env variable) */\n url?: string;\n /** Timeout in milliseconds (default: 60000) */\n timeout?: number;\n /** URLs to skip (regex pattern) */\n skipPattern?: string;\n /** Show only broken links (default: true) */\n showOnlyBroken?: boolean;\n /** Maximum number of concurrent requests (default: 50) */\n concurrency?: number;\n /** Output file path for report (optional) */\n outputFile?: string;\n /** Report format: 'json', 'markdown', 'text' (default: 'text') */\n reportFormat?: 'json' | 'markdown' | 'text';\n /** Verbose logging (default: true) */\n verbose?: boolean;\n}\n\nconst DEFAULT_SKIP_PATTERN = [\n 'github.com',\n 'twitter.com',\n 'linkedin.com',\n 'x.com',\n '127.0.0.1',\n 'localhost:[0-9]+',\n 'api\\\\.localhost',\n 'demo\\\\.localhost',\n 'cdn-cgi', // Cloudflare email protection\n 'mailto:', // Email links\n 'tel:', // Phone links\n 'javascript:', // JavaScript links\n].join('|');\n\nexport interface BrokenLink {\n url: string;\n status: number | string;\n reason?: string;\n isExternal: boolean;\n sourceUrl?: string;\n}\n\nexport interface CheckLinksResult {\n success: boolean;\n broken: number;\n total: number;\n errors: BrokenLink[];\n /** Internal broken links (same domain) */\n internalErrors: BrokenLink[];\n /** External broken links (different domain) */\n externalErrors: BrokenLink[];\n url: string;\n timestamp: string;\n duration?: number;\n}\n\n/**\n * Get site URL from options or environment variable\n */\nfunction getSiteUrl(options: CheckLinksOptions): string {\n if (options.url) {\n return options.url;\n }\n\n // Try environment variables\n const envUrl =\n process.env.NEXT_PUBLIC_SITE_URL ||\n process.env.SITE_URL ||\n process.env.BASE_URL;\n\n if (envUrl) {\n return envUrl;\n }\n\n throw new Error(\n 'URL is required. Provide it via options.url or set NEXT_PUBLIC_SITE_URL environment variable.'\n );\n}\n\n/**\n * Check if URL is external (different domain)\n */\nfunction isExternalUrl(linkUrl: string, baseUrl: string): boolean {\n try {\n const link = new URL(linkUrl);\n const base = new URL(baseUrl);\n return link.hostname !== base.hostname;\n } catch {\n return true;\n }\n}\n\n/**\n * Check all links on a website\n */\nexport async function checkLinks(options: CheckLinksOptions): Promise<CheckLinksResult> {\n const url = getSiteUrl(options);\n const {\n timeout = 60000,\n skipPattern = DEFAULT_SKIP_PATTERN,\n showOnlyBroken = true,\n concurrency = 50,\n outputFile,\n reportFormat = 'text',\n verbose = true,\n } = options;\n\n const startTime = Date.now();\n\n if (verbose) {\n console.log(chalk.cyan(`\\n🔍 Starting link check for: ${chalk.bold(url)}`));\n console.log(chalk.dim(` Timeout: ${timeout}ms | Concurrency: ${concurrency}`));\n console.log('');\n }\n\n const skipRegex = new RegExp(skipPattern);\n\n const checkOptions: CheckOptions = {\n path: url,\n recurse: true,\n timeout,\n concurrency,\n linksToSkip: (link: string) => {\n return Promise.resolve(skipRegex.test(link));\n },\n };\n\n const broken: BrokenLink[] = [];\n const internalErrors: BrokenLink[] = [];\n const externalErrors: BrokenLink[] = [];\n let total = 0;\n\n try {\n const results = await linkinator.check(checkOptions);\n\n for (const result of results.links) {\n total++;\n const status = result.status || 0;\n const isExternal = isExternalUrl(result.url, url);\n\n if (status < 200 || status >= 400 || result.state === 'BROKEN') {\n const statusValue = status || 'TIMEOUT';\n\n // Skip TIMEOUT errors on external URLs (rate limiting, slow servers)\n if (statusValue === 'TIMEOUT' && isExternal) {\n continue;\n }\n\n const brokenLink: BrokenLink = {\n url: result.url,\n status: statusValue,\n reason: result.state === 'BROKEN' ? 'BROKEN' : undefined,\n isExternal,\n sourceUrl: result.parent,\n };\n\n broken.push(brokenLink);\n\n if (isExternal) {\n externalErrors.push(brokenLink);\n } else {\n internalErrors.push(brokenLink);\n }\n }\n }\n\n const success = internalErrors.length === 0;\n\n if (!showOnlyBroken || broken.length > 0) {\n if (success && externalErrors.length === 0) {\n console.log(`✅ All links are valid!`);\n console.log(` Checked ${total} links.`);\n } else {\n // Show internal errors first (more important)\n if (internalErrors.length > 0) {\n console.log(chalk.red(`❌ Found ${internalErrors.length} broken internal links:`));\n for (const { url: linkUrl, status, reason } of internalErrors.slice(0, 20)) {\n console.log(` [${status}] ${linkUrl}${reason ? ` (${reason})` : ''}`);\n }\n if (internalErrors.length > 20) {\n console.log(chalk.dim(` ... and ${internalErrors.length - 20} more`));\n }\n }\n\n // Show external errors (less critical)\n if (externalErrors.length > 0) {\n console.log('');\n console.log(chalk.yellow(`⚠️ Found ${externalErrors.length} broken external links:`));\n for (const { url: linkUrl, status } of externalErrors.slice(0, 10)) {\n console.log(` [${status}] ${linkUrl}`);\n }\n if (externalErrors.length > 10) {\n console.log(chalk.dim(` ... and ${externalErrors.length - 10} more`));\n }\n }\n }\n }\n\n const duration = Date.now() - startTime;\n const result: CheckLinksResult = {\n success,\n broken: broken.length,\n total,\n errors: broken,\n internalErrors,\n externalErrors,\n url,\n timestamp: new Date().toISOString(),\n duration,\n };\n\n if (outputFile) {\n await saveReport(result, outputFile, reportFormat);\n console.log(chalk.green(`\\n📄 Report saved to: ${chalk.cyan(outputFile)}`));\n }\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorName = error instanceof Error ? error.name : 'UnknownError';\n\n if (\n errorMessage.includes('timeout') ||\n errorMessage.includes('TimeoutError') ||\n errorName === 'TimeoutError' ||\n errorMessage.includes('aborted')\n ) {\n console.warn(chalk.yellow(`⚠️ Some links timed out after ${timeout}ms`));\n console.warn(chalk.dim(` This is normal for slow or protected URLs.`));\n if (total > 0) {\n console.warn(chalk.dim(` Checked ${total} links before timeout.`));\n }\n\n if (broken.length > 0) {\n console.log(chalk.red(`\\n❌ Found ${broken.length} broken links:`));\n for (const { url, status, reason } of broken) {\n const statusColor =\n typeof status === 'number' && status >= 500 ? chalk.red : chalk.yellow;\n console.log(\n ` ${statusColor(`[${status}]`)} ${chalk.cyan(url)}${reason ? chalk.dim(` (${reason})`) : ''}`\n );\n }\n }\n } else {\n console.error(chalk.red(`❌ Error checking links: ${errorMessage}`));\n }\n\n const duration = Date.now() - startTime;\n const result: CheckLinksResult = {\n success: internalErrors.length === 0 && total > 0,\n broken: broken.length,\n total,\n errors: broken,\n internalErrors,\n externalErrors,\n url,\n timestamp: new Date().toISOString(),\n duration,\n };\n\n if (outputFile) {\n try {\n await saveReport(result, outputFile, reportFormat);\n console.log(chalk.green(`\\n📄 Report saved to: ${chalk.cyan(outputFile)}`));\n } catch (saveError) {\n console.warn(\n chalk.yellow(\n `\\n⚠️ Failed to save report: ${saveError instanceof Error ? saveError.message : String(saveError)}`\n )\n );\n }\n }\n\n return result;\n }\n}\n\n/**\n * Convert link check results to SEO issues\n * Separates internal (critical) from external (warning) issues\n */\nexport function linkResultsToSeoIssues(result: CheckLinksResult): SeoIssue[] {\n const issues: SeoIssue[] = [];\n\n // Internal broken links are more critical\n for (const error of result.internalErrors) {\n issues.push({\n id: `broken-internal-link-${hash(error.url)}`,\n url: error.url,\n category: 'technical' as const,\n severity: typeof error.status === 'number' && error.status >= 500 ? 'critical' as const : 'error' as const,\n title: `Broken internal link: ${error.status}`,\n description: `Internal link returned ${error.status} status${error.reason ? ` (${error.reason})` : ''}.`,\n recommendation: 'Fix the internal link. This affects user experience and SEO.',\n detectedAt: result.timestamp,\n metadata: {\n status: error.status,\n reason: error.reason,\n sourceUrl: error.sourceUrl || result.url,\n isExternal: false,\n },\n });\n }\n\n // External broken links are warnings\n for (const error of result.externalErrors) {\n issues.push({\n id: `broken-external-link-${hash(error.url)}`,\n url: error.url,\n category: 'technical' as const,\n severity: 'warning' as const,\n title: `Broken external link: ${error.status}`,\n description: `External link returned ${error.status} status.`,\n recommendation: 'Consider removing or updating the external link.',\n detectedAt: result.timestamp,\n metadata: {\n status: error.status,\n reason: error.reason,\n sourceUrl: error.sourceUrl || result.url,\n isExternal: true,\n },\n });\n }\n\n return issues;\n}\n\nasync function saveReport(\n result: CheckLinksResult,\n filePath: string,\n format: 'json' | 'markdown' | 'text'\n): Promise<void> {\n const dir = dirname(filePath);\n if (dir !== '.') {\n await mkdir(dir, { recursive: true });\n }\n\n let content: string;\n\n switch (format) {\n case 'json':\n content = JSON.stringify(result, null, 2);\n break;\n\n case 'markdown':\n content = generateMarkdownReport(result);\n break;\n\n case 'text':\n default:\n content = generateTextReport(result);\n break;\n }\n\n await writeFile(filePath, content, 'utf-8');\n}\n\nfunction generateMarkdownReport(result: CheckLinksResult): string {\n const lines: string[] = [];\n\n lines.push('# Link Check Report');\n lines.push('');\n lines.push(`**URL:** ${result.url}`);\n lines.push(`**Timestamp:** ${result.timestamp}`);\n if (result.duration) {\n lines.push(`**Duration:** ${(result.duration / 1000).toFixed(2)}s`);\n }\n lines.push('');\n lines.push(\n `**Status:** ${result.success ? '✅ All links valid' : '❌ Broken links found'}`\n );\n lines.push(`**Total links:** ${result.total}`);\n lines.push(`**Broken links:** ${result.broken}`);\n lines.push('');\n\n if (result.errors.length > 0) {\n lines.push('## Broken Links');\n lines.push('');\n lines.push('| Status | URL | Reason |');\n lines.push('|--------|-----|--------|');\n for (const { url, status, reason } of result.errors) {\n lines.push(`| ${status} | ${url} | ${reason || '-'} |`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction generateTextReport(result: CheckLinksResult): string {\n const lines: string[] = [];\n\n lines.push('Link Check Report');\n lines.push('='.repeat(50));\n lines.push(`URL: ${result.url}`);\n lines.push(`Timestamp: ${result.timestamp}`);\n if (result.duration) {\n lines.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);\n }\n lines.push('');\n lines.push(\n `Status: ${result.success ? '✅ All links valid' : '❌ Broken links found'}`\n );\n lines.push(`Total links: ${result.total}`);\n lines.push(`Broken links: ${result.broken}`);\n lines.push('');\n\n if (result.errors.length > 0) {\n lines.push('Broken Links:');\n lines.push('-'.repeat(50));\n for (const { url, status, reason } of result.errors) {\n lines.push(`[${status}] ${url}${reason ? ` (${reason})` : ''}`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction hash(str: string): string {\n let h = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n h = (h << 5) - h + char;\n h = h & h;\n }\n return Math.abs(h).toString(36);\n}\n"]}
@@ -0,0 +1,193 @@
1
+ import { SeoIssue, UrlInspectionResult, CrawlResult, SeoReport } from './types/index.js';
2
+
3
+ /**
4
+ * @djangocfg/seo - Report Generator
5
+ * Main report generation orchestrator
6
+ */
7
+
8
+ interface ReportGeneratorOptions {
9
+ outputDir: string;
10
+ formats: ('json' | 'markdown' | 'ai-summary' | 'split')[];
11
+ includeRawData?: boolean;
12
+ timestamp?: boolean;
13
+ /** Clear output directory before generating reports */
14
+ clearOutputDir?: boolean;
15
+ /** Maximum URLs to show per issue group (default: 10) */
16
+ maxUrlsPerIssue?: number;
17
+ }
18
+ interface GeneratedReports {
19
+ report: SeoReport;
20
+ files: {
21
+ json?: string;
22
+ markdown?: string;
23
+ aiSummary?: string;
24
+ split?: {
25
+ index: string;
26
+ categories: string[];
27
+ };
28
+ };
29
+ }
30
+ /**
31
+ * Generate and save SEO reports
32
+ */
33
+ declare function generateAndSaveReports(siteUrl: string, data: {
34
+ issues: SeoIssue[];
35
+ urlInspections?: UrlInspectionResult[];
36
+ crawlResults?: CrawlResult[];
37
+ }, options: ReportGeneratorOptions): Promise<GeneratedReports>;
38
+ /**
39
+ * Print report summary to console
40
+ */
41
+ declare function printReportSummary(report: SeoReport): void;
42
+ /**
43
+ * Merge multiple reports into one
44
+ */
45
+ declare function mergeReports(reports: SeoReport[]): SeoReport;
46
+
47
+ /**
48
+ * @djangocfg/seo - JSON Report Generator
49
+ * Generate AI-friendly JSON reports
50
+ */
51
+
52
+ interface JsonReportOptions {
53
+ includeRawData?: boolean;
54
+ prettyPrint?: boolean;
55
+ /** Maximum URLs to include per issue group (default: 10) */
56
+ maxUrlsPerIssue?: number;
57
+ }
58
+ /**
59
+ * Generate a comprehensive JSON report
60
+ */
61
+ declare function generateJsonReport(siteUrl: string, data: {
62
+ issues: SeoIssue[];
63
+ urlInspections?: UrlInspectionResult[];
64
+ crawlResults?: CrawlResult[];
65
+ }, options?: JsonReportOptions): SeoReport;
66
+ /**
67
+ * Export report as JSON string
68
+ */
69
+ declare function exportJsonReport(report: SeoReport, pretty?: boolean): string;
70
+ /**
71
+ * Generate AI-optimized schema for the report
72
+ * This schema helps AI understand the report structure
73
+ */
74
+ declare const AI_REPORT_SCHEMA: {
75
+ $schema: string;
76
+ title: string;
77
+ description: string;
78
+ type: string;
79
+ properties: {
80
+ id: {
81
+ type: string;
82
+ description: string;
83
+ };
84
+ siteUrl: {
85
+ type: string;
86
+ description: string;
87
+ };
88
+ generatedAt: {
89
+ type: string;
90
+ format: string;
91
+ };
92
+ summary: {
93
+ type: string;
94
+ description: string;
95
+ properties: {
96
+ totalUrls: {
97
+ type: string;
98
+ };
99
+ indexedUrls: {
100
+ type: string;
101
+ };
102
+ notIndexedUrls: {
103
+ type: string;
104
+ };
105
+ healthScore: {
106
+ type: string;
107
+ minimum: number;
108
+ maximum: number;
109
+ description: string;
110
+ };
111
+ };
112
+ };
113
+ issues: {
114
+ type: string;
115
+ description: string;
116
+ items: {
117
+ type: string;
118
+ properties: {
119
+ severity: {
120
+ type: string;
121
+ enum: string[];
122
+ };
123
+ category: {
124
+ type: string;
125
+ enum: string[];
126
+ };
127
+ title: {
128
+ type: string;
129
+ };
130
+ description: {
131
+ type: string;
132
+ };
133
+ recommendation: {
134
+ type: string;
135
+ };
136
+ url: {
137
+ type: string;
138
+ };
139
+ };
140
+ };
141
+ };
142
+ recommendations: {
143
+ type: string;
144
+ description: string;
145
+ items: {
146
+ type: string;
147
+ properties: {
148
+ priority: {
149
+ type: string;
150
+ minimum: number;
151
+ maximum: number;
152
+ };
153
+ title: {
154
+ type: string;
155
+ };
156
+ affectedUrls: {
157
+ type: string;
158
+ items: {
159
+ type: string;
160
+ };
161
+ };
162
+ actionItems: {
163
+ type: string;
164
+ items: {
165
+ type: string;
166
+ };
167
+ };
168
+ };
169
+ };
170
+ };
171
+ };
172
+ };
173
+
174
+ /**
175
+ * @djangocfg/seo - Markdown Report Generator
176
+ * Generate human-readable Markdown reports
177
+ */
178
+
179
+ interface MarkdownReportOptions {
180
+ includeRawIssues?: boolean;
181
+ includeUrls?: boolean;
182
+ maxUrlsPerIssue?: number;
183
+ }
184
+ /**
185
+ * Generate a Markdown report from SEO data
186
+ */
187
+ declare function generateMarkdownReport(report: SeoReport, options?: MarkdownReportOptions): string;
188
+ /**
189
+ * Generate a concise summary for AI consumption
190
+ */
191
+ declare function generateAiSummary(report: SeoReport): string;
192
+
193
+ export { AI_REPORT_SCHEMA as A, type GeneratedReports as G, type JsonReportOptions as J, type MarkdownReportOptions as M, type ReportGeneratorOptions as R, generateJsonReport as a, generateMarkdownReport as b, generateAiSummary as c, exportJsonReport as e, generateAndSaveReports as g, mergeReports as m, printReportSummary as p };
@@ -0,0 +1,24 @@
1
+ export { A as AI_REPORT_SCHEMA, G as GeneratedReports, J as JsonReportOptions, M as MarkdownReportOptions, R as ReportGeneratorOptions, e as exportJsonReport, c as generateAiSummary, g as generateAndSaveReports, a as generateJsonReport, b as generateMarkdownReport, m as mergeReports, p as printReportSummary } from '../markdown-report-B3QdDzxE.js';
2
+ import { SeoReport } from '../types/index.js';
3
+
4
+ /**
5
+ * @djangocfg/seo - Split Report Generator
6
+ * Generates AI-friendly split reports (max 1000 lines each)
7
+ */
8
+
9
+ interface SplitReportOptions {
10
+ outputDir: string;
11
+ /** Clear output directory before generating */
12
+ clearOutputDir?: boolean;
13
+ }
14
+ interface SplitReportResult {
15
+ indexFile: string;
16
+ categoryFiles: string[];
17
+ totalFiles: number;
18
+ }
19
+ /**
20
+ * Generate split reports for AI processing
21
+ */
22
+ declare function generateSplitReports(report: SeoReport, options: SplitReportOptions): SplitReportResult;
23
+
24
+ export { type SplitReportOptions, type SplitReportResult, generateSplitReports };