@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,126 @@
1
+ /**
2
+ * Routes Command - Scan app/ directory
3
+ */
4
+
5
+ import consola from 'consola';
6
+ import chalk from 'chalk';
7
+ import { getSiteUrl } from '../../config.js';
8
+ import { analyzeSitemap } from '../../crawler/index.js';
9
+ import { scanRoutes, findAppDir, compareWithSitemap, verifyRoutes, generateRoutesSummary } from '../../routes/index.js';
10
+ import type { CliOptions } from '../types.js';
11
+
12
+ export async function runRoutes(options: CliOptions) {
13
+ const siteUrl = getSiteUrl(options);
14
+ const appDir = options['app-dir'] || findAppDir();
15
+
16
+ if (!appDir) {
17
+ consola.error('Could not find app/ directory. Use --app-dir to specify path.');
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log('');
22
+ consola.box(`${chalk.bold('Routes Scanner')}\n${appDir}`);
23
+
24
+ // Scan routes
25
+ consola.start('Scanning app/ directory...');
26
+ const scanResult = scanRoutes({ appDir });
27
+
28
+ consola.success(`Found ${scanResult.routes.length} routes`);
29
+ console.log(` ├── Static: ${scanResult.staticRoutes.length}`);
30
+ console.log(` ├── Dynamic: ${scanResult.dynamicRoutes.length}`);
31
+ console.log(` └── API: ${scanResult.apiRoutes.length}`);
32
+
33
+ // Show routes
34
+ if (scanResult.staticRoutes.length > 0) {
35
+ console.log('');
36
+ consola.info('Static routes:');
37
+ for (const route of scanResult.staticRoutes.slice(0, 20)) {
38
+ console.log(` ${chalk.green('→')} ${route.path}`);
39
+ }
40
+ if (scanResult.staticRoutes.length > 20) {
41
+ console.log(` ${chalk.dim(`... +${scanResult.staticRoutes.length - 20} more`)}`);
42
+ }
43
+ }
44
+
45
+ if (scanResult.dynamicRoutes.length > 0) {
46
+ console.log('');
47
+ consola.info('Dynamic routes:');
48
+ for (const route of scanResult.dynamicRoutes.slice(0, 10)) {
49
+ const params = route.dynamicSegments.join(', ');
50
+ console.log(` ${chalk.yellow('→')} ${route.path} ${chalk.dim(`[${params}]`)}`);
51
+ }
52
+ if (scanResult.dynamicRoutes.length > 10) {
53
+ console.log(` ${chalk.dim(`... +${scanResult.dynamicRoutes.length - 10} more`)}`);
54
+ }
55
+ }
56
+
57
+ // Compare with sitemap (--check flag)
58
+ if (options.check) {
59
+ console.log('');
60
+ consola.start('Loading sitemap...');
61
+
62
+ try {
63
+ const sitemapUrl = new URL('/sitemap.xml', siteUrl).href;
64
+ const sitemap = await analyzeSitemap(sitemapUrl);
65
+ const sitemapUrls = sitemap.urls.map(u => u.loc);
66
+
67
+ consola.success(`Loaded ${sitemapUrls.length} URLs from sitemap`);
68
+
69
+ const comparison = compareWithSitemap(scanResult, sitemapUrls, siteUrl);
70
+
71
+ console.log('');
72
+ consola.info('Sitemap comparison:');
73
+ console.log(` ├── Matching: ${comparison.matching.length}`);
74
+ console.log(` ├── Missing from sitemap: ${chalk.yellow(String(comparison.missingFromSitemap.length))}`);
75
+ console.log(` └── Extra in sitemap: ${comparison.extraInSitemap.length}`);
76
+
77
+ if (comparison.missingFromSitemap.length > 0) {
78
+ console.log('');
79
+ consola.warn('Routes missing from sitemap:');
80
+ for (const route of comparison.missingFromSitemap.slice(0, 10)) {
81
+ console.log(` ${chalk.red('✗')} ${route.path}`);
82
+ }
83
+ if (comparison.missingFromSitemap.length > 10) {
84
+ console.log(` ${chalk.dim(`... +${comparison.missingFromSitemap.length - 10} more`)}`);
85
+ }
86
+ }
87
+ } catch (error) {
88
+ consola.error(`Failed to load sitemap: ${(error as Error).message}`);
89
+ }
90
+ }
91
+
92
+ // Verify routes (--verify flag)
93
+ if (options.verify) {
94
+ console.log('');
95
+ consola.start('Verifying routes...');
96
+
97
+ const verification = await verifyRoutes(scanResult, {
98
+ baseUrl: siteUrl,
99
+ timeout: parseInt(options.timeout, 10),
100
+ concurrency: 5,
101
+ staticOnly: true,
102
+ });
103
+
104
+ const accessible = verification.filter(r => r.isAccessible);
105
+ const broken = verification.filter(r => !r.isAccessible);
106
+
107
+ consola.success(`Verified ${verification.length} routes`);
108
+ console.log(` ├── Accessible: ${chalk.green(String(accessible.length))}`);
109
+ console.log(` └── Broken: ${chalk.red(String(broken.length))}`);
110
+
111
+ if (broken.length > 0) {
112
+ console.log('');
113
+ consola.error('Broken routes:');
114
+ for (const r of broken.slice(0, 10)) {
115
+ console.log(` ${chalk.red('✗')} ${r.route.path} → ${r.statusCode || r.error}`);
116
+ }
117
+ if (broken.length > 10) {
118
+ console.log(` ${chalk.dim(`... +${broken.length - 10} more`)}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ // Summary
124
+ console.log('');
125
+ console.log(generateRoutesSummary(scanResult));
126
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Sitemap Command - Sitemap validator
3
+ */
4
+
5
+ import consola from 'consola';
6
+ import { getSiteUrl } from '../../config.js';
7
+ import { analyzeAllSitemaps } from '../../crawler/index.js';
8
+ import type { CliOptions } from '../types.js';
9
+
10
+ export async function runSitemap(options: CliOptions) {
11
+ let sitemapUrl = getSiteUrl(options);
12
+
13
+ if (!sitemapUrl.endsWith('.xml')) {
14
+ sitemapUrl = new URL('/sitemap.xml', sitemapUrl).href;
15
+ }
16
+
17
+ consola.start(`Validating sitemap: ${sitemapUrl}`);
18
+
19
+ const analyses = await analyzeAllSitemaps(sitemapUrl);
20
+
21
+ let totalUrls = 0;
22
+ let totalIssues = 0;
23
+
24
+ for (const analysis of analyses) {
25
+ if (analysis.exists) {
26
+ consola.success(`${analysis.url}`);
27
+ consola.info(` Type: ${analysis.type}`);
28
+
29
+ if (analysis.type === 'sitemap') {
30
+ consola.info(` URLs: ${analysis.urls.length}`);
31
+ totalUrls += analysis.urls.length;
32
+ } else {
33
+ consola.info(` Child sitemaps: ${analysis.childSitemaps.length}`);
34
+ }
35
+
36
+ if (analysis.issues.length > 0) {
37
+ totalIssues += analysis.issues.length;
38
+ for (const issue of analysis.issues) {
39
+ consola.warn(` [${issue.severity}] ${issue.title}`);
40
+ }
41
+ }
42
+ } else {
43
+ consola.error(`${analysis.url} - Not found`);
44
+ }
45
+ }
46
+
47
+ consola.box(`Total URLs: ${totalUrls}\nTotal Issues: ${totalIssues}`);
48
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @djangocfg/seo - CLI Tool
5
+ */
6
+
7
+ import { parseArgs } from 'node:util';
8
+ import consola from 'consola';
9
+ import chalk from 'chalk';
10
+ import { loadEnvFiles } from '../config.js';
11
+ import { runAudit, runRoutes, runInspect, runCrawl, runLinks, runRobots, runSitemap, runContent } from './commands/index.js';
12
+ import type { CliOptions } from './types.js';
13
+
14
+ loadEnvFiles();
15
+
16
+ const VERSION = '1.0.0';
17
+
18
+ const HELP = `
19
+ ${chalk.bold('@djangocfg/seo')} - SEO Analysis Tool v${VERSION}
20
+
21
+ ${chalk.bold('Usage:')}
22
+ djangocfg-seo <command> [options]
23
+
24
+ ${chalk.bold('Commands:')}
25
+ audit Full SEO audit (robots + sitemap + crawl + links)
26
+ routes Scan app/ directory and compare with sitemap
27
+ content MDX content tools (check, fix, sitemap)
28
+ crawl Crawl site and analyze SEO issues
29
+ links Check all links for broken links
30
+ robots Analyze robots.txt
31
+ sitemap Validate sitemap
32
+ inspect Inspect URLs via Google Search Console API
33
+
34
+ ${chalk.bold('Options:')}
35
+ --env, -e Environment: prod (default) or dev
36
+ --site, -s Site URL (overrides env detection)
37
+ --output, -o Output directory for reports
38
+ --format, -f Report format: split (default), json, markdown, ai-summary, all
39
+ --max-pages Maximum pages to crawl (default: 100)
40
+ --max-depth Maximum crawl depth (default: 3)
41
+ --timeout Request timeout in ms (default: 60000)
42
+ --concurrency Max concurrent requests (default: 50)
43
+ --service-account Path to Google service account JSON
44
+ --app-dir Path to app/ directory (for routes command)
45
+ --content-dir Path to content/ directory (for content command)
46
+ --base-path Base URL path for docs (default: /docs)
47
+ --check Compare routes with sitemap
48
+ --verify Verify routes are accessible
49
+ --fix Apply fixes (for content fix command)
50
+ --help, -h Show this help
51
+ --version, -v Show version
52
+
53
+ ${chalk.bold('Examples:')}
54
+ ${chalk.dim('# Full SEO audit')}
55
+ djangocfg-seo audit
56
+
57
+ ${chalk.dim('# Scan app routes and compare with sitemap')}
58
+ djangocfg-seo routes --check
59
+
60
+ ${chalk.dim('# Check links on production')}
61
+ djangocfg-seo links
62
+
63
+ ${chalk.dim('# Check MDX content links')}
64
+ djangocfg-seo content check
65
+
66
+ ${chalk.dim('# Fix absolute links to relative')}
67
+ djangocfg-seo content fix --fix
68
+
69
+ ${chalk.dim('# Generate sitemap.ts from content')}
70
+ djangocfg-seo content sitemap
71
+ `;
72
+
73
+ async function main() {
74
+ const { values, positionals } = parseArgs({
75
+ allowPositionals: true,
76
+ options: {
77
+ env: { type: 'string', short: 'e', default: 'prod' },
78
+ site: { type: 'string', short: 's' },
79
+ output: { type: 'string', short: 'o', default: './seo-reports' },
80
+ urls: { type: 'string', short: 'u' },
81
+ 'max-pages': { type: 'string', default: '100' },
82
+ 'max-depth': { type: 'string', default: '3' },
83
+ timeout: { type: 'string', default: '60000' },
84
+ concurrency: { type: 'string', default: '50' },
85
+ format: { type: 'string', short: 'f', default: 'split' },
86
+ 'service-account': { type: 'string' },
87
+ 'app-dir': { type: 'string' },
88
+ 'content-dir': { type: 'string' },
89
+ 'base-path': { type: 'string' },
90
+ check: { type: 'boolean' },
91
+ verify: { type: 'boolean' },
92
+ fix: { type: 'boolean' },
93
+ help: { type: 'boolean', short: 'h' },
94
+ version: { type: 'boolean', short: 'v' },
95
+ },
96
+ });
97
+
98
+ if (values.version) {
99
+ console.log(VERSION);
100
+ process.exit(0);
101
+ }
102
+
103
+ if (values.help || positionals.length === 0) {
104
+ console.log(HELP);
105
+ process.exit(0);
106
+ }
107
+
108
+ const command = positionals[0];
109
+ const options = { ...values, _: positionals } as unknown as CliOptions;
110
+
111
+ try {
112
+ switch (command) {
113
+ case 'audit':
114
+ case 'report':
115
+ await runAudit(options);
116
+ break;
117
+ case 'routes':
118
+ await runRoutes(options);
119
+ break;
120
+ case 'inspect':
121
+ await runInspect(options);
122
+ break;
123
+ case 'crawl':
124
+ await runCrawl(options);
125
+ break;
126
+ case 'links':
127
+ await runLinks(options);
128
+ break;
129
+ case 'robots':
130
+ await runRobots(options);
131
+ break;
132
+ case 'sitemap':
133
+ await runSitemap(options);
134
+ break;
135
+ case 'content':
136
+ await runContent(options);
137
+ break;
138
+ default:
139
+ consola.error(`Unknown command: ${command}`);
140
+ console.log(HELP);
141
+ process.exit(1);
142
+ }
143
+ } catch (error) {
144
+ consola.error(error);
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ main().catch(consola.error);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI Types and Options
3
+ */
4
+
5
+ export interface CliOptions {
6
+ env: string;
7
+ site?: string;
8
+ output: string;
9
+ urls?: string;
10
+ 'max-pages': string;
11
+ 'max-depth': string;
12
+ timeout: string;
13
+ concurrency: string;
14
+ format: string;
15
+ 'service-account'?: string;
16
+ 'app-dir'?: string;
17
+ 'content-dir'?: string;
18
+ 'base-path'?: string;
19
+ check?: boolean;
20
+ verify?: boolean;
21
+ fix?: boolean;
22
+ help?: boolean;
23
+ version?: boolean;
24
+ /** Positional arguments */
25
+ _?: string[];
26
+ }
27
+
28
+ export type ReportFormat = 'json' | 'markdown' | 'ai-summary' | 'split';
29
+
30
+ export function parseFormats(format: string): ReportFormat[] {
31
+ if (format === 'all') {
32
+ return ['json', 'markdown', 'ai-summary', 'split'];
33
+ }
34
+
35
+ if (format === 'split') {
36
+ return ['ai-summary', 'split'];
37
+ }
38
+
39
+ return format.split(',').map((f) => f.trim()) as ReportFormat[];
40
+ }
package/src/config.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @djangocfg/seo - Configuration
3
+ * Environment detection and URL management
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import consola from 'consola';
9
+ import chalk from 'chalk';
10
+
11
+ export interface EnvConfig {
12
+ prod: string | undefined;
13
+ dev: string | undefined;
14
+ }
15
+
16
+ export interface SeoConfig {
17
+ env: EnvConfig;
18
+ cwd: string;
19
+ }
20
+
21
+ const config: SeoConfig = {
22
+ env: {
23
+ prod: undefined,
24
+ dev: undefined,
25
+ },
26
+ cwd: process.cwd(),
27
+ };
28
+
29
+ /**
30
+ * Parse a .env file and return key-value pairs
31
+ */
32
+ export function parseEnvFile(filePath: string): Record<string, string> {
33
+ const content = readFileSync(filePath, 'utf-8');
34
+ const vars: Record<string, string> = {};
35
+
36
+ for (const line of content.split('\n')) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith('#')) continue;
39
+
40
+ const eqIndex = trimmed.indexOf('=');
41
+ if (eqIndex === -1) continue;
42
+
43
+ const key = trimmed.slice(0, eqIndex).trim();
44
+ let value = trimmed.slice(eqIndex + 1).trim();
45
+
46
+ // Remove quotes
47
+ if ((value.startsWith('"') && value.endsWith('"')) ||
48
+ (value.startsWith("'") && value.endsWith("'"))) {
49
+ value = value.slice(1, -1);
50
+ }
51
+
52
+ vars[key] = value;
53
+ }
54
+
55
+ return vars;
56
+ }
57
+
58
+ /**
59
+ * Load .env files from the current working directory
60
+ */
61
+ export function loadEnvFiles(cwd?: string): EnvConfig {
62
+ const workDir = cwd || process.cwd();
63
+ config.cwd = workDir;
64
+
65
+ // Load .env.production
66
+ const prodEnvPath = resolve(workDir, '.env.production');
67
+ if (existsSync(prodEnvPath)) {
68
+ const vars = parseEnvFile(prodEnvPath);
69
+ config.env.prod = vars.NEXT_PUBLIC_SITE_URL || vars.SITE_URL;
70
+ }
71
+
72
+ // Load .env.development
73
+ const devEnvPath = resolve(workDir, '.env.development');
74
+ if (existsSync(devEnvPath)) {
75
+ const vars = parseEnvFile(devEnvPath);
76
+ config.env.dev = vars.NEXT_PUBLIC_SITE_URL || vars.SITE_URL;
77
+ }
78
+
79
+ // Fallback to .env.local or .env
80
+ if (!config.env.prod && !config.env.dev) {
81
+ for (const envFile of ['.env.local', '.env']) {
82
+ const envPath = resolve(workDir, envFile);
83
+ if (existsSync(envPath)) {
84
+ const vars = parseEnvFile(envPath);
85
+ const url = vars.NEXT_PUBLIC_SITE_URL || vars.SITE_URL;
86
+ if (url) {
87
+ config.env.prod = url;
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ return config.env;
95
+ }
96
+
97
+ /**
98
+ * Get the loaded environment configuration
99
+ */
100
+ export function getEnvConfig(): EnvConfig {
101
+ return config.env;
102
+ }
103
+
104
+ /**
105
+ * Get site URL based on options and environment
106
+ */
107
+ export function getSiteUrl(options: {
108
+ site?: string;
109
+ env?: string;
110
+ }): string {
111
+ // Explicit --site flag takes priority
112
+ if (options.site) {
113
+ return options.site;
114
+ }
115
+
116
+ // Use --env flag or default to 'prod'
117
+ const env = options.env || 'prod';
118
+ const isProd = env === 'prod' || env === 'production';
119
+
120
+ // Get URL from loaded env files
121
+ const url = isProd ? config.env.prod : config.env.dev;
122
+
123
+ if (url) {
124
+ const envLabel = isProd ? 'production' : 'development';
125
+ consola.info(`Using ${chalk.cyan(envLabel)} URL: ${chalk.bold(url)}`);
126
+ return url;
127
+ }
128
+
129
+ // Fallback to process.env (already loaded or from shell)
130
+ const fallbackUrl =
131
+ process.env.NEXT_PUBLIC_SITE_URL ||
132
+ process.env.SITE_URL ||
133
+ process.env.BASE_URL;
134
+
135
+ if (fallbackUrl) {
136
+ consola.info(`Using URL from environment: ${fallbackUrl}`);
137
+ return fallbackUrl;
138
+ }
139
+
140
+ // Show available options if no URL found
141
+ console.log('');
142
+ consola.error('No site URL found!');
143
+ console.log('');
144
+ if (config.env.prod || config.env.dev) {
145
+ consola.info('Available environments:');
146
+ if (config.env.prod) consola.log(` ${chalk.green('prod')}: ${config.env.prod}`);
147
+ if (config.env.dev) consola.log(` ${chalk.yellow('dev')}: ${config.env.dev}`);
148
+ console.log('');
149
+ consola.info(`Use ${chalk.cyan('--env prod')} or ${chalk.cyan('--env dev')} to select`);
150
+ } else {
151
+ consola.info('Create .env.production or .env.development with NEXT_PUBLIC_SITE_URL');
152
+ consola.info('Or use --site https://example.com');
153
+ }
154
+ process.exit(1);
155
+ }
156
+
157
+ /**
158
+ * Print detected environment info
159
+ */
160
+ export function printEnvInfo(): void {
161
+ console.log('');
162
+ consola.info('Environment configuration:');
163
+ if (config.env.prod) {
164
+ consola.log(` ${chalk.green('prod')}: ${config.env.prod}`);
165
+ }
166
+ if (config.env.dev) {
167
+ consola.log(` ${chalk.yellow('dev')}: ${config.env.dev}`);
168
+ }
169
+ if (!config.env.prod && !config.env.dev) {
170
+ consola.warn(' No .env files found');
171
+ }
172
+ }
173
+
174
+ /** Default Google service account key filename */
175
+ const GSC_KEY_FILENAME = 'gsc-key.json';
176
+
177
+ /**
178
+ * Find Google service account key file
179
+ * Searches in current directory for gsc-key.json
180
+ */
181
+ export function findGoogleServiceAccount(explicitPath?: string): string | undefined {
182
+ // Explicit path takes priority
183
+ if (explicitPath) {
184
+ const resolved = resolve(config.cwd, explicitPath);
185
+ if (existsSync(resolved)) {
186
+ return resolved;
187
+ }
188
+ consola.warn(`Service account file not found: ${explicitPath}`);
189
+ return undefined;
190
+ }
191
+
192
+ // Auto-detect in current directory
193
+ const defaultPath = resolve(config.cwd, GSC_KEY_FILENAME);
194
+ if (existsSync(defaultPath)) {
195
+ consola.info(`Found Google service account: ${chalk.cyan(GSC_KEY_FILENAME)}`);
196
+ return defaultPath;
197
+ }
198
+
199
+ return undefined;
200
+ }
201
+
202
+ /**
203
+ * Get Google service account key filename for hints
204
+ */
205
+ export function getGscKeyFilename(): string {
206
+ return GSC_KEY_FILENAME;
207
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Content module for MDX/Nextra projects
3
+ * Provides tools for checking links, fixing relative links, and generating sitemaps
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ ContentConfig,
9
+ SitemapConfig,
10
+ SitemapItem,
11
+ SitemapData,
12
+ FileInfo,
13
+ LinkType,
14
+ ExtractedLink,
15
+ BrokenLink,
16
+ LinkCheckResult,
17
+ LinkFix,
18
+ LinkFixResult,
19
+ FixLinksResult,
20
+ ContentScanResult,
21
+ } from './types.js';
22
+
23
+ export { DEFAULT_CONFIG, DEFAULT_SITEMAP_CONFIG } from './types.js';
24
+
25
+ // Scanner
26
+ export {
27
+ detectProjectType,
28
+ hasMetaFiles,
29
+ scanProject,
30
+ getAllFiles,
31
+ getAllMdxFiles,
32
+ getPageFiles,
33
+ getFileInfo,
34
+ pathExists,
35
+ findContentDir,
36
+ findAppDir,
37
+ } from './scanner.js';
38
+
39
+ // Link checker
40
+ export { checkContentLinks, groupBrokenLinksByFile } from './link-checker.js';
41
+
42
+ // Link fixer
43
+ export { fixContentLinks } from './link-fixer.js';
44
+
45
+ // Sitemap generator
46
+ export {
47
+ generateSitemapData,
48
+ generateSitemap,
49
+ flattenSitemap,
50
+ countSitemapItems,
51
+ } from './sitemap-generator.js';