@@ -1,10 +1,11 @@
1 1 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
2 2 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
AI-powered HTML → Hugo template conversion.
3 3 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
4 4 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
For already-Hugo themes, use hugoify_dir() to validate/augment.
5 5 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
For raw HTML, use hugoify_html() to produce Hugo layout files.
6 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ For Next.js apps, use hugoify_nextjs() to convert React components to Hugo layouts.
6 7 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
7 8 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
8 9 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
import json
9 10 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
import logging
10 11 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
import os
@@ -14,10 +15,17 @@
14 15 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
15 16 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
SYSTEM = (
16 17 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"You are an expert Hugo theme developer. Convert HTML templates to valid Hugo Go template files. "
17 18 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"Output only valid Hugo template syntax — no explanations, no markdown fences."
18 19 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
)
20 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
21 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ NEXTJS_SYSTEM = (
22 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "You are an expert at converting React/Next.js components to Hugo Go template files. "
23 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "You understand JSX, TSX, React component composition, and Hugo template syntax. "
24 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Convert React components to static Hugo HTML templates, preserving all CSS classes and visual structure. "
25 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "Output only valid Hugo template syntax — no explanations, no markdown fences."
26 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ )
19 27 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
20 28 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
21 29 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
def hugoify_html(html_path: str) -> dict:
22 30 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
23 31 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Convert a raw HTML file to a set of Hugo layout files.
@@ -66,10 +74,167 @@
66 74 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Return ONLY a valid JSON object, no explanation."""
67 75 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
68 76 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
response = call_ai(prompt, SYSTEM)
69 77 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return _parse_layout_json(response)
70 78 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
79 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
80 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def hugoify_nextjs(info: dict) -> dict:
81 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
82 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Convert a Next.js app to a set of Hugo layout files.
83 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
84 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Args:
85 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ info: dict from find_nextjs_app() with app_dir, router_type, etc.
86 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
87 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Returns:
88 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dict mapping relative layout paths to their content, same format as hugoify_html().
89 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
90 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ app_dir = info['app_dir']
91 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info(f"Hugoifying Next.js app at {app_dir} ...")
92 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
93 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sources = _collect_nextjs_sources(info)
94 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if not sources:
95 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.warning("No source files collected from Next.js app")
96 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return _fallback_layouts()
97 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
98 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Build the source context for the AI
99 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ source_block = ""
100 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for rel_path, content in sources.items():
101 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ source_block += f"\n{'='*60}\n// FILE: {rel_path}\n{'='*60}\n{content}\n"
102 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
103 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ prompt = f"""Convert the following Next.js React application into Hugo layout files.
104 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
105 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ The app uses the Next.js App Router with React components (TSX). Convert it to a static Hugo theme.
106 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
107 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Return a JSON object where keys are relative file paths under layouts/ and values are the Hugo template content.
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
109 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Required keys to produce:
110 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - "_default/baseof.html" — base template with the HTML shell, <head>, and blocks
111 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - "partials/header.html" — site header/navigation extracted as partial
112 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - "partials/footer.html" — footer extracted as partial
113 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - "index.html" — homepage using {{{{ define "main" }}}} ... {{{{ end }}}}
114 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Additional "partials/{{name}}.html" for each major section component
115 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
116 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Conversion rules:
117 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - JSX `className` → HTML `class`
118 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - React component composition → Hugo partials via `{{{{ partial "name.html" . }}}}`
119 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - `app/layout.tsx` → `_default/baseof.html` with `{{{{ block "main" . }}}}{{{{ end }}}}`
120 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - `app/page.tsx` → `index.html` with `{{{{ define "main" }}}}...{{{{ end }}}}`
121 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Each section component (e.g. HeroSection, CTASection, FooterSection) → `partials/{{name}}.html`
122 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - `<Link href="...">text</Link>` → `<a href="...">text</a>`
123 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - `<Image src="..." alt="..." />` → `<img src="..." alt="..." />`
124 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - next/font imports (Geist, Sora, etc.) → Google Fonts <link> tags in <head>
125 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Conditional rendering `{{condition && <div>...</div>}}` → render the static content
126 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - `map()` calls over static arrays → unroll into static HTML
127 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Interactive elements (onClick, useState, useEffect, motion.*) → strip interactivity, keep the static HTML structure
128 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Animation wrappers (FadeIn, motion.div) → plain `<div>` elements preserving classes
129 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Preserve ALL Tailwind CSS classes and inline styles exactly as-is
130 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Replace hardcoded page titles with `{{{{ .Title }}}}`
131 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Replace hardcoded site name with `{{{{ .Site.Title }}}}`
132 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Replace copyright year with `{{{{ now.Year }}}}`
133 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - For Tailwind CSS, include `<script src="https://cdn.tailwindcss.com"></script>` in the <head> of baseof.html
134 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Link any CSS files as `<link rel="stylesheet" href="/css/globals.css">`
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - SVG content should be preserved inline as-is
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ - Keep all `id` attributes on sections for anchor navigation
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
138 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Source files:
139 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ {source_block}
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
141 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Return ONLY a valid JSON object, no explanation."""
142 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
143 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ response = call_ai(prompt, NEXTJS_SYSTEM, max_tokens=16384)
144 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return _parse_layout_json(response)
145 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
146 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
147 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _collect_nextjs_sources(info: dict) -> dict:
148 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
149 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Collect relevant source files from a Next.js app into a dict
150 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ keyed by relative path. Applies priority-based context budgeting.
151 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
152 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ app_dir = info['app_dir']
153 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sources = {}
154 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ budget = 80000
155 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
156 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Tier 1: Layout and page entry points (always include)
157 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier1 = []
158 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if info.get('layout_file'):
159 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier1.append(info['layout_file'])
160 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if info.get('page_file'):
161 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier1.append(info['page_file'])
162 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
163 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Tier 2: Section-level components (most important for structure)
164 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier2 = []
165 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Tier 3: Page components
166 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier3 = []
167 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Tier 4: UI/marketing components
168 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier4 = []
169 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Tier 5: CSS and config
170 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier5 = list(info.get('css_files', []))
171 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
172 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Walk source directories looking for components
173 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for search_root in [os.path.join(app_dir, 'src'), os.path.join(app_dir, 'app'), app_dir]:
174 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if not os.path.isdir(search_root):
175 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
176 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for root, dirs, files in os.walk(search_root):
177 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Skip junk
178 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.next', '__MACOSX', '.git', '__tests__')]
179 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for f in files:
180 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if not f.endswith(('.tsx', '.jsx', '.ts', '.js')):
181 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
182 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ full = os.path.join(root, f)
183 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Skip test files, config files, API routes
184 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if '.test.' in f or '.spec.' in f:
185 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
186 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if '/api/' in full:
187 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
188 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Skip files already in tier 1
189 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if full in tier1:
190 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
191 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
192 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ rel = os.path.relpath(root, app_dir)
193 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ basename = f.lower()
194 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
195 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if 'section' in basename or 'section' in rel.lower():
196 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier2.append(full)
197 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ elif 'page' in basename and 'page' not in rel.lower().split('app')[-1:]:
198 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier3.append(full)
199 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ elif any(k in rel.lower() for k in ('components', 'marketing')):
200 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ tier4.append(full)
201 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
202 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Assemble by priority, tracking budget
203 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ used = 0
204 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for tier_files in [tier1, tier2, tier3, tier4, tier5]:
205 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for fpath in tier_files:
206 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if not os.path.isfile(fpath):
207 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
208 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try:
209 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ with open(fpath, 'r', errors='replace') as fh:
210 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ content = fh.read()
211 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ except OSError:
212 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
213 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
214 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ rel_path = os.path.relpath(fpath, app_dir)
215 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Skip if already collected (dedup across tiers)
216 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if rel_path in sources:
217 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ continue
218 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
219 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Truncate individual large files
220 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(content) > 8000:
221 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ content = content[:8000] + '\n// ... [truncated]'
222 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
223 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if used + len(content) > budget:
224 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ remaining = budget - used
225 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if remaining > 500:
226 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ content = content[:remaining] + '\n// ... [truncated - budget]'
227 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sources[rel_path] = content
228 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ used += len(content)
229 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ break
230 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ sources[rel_path] = content
231 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ used += len(content)
232 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
233 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info(f"Collected {len(sources)} source files ({used} chars) from Next.js app")
234 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return sources
235 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
71 236 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
72 237 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
def hugoify_dir(theme_dir: str) -> str:
73 238 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
74 239 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Validate and optionally augment an existing Hugo theme directory.
75 240 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Returns a status message.
@@ -101,18 +266,24 @@
101 266 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# CLI entry point (used by cli.py)
102 267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
def hugoify(path: str) -> str:
103 268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
104 269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
Entry point for the CLI 'hugoify' command.
105 270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
If path is a Hugo theme dir: validate it.
271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ If path is a Next.js app: convert React components to Hugo.
106 272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
If path is an HTML file or raw HTML dir: convert it.
107 273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"""
108 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- from .theme_finder import find_hugo_theme, find_raw_html_files
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ from .theme_finder import find_hugo_theme, find_nextjs_app, find_raw_html_files
109 275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
110 276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
info = find_hugo_theme(path)
111 277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if info:
112 278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return hugoify_dir(info['theme_dir'])
113 279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ nextjs_info = find_nextjs_app(path)
281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if nextjs_info:
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ layouts = hugoify_nextjs(nextjs_info)
283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return f"Converted Next.js app to {len(layouts)} layout files: {list(layouts.keys())}"
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
114 285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if os.path.isfile(path) and path.endswith('.html'):
115 286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
layouts = hugoify_html(path)
116 287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return f"Converted to {len(layouts)} layout files: {list(layouts.keys())}"
117 288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
118 289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
html_files = find_raw_html_files(path)
@@ -130,21 +301,72 @@
130 301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# ---------------------------------------------------------------------------
131 302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# Helpers
132 303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# ---------------------------------------------------------------------------
133 304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
134 305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
def _parse_layout_json(response: str) -> dict:
135 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- """Extract JSON from AI response, even if surrounded by prose."""
136 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- # Try to find JSON block
137 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- match = re.search(r'\{.*\}', response, re.DOTALL)
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """Extract JSON from AI response, even if surrounded by prose or markdown fences."""
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Strip markdown fences if present
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ stripped = re.sub(r'```(?:json)?\s*', '', response)
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ stripped = re.sub(r'```\s*$', '', stripped.strip())
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Try the full stripped response as JSON first
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ try:
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ result = json.loads(stripped)
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if isinstance(result, dict):
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info(f"Parsed {len(result)} layout files from AI response")
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return result
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ except json.JSONDecodeError:
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pass
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Try to find JSON block (outermost braces)
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ match = re.search(r'\{.*\}', stripped, re.DOTALL)
138 322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if match:
139 323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
try:
140 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
- return json.loads(match.group(0))
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ result = json.loads(match.group(0))
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if isinstance(result, dict):
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info(f"Parsed {len(result)} layout files from AI response (extracted)")
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return result
141 328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
except json.JSONDecodeError:
142 329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
pass
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # AI sometimes uses backtick-delimited values instead of JSON strings.
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Parse with a regex-based key-value extractor.
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ backtick_result = _parse_backtick_json(match.group(0))
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if backtick_result:
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.info(f"Parsed {len(backtick_result)} layout files from backtick-delimited response")
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return backtick_result
143 337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
144 338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
# Fallback: return a minimal layout
145 339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
logging.warning("Could not parse AI response as JSON, using fallback layouts")
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ logging.debug(f"AI response was: {response[:500]!r}")
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return {
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "_default/baseof.html": _fallback_baseof(),
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "partials/header.html": "<header><!-- header --></header>",
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "partials/footer.html": "<footer>{{ .Site.Params.copyright }}</footer>",
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ "index.html": '{{ define "main" }}<main>{{ .Content }}</main>{{ end }}',
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _parse_backtick_json(text: str) -> dict | None:
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ Parse a JSON-like object where values are backtick-delimited template literals
352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ instead of proper JSON strings. This happens when the AI uses JS template syntax.
353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ e.g.: { "key": `<html>...</html>` }
354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """
355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ result = {}
356 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ # Match "key": `value` pairs where value can span multiple lines
357 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ pattern = re.compile(r'"([^"]+)"\s*:\s*`(.*?)`(?:\s*[,}])', re.DOTALL)
358 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for m in pattern.finditer(text):
359 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ key = m.group(1)
360 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ value = m.group(2).strip()
361 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ result[key] = value
362 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
363 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return result if result else None
364 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
365 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
366 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ def _fallback_layouts() -> dict:
367 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ """Minimal fallback when source collection fails."""
146 368 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return {
147 369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"_default/baseof.html": _fallback_baseof(),
148 370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"partials/header.html": "<header><!-- header --></header>",
149 371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"partials/footer.html": "<footer>{{ .Site.Params.copyright }}</footer>",
150 372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
"index.html": '{{ define "main" }}<main>{{ .Content }}</main>{{ end }}',
151 373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!