@litodocs/cli 0.6.0 → 0.7.0

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.
@@ -0,0 +1,708 @@
1
+ /**
2
+ * Landing Page Sync Module
3
+ *
4
+ * Supports two modes:
5
+ * 1. Full custom landing (_landing/ folder with HTML/CSS/JS)
6
+ * 2. Section-based landing (mix of custom HTML and default components)
7
+ *
8
+ * The landing page configuration in docs-config.json determines which mode is used.
9
+ */
10
+
11
+ import pkg from 'fs-extra';
12
+ const { copy, ensureDir, readFile, writeFile, pathExists, readJson } = pkg;
13
+ import { join, relative, basename, extname } from 'path';
14
+ import { readdir } from 'fs/promises';
15
+
16
+ /**
17
+ * Landing page types
18
+ */
19
+ export const LANDING_TYPES = {
20
+ NONE: 'none', // No landing page, go straight to docs
21
+ DEFAULT: 'default', // Use template's default landing
22
+ CONFIG: 'config', // Landing defined in docs-config.json (hero, features, etc.)
23
+ CUSTOM: 'custom', // Full custom HTML/CSS/JS from _landing/ folder
24
+ SECTIONS: 'sections', // Section-based: mix of custom HTML and default components
25
+ };
26
+
27
+ /**
28
+ * Default section types that templates should support
29
+ */
30
+ export const DEFAULT_SECTION_TYPES = [
31
+ 'hero',
32
+ 'features',
33
+ 'cta',
34
+ 'testimonials',
35
+ 'pricing',
36
+ 'faq',
37
+ 'stats',
38
+ 'logos',
39
+ 'comparison',
40
+ 'footer',
41
+ ];
42
+
43
+ /**
44
+ * Detect landing page type from user's docs directory
45
+ * @param {string} sourcePath - User's docs directory
46
+ * @param {object} docsConfig - Parsed docs-config.json
47
+ * @returns {Promise<{type: string, config: object}>}
48
+ */
49
+ export async function detectLandingType(sourcePath, docsConfig) {
50
+ const landingConfig = docsConfig?.landing || {};
51
+
52
+ // Check if landing is explicitly disabled
53
+ if (landingConfig.enabled === false) {
54
+ return { type: LANDING_TYPES.NONE, config: landingConfig };
55
+ }
56
+
57
+ // Check for _landing folder (custom HTML/CSS/JS)
58
+ const landingFolderPath = join(sourcePath, '_landing');
59
+ const hasLandingFolder = await pathExists(landingFolderPath);
60
+
61
+ // Determine type based on config
62
+ if (landingConfig.type === 'custom' || (hasLandingFolder && !landingConfig.type)) {
63
+ return { type: LANDING_TYPES.CUSTOM, config: landingConfig };
64
+ }
65
+
66
+ if (landingConfig.type === 'sections' && landingConfig.sections) {
67
+ return { type: LANDING_TYPES.SECTIONS, config: landingConfig };
68
+ }
69
+
70
+ if (landingConfig.type === 'none') {
71
+ return { type: LANDING_TYPES.NONE, config: landingConfig };
72
+ }
73
+
74
+ // Check for config-based landing (hero, features defined in config)
75
+ if (landingConfig.hero || landingConfig.features) {
76
+ return { type: LANDING_TYPES.CONFIG, config: landingConfig };
77
+ }
78
+
79
+ // Default to template's default landing
80
+ return { type: LANDING_TYPES.DEFAULT, config: landingConfig };
81
+ }
82
+
83
+ /**
84
+ * Sync custom landing page from _landing/ folder
85
+ * @param {string} sourcePath - User's docs directory
86
+ * @param {string} projectDir - Scaffolded project directory
87
+ * @param {object} frameworkConfig - Framework configuration
88
+ * @param {object} landingConfig - Landing configuration
89
+ */
90
+ export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig, landingConfig) {
91
+ const landingSource = join(sourcePath, landingConfig.source || '_landing');
92
+
93
+ if (!await pathExists(landingSource)) {
94
+ console.warn(`Warning: Landing folder not found at ${landingSource}`);
95
+ return;
96
+ }
97
+
98
+ // Read all files from _landing/
99
+ const files = await readdir(landingSource, { withFileTypes: true });
100
+
101
+ // Separate files by type
102
+ const htmlFiles = [];
103
+ const cssFiles = [];
104
+ const jsFiles = [];
105
+ const assetFiles = [];
106
+
107
+ for (const file of files) {
108
+ if (file.isDirectory()) {
109
+ // Handle subdirectories (e.g., _landing/assets/)
110
+ if (file.name === 'assets' || file.name === 'images') {
111
+ assetFiles.push(file.name);
112
+ }
113
+ continue;
114
+ }
115
+
116
+ const ext = extname(file.name).toLowerCase();
117
+ if (ext === '.html' || ext === '.htm') {
118
+ htmlFiles.push(file.name);
119
+ } else if (ext === '.css') {
120
+ cssFiles.push(file.name);
121
+ } else if (ext === '.js' || ext === '.mjs') {
122
+ jsFiles.push(file.name);
123
+ }
124
+ }
125
+
126
+ // Generate landing page based on framework
127
+ await generateLandingForFramework(
128
+ projectDir,
129
+ frameworkConfig,
130
+ {
131
+ sourcePath: landingSource,
132
+ htmlFiles,
133
+ cssFiles,
134
+ jsFiles,
135
+ assetFiles,
136
+ config: landingConfig,
137
+ }
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Sync section-based landing page
143
+ * @param {string} sourcePath - User's docs directory
144
+ * @param {string} projectDir - Scaffolded project directory
145
+ * @param {object} frameworkConfig - Framework configuration
146
+ * @param {object} landingConfig - Landing configuration with sections array
147
+ */
148
+ export async function syncSectionsLanding(sourcePath, projectDir, frameworkConfig, landingConfig) {
149
+ const sections = landingConfig.sections || [];
150
+ const processedSections = [];
151
+
152
+ for (const section of sections) {
153
+ if (section.type === 'custom' && section.html) {
154
+ // Load custom HTML for this section
155
+ const htmlPath = join(sourcePath, section.html);
156
+ if (await pathExists(htmlPath)) {
157
+ const htmlContent = await readFile(htmlPath, 'utf-8');
158
+ processedSections.push({
159
+ ...section,
160
+ type: 'custom',
161
+ content: htmlContent,
162
+ });
163
+ } else {
164
+ console.warn(`Warning: Section HTML not found: ${htmlPath}`);
165
+ processedSections.push(section);
166
+ }
167
+ } else {
168
+ // Use default component for this section type
169
+ processedSections.push(section);
170
+ }
171
+ }
172
+
173
+ // Generate sections landing for framework
174
+ await generateSectionsLandingForFramework(
175
+ projectDir,
176
+ frameworkConfig,
177
+ {
178
+ sections: processedSections,
179
+ config: landingConfig,
180
+ }
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Generate landing page for specific framework
186
+ */
187
+ async function generateLandingForFramework(projectDir, frameworkConfig, landingData) {
188
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
189
+
190
+ switch (frameworkConfig.name) {
191
+ case 'astro':
192
+ await generateAstroLanding(projectDir, landingData);
193
+ break;
194
+ case 'react':
195
+ await generateReactLanding(projectDir, landingData);
196
+ break;
197
+ case 'next':
198
+ await generateNextLanding(projectDir, landingData);
199
+ break;
200
+ case 'vue':
201
+ await generateVueLanding(projectDir, landingData);
202
+ break;
203
+ case 'nuxt':
204
+ await generateNuxtLanding(projectDir, landingData);
205
+ break;
206
+ default:
207
+ // Default to Astro-style
208
+ await generateAstroLanding(projectDir, landingData);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Generate Astro landing page (standalone, no template imports)
214
+ */
215
+ async function generateAstroLanding(projectDir, landingData) {
216
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
217
+
218
+ // Read main HTML file
219
+ const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
220
+ if (!mainHtml) {
221
+ console.warn('Warning: No HTML file found in _landing/');
222
+ return;
223
+ }
224
+
225
+ let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
226
+
227
+ // Read CSS files
228
+ let cssContent = '';
229
+ for (const cssFile of cssFiles) {
230
+ const css = await readFile(join(sourcePath, cssFile), 'utf-8');
231
+ cssContent += `/* ${cssFile} */\n${css}\n\n`;
232
+ }
233
+
234
+ // Read JS files
235
+ let jsContent = '';
236
+ for (const jsFile of jsFiles) {
237
+ const js = await readFile(join(sourcePath, jsFile), 'utf-8');
238
+ jsContent += `// ${jsFile}\n${js}\n\n`;
239
+ }
240
+
241
+ // Generate standalone Astro component
242
+ const astroContent = `---
243
+ // Custom landing page - auto-generated by Lito CLI
244
+ // Source: _landing/ folder
245
+ import '../styles/global.css';
246
+ import Header from '../components/Header.astro';
247
+ import Footer from '../components/Footer.astro';
248
+ import { getConfigFile } from '../utils/config';
249
+
250
+ const config = await getConfigFile();
251
+ ---
252
+
253
+ <!doctype html>
254
+ <html lang="en" class="dark">
255
+ <head>
256
+ <meta charset="UTF-8" />
257
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
258
+ <title>{config.metadata?.name || 'Home'}</title>
259
+ <meta name="description" content={config.metadata?.description || ''} />
260
+ <link rel="icon" href={config.branding?.favicon || '/favicon.svg'} />
261
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
262
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
263
+ <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet" />
264
+ <script is:inline>
265
+ const savedTheme = localStorage.getItem('theme');
266
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
267
+ if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
268
+ document.documentElement.classList.remove('dark');
269
+ document.documentElement.classList.add('light');
270
+ }
271
+ </script>
272
+ <style>
273
+ ${cssContent}
274
+ </style>
275
+ </head>
276
+ <body class="bg-background text-foreground font-sans antialiased">
277
+ <Header />
278
+
279
+ <main class="landing-custom">
280
+ ${htmlContent}
281
+ </main>
282
+
283
+ <Footer />
284
+
285
+ ${jsContent ? `<script>\n${jsContent}\n</script>` : ''}
286
+ </body>
287
+ </html>
288
+ `;
289
+
290
+ // Write to index.astro
291
+ const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
292
+ await writeFile(indexPath, astroContent, 'utf-8');
293
+
294
+ // Copy assets if they exist
295
+ await copyLandingAssets(sourcePath, projectDir);
296
+ }
297
+
298
+ /**
299
+ * Generate React landing page (standalone)
300
+ */
301
+ async function generateReactLanding(projectDir, landingData) {
302
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
303
+
304
+ // Read main HTML file
305
+ const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
306
+ if (!mainHtml) {
307
+ console.warn('Warning: No HTML file found in _landing/');
308
+ return;
309
+ }
310
+
311
+ let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
312
+
313
+ // Read CSS files
314
+ let cssContent = '';
315
+ for (const cssFile of cssFiles) {
316
+ const css = await readFile(join(sourcePath, cssFile), 'utf-8');
317
+ cssContent += `/* ${cssFile} */\n${css}\n\n`;
318
+ }
319
+
320
+ // Write CSS to a separate file
321
+ const cssPath = join(projectDir, 'src', 'styles', 'landing.css');
322
+ await ensureDir(join(projectDir, 'src', 'styles'));
323
+ await writeFile(cssPath, cssContent, 'utf-8');
324
+
325
+ // Read JS files for inline script
326
+ let jsContent = '';
327
+ for (const jsFile of jsFiles) {
328
+ const js = await readFile(join(sourcePath, jsFile), 'utf-8');
329
+ jsContent += `// ${jsFile}\n${js}\n\n`;
330
+ }
331
+
332
+ // Generate standalone React component
333
+ const reactContent = `// Custom landing page - auto-generated by Lito CLI
334
+ // Source: _landing/ folder
335
+ import { useEffect } from 'react';
336
+ import '../styles/landing.css';
337
+
338
+ export default function LandingPage() {
339
+ useEffect(() => {
340
+ // Landing page scripts
341
+ ${jsContent}
342
+ }, []);
343
+
344
+ return (
345
+ <main
346
+ className="landing-custom"
347
+ dangerouslySetInnerHTML={{ __html: \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\` }}
348
+ />
349
+ );
350
+ }
351
+ `;
352
+
353
+ // Write to index page
354
+ const landingPath = join(projectDir, 'src', 'pages', 'index.jsx');
355
+ await ensureDir(join(projectDir, 'src', 'pages'));
356
+ await writeFile(landingPath, reactContent, 'utf-8');
357
+
358
+ // Copy assets if they exist
359
+ await copyLandingAssets(sourcePath, projectDir);
360
+ }
361
+
362
+ /**
363
+ * Generate Next.js landing page (standalone)
364
+ */
365
+ async function generateNextLanding(projectDir, landingData) {
366
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
367
+
368
+ // Read main HTML file
369
+ const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
370
+ if (!mainHtml) {
371
+ console.warn('Warning: No HTML file found in _landing/');
372
+ return;
373
+ }
374
+
375
+ let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
376
+
377
+ // Read CSS files
378
+ let cssContent = '';
379
+ for (const cssFile of cssFiles) {
380
+ const css = await readFile(join(sourcePath, cssFile), 'utf-8');
381
+ cssContent += `/* ${cssFile} */\n${css}\n\n`;
382
+ }
383
+
384
+ // Write CSS to globals or landing file
385
+ const cssPath = join(projectDir, 'app', 'landing.css');
386
+ await ensureDir(join(projectDir, 'app'));
387
+ await writeFile(cssPath, cssContent, 'utf-8');
388
+
389
+ // Read JS files
390
+ let jsContent = '';
391
+ for (const jsFile of jsFiles) {
392
+ const js = await readFile(join(sourcePath, jsFile), 'utf-8');
393
+ jsContent += `// ${jsFile}\n${js}\n\n`;
394
+ }
395
+
396
+ // Generate standalone Next.js page
397
+ const nextContent = `// Custom landing page - auto-generated by Lito CLI
398
+ // Source: _landing/ folder
399
+ 'use client';
400
+
401
+ import { useEffect } from 'react';
402
+ import './landing.css';
403
+
404
+ export default function Home() {
405
+ useEffect(() => {
406
+ // Landing page scripts
407
+ ${jsContent}
408
+ }, []);
409
+
410
+ return (
411
+ <main
412
+ className="landing-custom"
413
+ dangerouslySetInnerHTML={{ __html: \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\` }}
414
+ />
415
+ );
416
+ }
417
+ `;
418
+
419
+ // Write to page.jsx (Next.js 13+ app router)
420
+ const pagePath = join(projectDir, 'app', 'page.jsx');
421
+ await writeFile(pagePath, nextContent, 'utf-8');
422
+
423
+ // Copy assets if they exist
424
+ await copyLandingAssets(sourcePath, projectDir);
425
+ }
426
+
427
+ /**
428
+ * Generate Vue landing page (standalone)
429
+ */
430
+ async function generateVueLanding(projectDir, landingData) {
431
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
432
+
433
+ // Read main HTML file
434
+ const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
435
+ if (!mainHtml) {
436
+ console.warn('Warning: No HTML file found in _landing/');
437
+ return;
438
+ }
439
+
440
+ let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
441
+
442
+ // Read CSS files
443
+ let cssContent = '';
444
+ for (const cssFile of cssFiles) {
445
+ const css = await readFile(join(sourcePath, cssFile), 'utf-8');
446
+ cssContent += `/* ${cssFile} */\n${css}\n\n`;
447
+ }
448
+
449
+ // Read JS files
450
+ let jsContent = '';
451
+ for (const jsFile of jsFiles) {
452
+ const js = await readFile(join(sourcePath, jsFile), 'utf-8');
453
+ jsContent += js + '\n';
454
+ }
455
+
456
+ // Generate standalone Vue SFC
457
+ const vueContent = `<script setup>
458
+ // Custom landing page - auto-generated by Lito CLI
459
+ // Source: _landing/ folder
460
+ import { onMounted, ref } from 'vue';
461
+
462
+ const landingHtml = ref(\`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`);
463
+
464
+ onMounted(() => {
465
+ // Landing page scripts
466
+ ${jsContent}
467
+ });
468
+ </script>
469
+
470
+ <template>
471
+ <main class="landing-custom" v-html="landingHtml"></main>
472
+ </template>
473
+
474
+ <style scoped>
475
+ ${cssContent}
476
+ </style>
477
+ `;
478
+
479
+ // Write to index page
480
+ const vuePath = join(projectDir, 'src', 'pages', 'index.vue');
481
+ await ensureDir(join(projectDir, 'src', 'pages'));
482
+ await writeFile(vuePath, vueContent, 'utf-8');
483
+
484
+ // Copy assets if they exist
485
+ await copyLandingAssets(sourcePath, projectDir);
486
+ }
487
+
488
+ /**
489
+ * Generate Nuxt landing page (standalone)
490
+ */
491
+ async function generateNuxtLanding(projectDir, landingData) {
492
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
493
+
494
+ // Read main HTML file
495
+ const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
496
+ if (!mainHtml) {
497
+ console.warn('Warning: No HTML file found in _landing/');
498
+ return;
499
+ }
500
+
501
+ let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
502
+
503
+ // Read CSS files
504
+ let cssContent = '';
505
+ for (const cssFile of cssFiles) {
506
+ const css = await readFile(join(sourcePath, cssFile), 'utf-8');
507
+ cssContent += `/* ${cssFile} */\n${css}\n\n`;
508
+ }
509
+
510
+ // Read JS files
511
+ let jsContent = '';
512
+ for (const jsFile of jsFiles) {
513
+ const js = await readFile(join(sourcePath, jsFile), 'utf-8');
514
+ jsContent += js + '\n';
515
+ }
516
+
517
+ // Generate standalone Nuxt page
518
+ const nuxtContent = `<script setup>
519
+ // Custom landing page - auto-generated by Lito CLI
520
+ // Source: _landing/ folder
521
+
522
+ const landingHtml = \`${htmlContent.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`;
523
+
524
+ onMounted(() => {
525
+ // Landing page scripts
526
+ ${jsContent}
527
+ });
528
+ </script>
529
+
530
+ <template>
531
+ <main class="landing-custom" v-html="landingHtml"></main>
532
+ </template>
533
+
534
+ <style scoped>
535
+ ${cssContent}
536
+ </style>
537
+ `;
538
+
539
+ // Write to index page
540
+ const nuxtPath = join(projectDir, 'pages', 'index.vue');
541
+ await ensureDir(join(projectDir, 'pages'));
542
+ await writeFile(nuxtPath, nuxtContent, 'utf-8');
543
+
544
+ // Copy assets if they exist
545
+ await copyLandingAssets(sourcePath, projectDir);
546
+ }
547
+
548
+ /**
549
+ * Generate sections-based landing for framework
550
+ */
551
+ async function generateSectionsLandingForFramework(projectDir, frameworkConfig, landingData) {
552
+ const { sections, config } = landingData;
553
+
554
+ // For now, we'll generate a JSON manifest that templates can read
555
+ // Templates are responsible for rendering the sections
556
+ const manifestPath = join(projectDir, 'src', 'data', 'landing-sections.json');
557
+ await ensureDir(join(projectDir, 'src', 'data'));
558
+
559
+ await writeFile(manifestPath, JSON.stringify({
560
+ type: 'sections',
561
+ sections: sections,
562
+ }, null, 2), 'utf-8');
563
+
564
+ // Additionally, generate a default sections component for Astro
565
+ if (frameworkConfig.name === 'astro') {
566
+ await generateAstroSectionsLanding(projectDir, landingData);
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Generate Astro sections landing page
572
+ */
573
+ async function generateAstroSectionsLanding(projectDir, landingData) {
574
+ const { sections, config } = landingData;
575
+
576
+ // Generate section renders
577
+ const sectionRenders = sections.map((section, index) => {
578
+ if (section.type === 'custom' && section.content) {
579
+ // Custom HTML section
580
+ return `
581
+ <!-- Custom Section ${index + 1} -->
582
+ <section class="landing-section landing-section-custom" id="section-${index}">
583
+ <Fragment set:html={\`${section.content.replace(/`/g, '\\`')}\`} />
584
+ </section>`;
585
+ } else {
586
+ // Default component section - template must provide these components
587
+ const componentName = section.type.charAt(0).toUpperCase() + section.type.slice(1) + 'Section';
588
+ return `
589
+ <!-- ${section.type} Section -->
590
+ {Astro.glob('../components/landing/${componentName}.astro').then(m => m[0] ? <m[0].default {...${JSON.stringify(section.props || {})}} /> : null)}`;
591
+ }
592
+ }).join('\n');
593
+
594
+ const astroContent = `---
595
+ // Sections-based landing page - auto-generated by Lito CLI
596
+ import '../styles/global.css';
597
+ import Header from '../components/Header.astro';
598
+ import Footer from '../components/Footer.astro';
599
+ import { getConfigFile } from '../utils/config';
600
+
601
+ const config = await getConfigFile();
602
+ ---
603
+
604
+ <!doctype html>
605
+ <html lang="en" class="dark">
606
+ <head>
607
+ <meta charset="UTF-8" />
608
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
609
+ <title>{config.metadata?.name || 'Home'}</title>
610
+ <link rel="icon" href={config.branding?.favicon || '/favicon.svg'} />
611
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
612
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
613
+ <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&display=swap" rel="stylesheet" />
614
+ <script is:inline>
615
+ const savedTheme = localStorage.getItem('theme');
616
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
617
+ if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
618
+ document.documentElement.classList.remove('dark');
619
+ document.documentElement.classList.add('light');
620
+ }
621
+ </script>
622
+ </head>
623
+ <body class="bg-background text-foreground font-sans antialiased">
624
+ <Header />
625
+
626
+ <main class="landing-sections">
627
+ ${sectionRenders}
628
+ </main>
629
+
630
+ <Footer />
631
+ </body>
632
+ </html>
633
+ `;
634
+
635
+ // Only write if custom sections are defined
636
+ if (sections.some(s => s.type === 'custom')) {
637
+ const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
638
+ await writeFile(indexPath, astroContent, 'utf-8');
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Copy landing assets to public directory
644
+ */
645
+ async function copyLandingAssets(sourcePath, projectDir) {
646
+ const assetDirs = ['assets', 'images', 'img', 'icons'];
647
+
648
+ for (const dir of assetDirs) {
649
+ const assetSource = join(sourcePath, dir);
650
+ if (await pathExists(assetSource)) {
651
+ const assetTarget = join(projectDir, 'public', 'landing', dir);
652
+ await ensureDir(assetTarget);
653
+ await copy(assetSource, assetTarget, { overwrite: true });
654
+ }
655
+ }
656
+
657
+ // Also copy any image files directly in _landing/
658
+ try {
659
+ const files = await readdir(sourcePath);
660
+ const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'];
661
+
662
+ for (const file of files) {
663
+ const ext = extname(file).toLowerCase();
664
+ if (imageExts.includes(ext)) {
665
+ const src = join(sourcePath, file);
666
+ const dest = join(projectDir, 'public', 'landing', file);
667
+ await ensureDir(join(projectDir, 'public', 'landing'));
668
+ await copy(src, dest, { overwrite: true });
669
+ }
670
+ }
671
+ } catch {
672
+ // Ignore errors
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Main landing sync function
678
+ * @param {string} sourcePath - User's docs directory
679
+ * @param {string} projectDir - Scaffolded project directory
680
+ * @param {object} frameworkConfig - Framework configuration
681
+ * @param {object} docsConfig - Parsed docs-config.json
682
+ */
683
+ export async function syncLanding(sourcePath, projectDir, frameworkConfig, docsConfig) {
684
+ const { type, config } = await detectLandingType(sourcePath, docsConfig);
685
+
686
+ switch (type) {
687
+ case LANDING_TYPES.NONE:
688
+ // Remove default landing page, show docs index directly
689
+ // This is handled by templates based on config
690
+ break;
691
+
692
+ case LANDING_TYPES.CUSTOM:
693
+ await syncCustomLanding(sourcePath, projectDir, frameworkConfig, config);
694
+ break;
695
+
696
+ case LANDING_TYPES.SECTIONS:
697
+ await syncSectionsLanding(sourcePath, projectDir, frameworkConfig, config);
698
+ break;
699
+
700
+ case LANDING_TYPES.CONFIG:
701
+ case LANDING_TYPES.DEFAULT:
702
+ // These are handled by templates reading docs-config.json
703
+ // Just ensure the config is synced (done elsewhere)
704
+ break;
705
+ }
706
+
707
+ return { type, config };
708
+ }