Skip to content

Commit 1cbe207

Browse files
DevRohit06claude
andcommitted
fix: properly handle full HTML documents in custom landing pages
Custom landing pages that are complete HTML documents (with <!doctype>, <html>, <head>, <body>) were being nested inside Astro's own HTML wrapper, creating invalid markup with duplicate <html>/<head>/<body> tags. This broke scripts (GSAP, onclick handlers), styles (:root variables, animations), and external CDN resources. - Parse full HTML documents and extract head/body content separately - Add is:inline to <script> and <style> tags so Astro passes them through - Split generation into full-doc vs fragment paths for correct handling Bumps version to 1.2.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 92ea120 commit 1cbe207

2 files changed

Lines changed: 129 additions & 16 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@litodocs/cli",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Beautiful documentation sites from Markdown. Fast, simple, and open-source.",
55
"type": "module",
66
"bin": {

src/core/landing-sync.js

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,46 @@ const { copy, ensureDir, readFile, writeFile, pathExists, readJson } = pkg;
1313
import { join, relative, basename, extname } from 'path';
1414
import { 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(/<script(?![^>]*is:inline)([^>]*>)/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(/<style(?![^>]*is:(?:inline|global))([^>]*>)/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 = /<!doctype\s+html|<html[\s>]/i.test(html);
37+
if (!isFullDoc) return null;
38+
39+
// Extract <html> tag attributes
40+
const htmlTagMatch = html.match(/<html([^>]*)>/i);
41+
const htmlAttrs = htmlTagMatch ? htmlTagMatch[1].trim() : '';
42+
43+
// Extract <head> inner content
44+
const headMatch = html.match(/<head[^>]*>([\s\S]*)<\/head>/i);
45+
const headContent = headMatch ? headMatch[1].trim() : '';
46+
47+
// Extract <body> tag attributes and inner content
48+
const bodyTagMatch = html.match(/<body([^>]*)>/i);
49+
const bodyAttrs = bodyTagMatch ? bodyTagMatch[1].trim() : '';
50+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/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
353473
import '../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

Comments
 (0)