@djangocfg/seo 2.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +3780 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/crawler/index.d.ts +88 -0
- package/dist/crawler/index.mjs +610 -0
- package/dist/crawler/index.mjs.map +1 -0
- package/dist/google-console/index.d.ts +95 -0
- package/dist/google-console/index.mjs +539 -0
- package/dist/google-console/index.mjs.map +1 -0
- package/dist/index.d.ts +285 -0
- package/dist/index.mjs +3236 -0
- package/dist/index.mjs.map +1 -0
- package/dist/link-checker/index.d.ts +76 -0
- package/dist/link-checker/index.mjs +326 -0
- package/dist/link-checker/index.mjs.map +1 -0
- package/dist/markdown-report-B3QdDzxE.d.ts +193 -0
- package/dist/reports/index.d.ts +24 -0
- package/dist/reports/index.mjs +836 -0
- package/dist/reports/index.mjs.map +1 -0
- package/dist/routes/index.d.ts +69 -0
- package/dist/routes/index.mjs +372 -0
- package/dist/routes/index.mjs.map +1 -0
- package/dist/scanner-Cz4Th2Pt.d.ts +60 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +114 -0
- package/src/analyzer.ts +256 -0
- package/src/cli/commands/audit.ts +260 -0
- package/src/cli/commands/content.ts +180 -0
- package/src/cli/commands/crawl.ts +32 -0
- package/src/cli/commands/index.ts +12 -0
- package/src/cli/commands/inspect.ts +60 -0
- package/src/cli/commands/links.ts +41 -0
- package/src/cli/commands/robots.ts +36 -0
- package/src/cli/commands/routes.ts +126 -0
- package/src/cli/commands/sitemap.ts +48 -0
- package/src/cli/index.ts +149 -0
- package/src/cli/types.ts +40 -0
- package/src/config.ts +207 -0
- package/src/content/index.ts +51 -0
- package/src/content/link-checker.ts +182 -0
- package/src/content/link-fixer.ts +188 -0
- package/src/content/scanner.ts +200 -0
- package/src/content/sitemap-generator.ts +321 -0
- package/src/content/types.ts +140 -0
- package/src/crawler/crawler.ts +425 -0
- package/src/crawler/index.ts +10 -0
- package/src/crawler/robots-parser.ts +171 -0
- package/src/crawler/sitemap-validator.ts +204 -0
- package/src/google-console/analyzer.ts +317 -0
- package/src/google-console/auth.ts +100 -0
- package/src/google-console/client.ts +281 -0
- package/src/google-console/index.ts +9 -0
- package/src/index.ts +144 -0
- package/src/link-checker/index.ts +461 -0
- package/src/reports/claude-context.ts +149 -0
- package/src/reports/generator.ts +244 -0
- package/src/reports/index.ts +27 -0
- package/src/reports/json-report.ts +320 -0
- package/src/reports/markdown-report.ts +246 -0
- package/src/reports/split-report.ts +252 -0
- package/src/routes/analyzer.ts +324 -0
- package/src/routes/index.ts +25 -0
- package/src/routes/scanner.ts +298 -0
- package/src/types/index.ts +222 -0
- package/src/utils/index.ts +154 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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);
|
package/src/cli/types.ts
ADDED
|
@@ -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';
|