@@ -13,6 +13,46 @@ const { copy, ensureDir, readFile, writeFile, pathExists, readJson } = pkg;
1313import { join , relative , basename , extname } from 'path' ;
1414import { readdir } from 'fs/promises' ;
1515
16+ /**
17+ * Add is:inline to all <script> and <style> tags in HTML so Astro ships
18+ * them as-is. Without this, Astro treats scripts as ES modules (scoping
19+ * declarations, breaking onclick handlers) and scopes styles (breaking
20+ * global CSS like :root variables, animations, etc.).
21+ */
22+ function inlineForAstro ( html ) {
23+ // Add is:inline to <script> tags that don't already have it
24+ html = html . replace ( / < s c r i p t (? ! [ ^ > ] * i s : i n l i n e ) ( [ ^ > ] * > ) / gi, '<script is:inline$1' ) ;
25+ // Add is:inline to <style> tags that don't already have is:inline or is:global
26+ html = html . replace ( / < s t y l e (? ! [ ^ > ] * i s : (?: i n l i n e | g l o b a l ) ) ( [ ^ > ] * > ) / gi, '<style is:inline$1' ) ;
27+ return html ;
28+ }
29+
30+ /**
31+ * Check if HTML is a full document (has <html> or <!doctype>).
32+ * If so, extract head content, body content, and html/body attributes
33+ * so we can merge them into the Astro template properly.
34+ */
35+ function parseFullHtmlDocument ( html ) {
36+ const isFullDoc = / < ! d o c t y p e \s + h t m l | < h t m l [ \s > ] / i. test ( html ) ;
37+ if ( ! isFullDoc ) return null ;
38+
39+ // Extract <html> tag attributes
40+ const htmlTagMatch = html . match ( / < h t m l ( [ ^ > ] * ) > / i) ;
41+ const htmlAttrs = htmlTagMatch ? htmlTagMatch [ 1 ] . trim ( ) : '' ;
42+
43+ // Extract <head> inner content
44+ const headMatch = html . match ( / < h e a d [ ^ > ] * > ( [ \s \S ] * ) < \/ h e a d > / i) ;
45+ const headContent = headMatch ? headMatch [ 1 ] . trim ( ) : '' ;
46+
47+ // Extract <body> tag attributes and inner content
48+ const bodyTagMatch = html . match ( / < b o d y ( [ ^ > ] * ) > / i) ;
49+ const bodyAttrs = bodyTagMatch ? bodyTagMatch [ 1 ] . trim ( ) : '' ;
50+ const bodyMatch = html . match ( / < b o d y [ ^ > ] * > ( [ \s \S ] * ) < \/ b o d y > / i) ;
51+ const bodyContent = bodyMatch ? bodyMatch [ 1 ] . trim ( ) : '' ;
52+
53+ return { htmlAttrs, headContent, bodyContent, bodyAttrs } ;
54+ }
55+
1656/**
1757 * Landing page types
1858 */
@@ -309,6 +349,9 @@ async function generateAstroLanding(projectDir, landingData) {
309349
310350 let htmlContent = await readFile ( join ( sourcePath , mainHtml ) , 'utf-8' ) ;
311351
352+ // Make all <script> tags in the user's HTML pass through Astro untouched
353+ htmlContent = inlineForAstro ( htmlContent ) ;
354+
312355 // Read CSS files and write to a separate file
313356 let cssContent = '' ;
314357 for ( const cssFile of cssFiles ) {
@@ -327,7 +370,36 @@ async function generateAstroLanding(projectDir, landingData) {
327370 jsContent += `// ${ jsFile } \n${ js } \n\n` ;
328371 }
329372
330- // Determine header/footer: hidden ('__hidden__'), custom (string HTML), or default (null)
373+ // Check if the user's HTML is a full document (has <html>, <head>, <body>)
374+ const parsed = parseFullHtmlDocument ( htmlContent ) ;
375+
376+ let astroContent ;
377+
378+ if ( parsed ) {
379+ // Full HTML document: merge the user's head/body into the Astro page
380+ // instead of nesting an entire HTML document inside another one.
381+ astroContent = generateAstroFromFullDoc ( parsed , { cssFiles, jsContent, navbarContent, footerContent } ) ;
382+ } else {
383+ // HTML fragment: wrap it in a full Astro page
384+ astroContent = generateAstroFromFragment ( htmlContent , { jsContent, navbarContent, footerContent } ) ;
385+ }
386+
387+ // Write to index.astro
388+ const indexPath = join ( projectDir , 'src' , 'pages' , 'index.astro' ) ;
389+ await writeFile ( indexPath , astroContent , 'utf-8' ) ;
390+
391+ // Copy assets if they exist
392+ await copyLandingAssets ( sourcePath , projectDir ) ;
393+ }
394+
395+ /**
396+ * Generate Astro page from a full HTML document.
397+ * Extracts <head> and <body> content, preserves the user's structure.
398+ */
399+ function generateAstroFromFullDoc ( parsed , { cssFiles, jsContent, navbarContent, footerContent } ) {
400+ const { htmlAttrs, headContent, bodyContent, bodyAttrs } = parsed ;
401+
402+ // Determine header/footer rendering
331403 const navbarIsHidden = navbarContent === '__hidden__' ;
332404 const footerIsHidden = footerContent === '__hidden__' ;
333405 const hasCustomNavbar = ! navbarIsHidden && ! ! navbarContent ;
@@ -338,16 +410,64 @@ async function generateAstroLanding(projectDir, landingData) {
338410 const headerRender = navbarIsHidden
339411 ? ''
340412 : hasCustomNavbar
341- ? `<header class="landing-custom-navbar">\n ${ navbarContent } \n </header>`
413+ ? `<header class="landing-custom-navbar">\n ${ inlineForAstro ( navbarContent ) } \n </header>`
342414 : '<Header />' ;
343415 const footerRender = footerIsHidden
344416 ? ''
345417 : hasCustomFooter
346- ? `<footer class="landing-custom-footer">\n ${ footerContent } \n </footer>`
418+ ? `<footer class="landing-custom-footer">\n ${ inlineForAstro ( footerContent ) } \n </footer>`
347419 : '<Footer />' ;
348420
349- // Generate standalone Astro component
350- const astroContent = `---
421+ return `---
422+ // Custom landing page - auto-generated by Lito CLI
423+ // Source: _landing/ folder (full HTML document)
424+ import '../styles/landing.css';
425+ ${ headerImport }
426+ ${ footerImport }
427+ ---
428+
429+ <!doctype html>
430+ <html ${ htmlAttrs } >
431+ <head>
432+ ${ headContent }
433+ </head>
434+ <body ${ bodyAttrs } >
435+ ${ headerRender }
436+
437+ ${ bodyContent }
438+
439+ ${ footerRender }
440+
441+ ${ jsContent ? `<script is:inline>\n${ jsContent } \n</script>` : '' }
442+ </body>
443+ </html>
444+ ` ;
445+ }
446+
447+ /**
448+ * Generate Astro page from an HTML fragment.
449+ * Wraps it in a full Astro page with Lito's defaults.
450+ */
451+ function generateAstroFromFragment ( htmlContent , { jsContent, navbarContent, footerContent } ) {
452+ const navbarIsHidden = navbarContent === '__hidden__' ;
453+ const footerIsHidden = footerContent === '__hidden__' ;
454+ const hasCustomNavbar = ! navbarIsHidden && ! ! navbarContent ;
455+ const hasCustomFooter = ! footerIsHidden && ! ! footerContent ;
456+
457+ const headerImport = navbarIsHidden || hasCustomNavbar ? '' : "import Header from '../components/Header.astro';" ;
458+ const footerImport = footerIsHidden || hasCustomFooter ? '' : "import Footer from '../components/Footer.astro';" ;
459+ const headerRender = navbarIsHidden
460+ ? ''
461+ : hasCustomNavbar
462+ ? `<header class="landing-custom-navbar">\n ${ inlineForAstro ( navbarContent ) } \n </header>`
463+ : '<Header />' ;
464+ const footerRender = footerIsHidden
465+ ? ''
466+ : hasCustomFooter
467+ ? `<footer class="landing-custom-footer">\n ${ inlineForAstro ( footerContent ) } \n </footer>`
468+ : '<Footer />' ;
469+
470+ return `---
351471// Custom landing page - auto-generated by Lito CLI
352472// Source: _landing/ folder
353473import '../styles/global.css';
@@ -388,17 +508,10 @@ const config = await getConfigFile();
388508
389509 ${ footerRender }
390510
391- ${ jsContent ? `<script>\n${ jsContent } \n</script>` : '' }
511+ ${ jsContent ? `<script is:inline >\n${ jsContent } \n</script>` : '' }
392512 </body>
393513</html>
394514` ;
395-
396- // Write to index.astro
397- const indexPath = join ( projectDir , 'src' , 'pages' , 'index.astro' ) ;
398- await writeFile ( indexPath , astroContent , 'utf-8' ) ;
399-
400- // Copy assets if they exist
401- await copyLandingAssets ( sourcePath , projectDir ) ;
402515}
403516
404517/**
@@ -708,12 +821,12 @@ async function generateAstroSectionsLanding(projectDir, landingData) {
708821 const headerRender = navbarIsHidden
709822 ? ''
710823 : hasCustomNavbar
711- ? `<header class="landing-custom-navbar">\n ${ navbarContent } \n </header>`
824+ ? `<header class="landing-custom-navbar">\n ${ inlineForAstro ( navbarContent ) } \n </header>`
712825 : '<Header />' ;
713826 const footerRender = footerIsHidden
714827 ? ''
715828 : hasCustomFooter
716- ? `<footer class="landing-custom-footer">\n ${ footerContent } \n </footer>`
829+ ? `<footer class="landing-custom-footer">\n ${ inlineForAstro ( footerContent ) } \n </footer>`
717830 : '<Footer />' ;
718831
719832 const astroContent = `---
0 commit comments