@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,321 @@
1
+ /**
2
+ * Universal Sitemap Generator for Next.js App Router projects
3
+ * Supports both Nextra (MDX content) and standard Next.js projects
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import type { SitemapItem, SitemapData, ContentConfig } from './types.js';
9
+ import { scanRoutes, type RouteInfo } from '../routes/scanner.js';
10
+ import { detectProjectType } from './scanner.js';
11
+
12
+ /**
13
+ * Dynamically import _meta.ts file (Nextra pattern)
14
+ */
15
+ async function getMeta(dir: string): Promise<Record<string, unknown>> {
16
+ const metaPath = path.join(dir, '_meta.ts');
17
+ if (!fs.existsSync(metaPath)) return {};
18
+
19
+ try {
20
+ const meta = await import(metaPath);
21
+ return meta.default || {};
22
+ } catch {
23
+ // Fallback: try to parse with regex for simple cases
24
+ try {
25
+ const content = fs.readFileSync(metaPath, 'utf-8');
26
+ const matches = content.matchAll(/'([^']+)':\s*['"]([^'"]+)['"]/g);
27
+ const result: Record<string, string> = {};
28
+ for (const match of matches) {
29
+ const key = match[1];
30
+ const value = match[2];
31
+ if (key && value) {
32
+ result[key] = value;
33
+ }
34
+ }
35
+ return result;
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get title from meta object
44
+ */
45
+ function getTitleFromMeta(key: string, meta: Record<string, unknown>): string {
46
+ const value = meta[key];
47
+ if (!value) return key;
48
+ if (typeof value === 'string') return value;
49
+ if (typeof value === 'object' && value !== null && 'title' in value) {
50
+ return String((value as { title: unknown }).title);
51
+ }
52
+ return key;
53
+ }
54
+
55
+ /**
56
+ * Scan Nextra content directory recursively
57
+ */
58
+ async function scanContent(
59
+ dir: string,
60
+ baseUrl: string = '/docs'
61
+ ): Promise<SitemapItem[]> {
62
+ const items: SitemapItem[] = [];
63
+
64
+ if (!fs.existsSync(dir)) return items;
65
+
66
+ const meta = await getMeta(dir);
67
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
68
+
69
+ // Build file map
70
+ const fileMap = new Map<string, fs.Dirent>();
71
+ for (const entry of entries) {
72
+ const key = entry.name.replace(/\.mdx?$/, '');
73
+ fileMap.set(key, entry);
74
+ }
75
+
76
+ // Process meta keys first to preserve order
77
+ const metaKeys = Object.keys(meta);
78
+ for (const key of metaKeys) {
79
+ const entry = fileMap.get(key);
80
+ if (!entry) continue;
81
+
82
+ fileMap.delete(key);
83
+ const fullPath = path.join(dir, entry.name);
84
+ const itemPath = path.join(baseUrl, key).replace(/\\/g, '/');
85
+
86
+ if (entry.isDirectory()) {
87
+ const children = await scanContent(fullPath, itemPath);
88
+ items.push({
89
+ title: getTitleFromMeta(key, meta),
90
+ path: itemPath,
91
+ children: children.length > 0 ? children : undefined,
92
+ });
93
+ } else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
94
+ if (entry.name !== 'index.mdx' && entry.name !== 'index.md') {
95
+ items.push({
96
+ title: getTitleFromMeta(key, meta),
97
+ path: itemPath,
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ // Process remaining entries not in meta
104
+ for (const [key, entry] of fileMap.entries()) {
105
+ if (key.startsWith('_') || key.startsWith('.')) continue;
106
+
107
+ const fullPath = path.join(dir, entry.name);
108
+ const itemPath = path.join(baseUrl, key).replace(/\\/g, '/');
109
+
110
+ if (entry.isDirectory()) {
111
+ const children = await scanContent(fullPath, itemPath);
112
+ items.push({
113
+ title: key,
114
+ path: itemPath,
115
+ children: children.length > 0 ? children : undefined,
116
+ });
117
+ } else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
118
+ if (entry.name !== 'index.mdx' && entry.name !== 'index.md') {
119
+ items.push({
120
+ title: key,
121
+ path: itemPath,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ return items;
128
+ }
129
+
130
+ /**
131
+ * Convert route path to title
132
+ */
133
+ function routeToTitle(routePath: string): string {
134
+ const segment = routePath.split('/').filter(Boolean).pop() || 'Home';
135
+ // Skip dynamic segments
136
+ if (segment.startsWith('[')) return segment;
137
+ // Capitalize and replace hyphens
138
+ return segment
139
+ .split('-')
140
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
141
+ .join(' ');
142
+ }
143
+
144
+ /**
145
+ * Convert routes to sitemap items (tree structure)
146
+ * Uses existing routes/scanner.ts for universal Next.js support
147
+ */
148
+ function routesToSitemapItems(routes: RouteInfo[]): SitemapItem[] {
149
+ // Build tree from flat routes
150
+ const root: Map<string, SitemapItem> = new Map();
151
+ const items: SitemapItem[] = [];
152
+
153
+ // Sort routes by path depth
154
+ const sortedRoutes = [...routes]
155
+ .filter(r => r.type === 'page' && !r.isDynamic) // Only static pages
156
+ .sort((a, b) => a.path.split('/').length - b.path.split('/').length);
157
+
158
+ for (const route of sortedRoutes) {
159
+ const segments = route.path.split('/').filter(Boolean);
160
+
161
+ if (segments.length === 0) {
162
+ // Root path
163
+ items.push({
164
+ title: 'Home',
165
+ path: '/',
166
+ });
167
+ continue;
168
+ }
169
+
170
+ // Find or create parent
171
+ let currentPath = '';
172
+ let parentChildren: SitemapItem[] = items;
173
+
174
+ for (let i = 0; i < segments.length - 1; i++) {
175
+ currentPath += '/' + segments[i];
176
+ let parent = root.get(currentPath);
177
+
178
+ if (!parent) {
179
+ parent = {
180
+ title: routeToTitle(currentPath),
181
+ path: currentPath,
182
+ children: [],
183
+ };
184
+ root.set(currentPath, parent);
185
+ parentChildren.push(parent);
186
+ }
187
+
188
+ parentChildren = parent.children || (parent.children = []);
189
+ }
190
+
191
+ // Add current item
192
+ const item: SitemapItem = {
193
+ title: routeToTitle(route.path),
194
+ path: route.path,
195
+ };
196
+ parentChildren.push(item);
197
+ }
198
+
199
+ return items;
200
+ }
201
+
202
+ /**
203
+ * Generate sitemap data from project
204
+ * Auto-detects project type (Nextra vs standard Next.js)
205
+ */
206
+ export async function generateSitemapData(
207
+ cwd: string,
208
+ config?: Partial<ContentConfig>
209
+ ): Promise<SitemapData> {
210
+ const projectType = detectProjectType(cwd);
211
+ const contentDir = path.join(cwd, config?.contentDir || 'content');
212
+ const appDir = path.join(cwd, config?.appDir || 'app');
213
+ const basePath = config?.basePath || '/docs';
214
+
215
+ let docsItems: SitemapItem[] = [];
216
+ let appItems: SitemapItem[] = [];
217
+
218
+ // Scan content/ for Nextra projects
219
+ if (projectType === 'nextra' && fs.existsSync(contentDir)) {
220
+ docsItems = await scanContent(contentDir, basePath);
221
+ }
222
+
223
+ // Scan app/ using routes scanner (universal)
224
+ if (fs.existsSync(appDir)) {
225
+ try {
226
+ const scanResult = scanRoutes({ appDir, includeApi: false });
227
+ appItems = routesToSitemapItems(scanResult.routes);
228
+ } catch {
229
+ // Fallback to simple scan if routes scanner fails
230
+ appItems = [];
231
+ }
232
+ }
233
+
234
+ return {
235
+ app: appItems,
236
+ docs: docsItems,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Generate TypeScript file content
242
+ */
243
+ function generateTsContent(data: SitemapData): string {
244
+ return `
245
+ // This file is auto-generated by @djangocfg/seo
246
+ // Do not edit manually
247
+
248
+ export interface SitemapItem {
249
+ title: string;
250
+ path: string;
251
+ children?: SitemapItem[];
252
+ }
253
+
254
+ export const sitemap: { app: SitemapItem[], docs: SitemapItem[] } = ${JSON.stringify(data, null, 2)};
255
+ `;
256
+ }
257
+
258
+ /**
259
+ * Generate and save sitemap file
260
+ */
261
+ export async function generateSitemap(
262
+ cwd: string,
263
+ options: {
264
+ output?: string;
265
+ config?: Partial<ContentConfig>;
266
+ } = {}
267
+ ): Promise<{ outputPath: string; data: SitemapData }> {
268
+ const output = options.output || 'app/_core/sitemap.ts';
269
+ const outputPath = path.join(cwd, output);
270
+
271
+ // Generate data
272
+ const data = await generateSitemapData(cwd, options.config);
273
+
274
+ // Ensure output directory exists
275
+ const outputDir = path.dirname(outputPath);
276
+ if (!fs.existsSync(outputDir)) {
277
+ fs.mkdirSync(outputDir, { recursive: true });
278
+ }
279
+
280
+ // Write file
281
+ const content = generateTsContent(data);
282
+ fs.writeFileSync(outputPath, content);
283
+
284
+ return { outputPath, data };
285
+ }
286
+
287
+ /**
288
+ * Flatten sitemap items to list of paths
289
+ */
290
+ export function flattenSitemap(items: SitemapItem[]): string[] {
291
+ const paths: string[] = [];
292
+
293
+ function traverse(item: SitemapItem) {
294
+ paths.push(item.path);
295
+ if (item.children) {
296
+ for (const child of item.children) {
297
+ traverse(child);
298
+ }
299
+ }
300
+ }
301
+
302
+ for (const item of items) {
303
+ traverse(item);
304
+ }
305
+
306
+ return paths;
307
+ }
308
+
309
+ /**
310
+ * Count total items in sitemap
311
+ */
312
+ export function countSitemapItems(data: SitemapData): { app: number; docs: number; total: number } {
313
+ const appPaths = flattenSitemap(data.app);
314
+ const docsPaths = flattenSitemap(data.docs);
315
+
316
+ return {
317
+ app: appPaths.length,
318
+ docs: docsPaths.length,
319
+ total: appPaths.length + docsPaths.length,
320
+ };
321
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Content module types for MDX/Nextra projects
3
+ */
4
+
5
+ export interface ContentConfig {
6
+ /** Content directory path (default: 'content') */
7
+ contentDir: string;
8
+ /** App directory path (default: 'app') */
9
+ appDir: string;
10
+ /** Base URL path for docs (default: '/docs') */
11
+ basePath: string;
12
+ /** File extensions to scan (default: ['.mdx', '.md']) */
13
+ extensions: string[];
14
+ /** Asset extensions to ignore in links */
15
+ assetExtensions: string[];
16
+ }
17
+
18
+ export interface SitemapConfig {
19
+ /** Output file path (default: 'app/_core/sitemap.ts') */
20
+ output: string;
21
+ /** Include app pages in sitemap */
22
+ includeApp: boolean;
23
+ /** Include content pages in sitemap */
24
+ includeContent: boolean;
25
+ }
26
+
27
+ export interface SitemapItem {
28
+ title: string;
29
+ path: string;
30
+ children?: SitemapItem[];
31
+ }
32
+
33
+ export interface SitemapData {
34
+ app: SitemapItem[];
35
+ docs: SitemapItem[];
36
+ }
37
+
38
+ export interface FileInfo {
39
+ /** Full path to the file */
40
+ fullPath: string;
41
+ /** Path relative to content directory */
42
+ relativePath: string;
43
+ /** Whether this is an index file */
44
+ isIndex: boolean;
45
+ /** Parent folder path */
46
+ folder: string;
47
+ /** File name without extension */
48
+ name: string;
49
+ }
50
+
51
+ export type LinkType = 'absolute' | 'dotslash' | 'parent' | 'simple';
52
+
53
+ export interface ExtractedLink {
54
+ /** Raw link as found in file */
55
+ raw: string;
56
+ /** Resolved absolute path */
57
+ resolved: string;
58
+ /** Type of link syntax */
59
+ type: LinkType;
60
+ /** Line number in file */
61
+ line: number;
62
+ }
63
+
64
+ export interface BrokenLink {
65
+ /** File containing the broken link */
66
+ file: string;
67
+ /** Full link path (e.g., /docs/path) */
68
+ link: string;
69
+ /** Link type */
70
+ type: LinkType;
71
+ /** Raw link text */
72
+ raw: string;
73
+ /** Line number */
74
+ line: number;
75
+ }
76
+
77
+ export interface LinkCheckResult {
78
+ /** Total files checked */
79
+ filesChecked: number;
80
+ /** Unique links checked */
81
+ uniqueLinks: number;
82
+ /** Broken links found */
83
+ brokenLinks: BrokenLink[];
84
+ /** All links are valid */
85
+ success: boolean;
86
+ }
87
+
88
+ export interface LinkFix {
89
+ /** Original link */
90
+ from: string;
91
+ /** Replacement link */
92
+ to: string;
93
+ /** Line number */
94
+ line: number;
95
+ }
96
+
97
+ export interface LinkFixResult {
98
+ /** File path */
99
+ file: string;
100
+ /** Full path to file */
101
+ fullPath: string;
102
+ /** Fixes applied/suggested */
103
+ fixes: LinkFix[];
104
+ }
105
+
106
+ export interface FixLinksResult {
107
+ /** Total changes made/suggested */
108
+ totalChanges: number;
109
+ /** Files with changes */
110
+ fileChanges: LinkFixResult[];
111
+ /** Whether fixes were applied */
112
+ applied: boolean;
113
+ }
114
+
115
+ export interface ContentScanResult {
116
+ /** Project type detected */
117
+ projectType: 'nextra' | 'nextjs' | 'unknown';
118
+ /** Content directory found */
119
+ hasContent: boolean;
120
+ /** App directory found */
121
+ hasApp: boolean;
122
+ /** MDX files found */
123
+ mdxFiles: string[];
124
+ /** Page files found in app/ */
125
+ pageFiles: string[];
126
+ }
127
+
128
+ export const DEFAULT_CONFIG: ContentConfig = {
129
+ contentDir: 'content',
130
+ appDir: 'app',
131
+ basePath: '/docs',
132
+ extensions: ['.mdx', '.md'],
133
+ assetExtensions: ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.pdf', '.zip', '.tar', '.gz'],
134
+ };
135
+
136
+ export const DEFAULT_SITEMAP_CONFIG: SitemapConfig = {
137
+ output: 'app/_core/sitemap.ts',
138
+ includeApp: true,
139
+ includeContent: true,
140
+ };