Skip to content

Commit 41286be

Browse files
committed
fix: make static site generation work
1 parent 516cb8b commit 41286be

5 files changed

Lines changed: 249 additions & 97 deletions

File tree

packages/markopress/src/build/index.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import type { ResolvedConfig } from '../config/types.js';
1414
import { getDesignSystem, getDarkModeOverride, type DesignSystem } from '../theme/default/design-systems/index.js';
1515
import { globalTagValidator, formatValidationError } from '../markdown/tag-validator.js';
1616
import { PluginManager } from '../plugin/manager.js';
17-
import { loadMarkdownModule, registerMarkdownContent } from './vite-markdown-plugin.js';
17+
import { loadMarkdownModule, registerMarkdownContent, escapeMarkoText } from './vite-markdown-plugin.js';
18+
import { renderMarkdown } from '../markdown/renderer.js';
1819

1920
const BUILD_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
2021
const MARKOPRESS_PACKAGE_ROOT = path.resolve(BUILD_MODULE_DIR, '..', '..');
@@ -202,9 +203,10 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
202203
}
203204
}
204205

205-
const enhancementsPath = path.join(generatedDir, 'module-enhancements.json');
206-
await fs.writeFile(enhancementsPath, JSON.stringify(moduleEnhancements, null, 2), 'utf-8');
207-
console.log(` Wrote module enhancements to src/.generated/module-enhancements.json\n`);
206+
const enhancementsPath = path.join(generatedDir, 'module-enhancements.js');
207+
const enhancementsJs = `// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(moduleEnhancements, null, 2)};\n`;
208+
await fs.writeFile(enhancementsPath, enhancementsJs, 'utf-8');
209+
console.log(` Wrote module enhancements to src/.generated/module-enhancements.js\n`);
208210
}
209211

210212
// Step 5: Initialize tag validator if Marko tags enabled
@@ -244,8 +246,57 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
244246
const t6 = time('Route generation');
245247
t6.start();
246248
const routeMode = useCatchAllRoutes ?? config.build.useCatchAllRoutes;
249+
250+
// Step 7.5: Pre-render markdown to .marko files (build mode only)
251+
// This runs before route generation so that import.meta.glob can discover the files
252+
console.log('📄 Pre-rendering markdown to .marko files...');
253+
const t7b = time('Pre-render markdown');
254+
t7b.start();
255+
const generatedMarkdownDir = path.join(root, 'src', '.generated', 'markdown');
256+
await fs.mkdir(generatedMarkdownDir, { recursive: true });
257+
const markdownMetadata: Record<string, { frontmatter: Record<string, unknown>; headers: unknown[] }> = {};
258+
let preRenderedCount = 0;
259+
260+
for (const mod of modules) {
261+
const moduleMarkdownDir = path.join(generatedMarkdownDir, mod.id);
262+
await fs.mkdir(moduleMarkdownDir, { recursive: true });
263+
264+
for (const file of mod.files) {
265+
try {
266+
const source = await fs.readFile(file.filePath, 'utf-8');
267+
const rendered = await renderMarkdown(source);
268+
269+
// Write .marko file with escaped HTML
270+
// Clean up Shiki output: remove empty trailing <span class="line"></span>
271+
// before </code> since Marko's parser is strict about tag nesting
272+
let cleanHtml = rendered.html.replace(/<span class="line"><\/span>(\s*<\/code>)/g, '$1');
273+
const markoContent = `<div class="markdown-content">\n${escapeMarkoText(cleanHtml)}\n</div>`;
274+
const markoPath = path.join(moduleMarkdownDir, `${file.slug}.marko`);
275+
await fs.writeFile(markoPath, markoContent);
276+
277+
// Store metadata
278+
const metaKey = `${mod.id}/${file.slug}`;
279+
markdownMetadata[metaKey] = {
280+
frontmatter: rendered.frontmatter,
281+
headers: rendered.headers || [],
282+
};
283+
284+
preRenderedCount++;
285+
} catch (err) {
286+
console.warn(` Warning: Failed to pre-render ${mod.id}/${file.slug}:`, err);
287+
}
288+
}
289+
}
290+
291+
// Write metadata as JS module (placed outside markdown/ dir to avoid vite plugin collision)
292+
const metadataPath = path.join(root, 'src', '.generated', 'content-metadata.js');
293+
const metadataJs = `// Auto-generated by MarkoPress - Do not edit\nexport default ${JSON.stringify(markdownMetadata, null, 2)};\n`;
294+
await fs.writeFile(metadataPath, metadataJs);
295+
t7b.end();
296+
console.log(` Pre-rendered ${preRenderedCount} markdown files\n`);
297+
247298
if (routeMode) {
248-
await generateCatchAllRoutes(manifest, routesDir, config, modules, debug);
299+
await generateCatchAllRoutes(manifest, routesDir, config, modules, debug, true);
249300
console.log(' Using catch-all dynamic routes');
250301
} else {
251302
await generateRoutes(manifest, routesDir, config, modules, debug);
@@ -347,6 +398,32 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
347398
// t13.end();
348399
// console.log(' Theme components copied\n');
349400

401+
// Step 15.5: Generate static URL manifest for the static adapter
402+
// This allows @marko/run-adapter-static to crawl all dynamic routes
403+
const staticUrls: string[] = [];
404+
for (const mod of modules) {
405+
for (const file of mod.files) {
406+
if (mod.id === 'pages') {
407+
// Pages are root-level: index → /, other → /slug
408+
staticUrls.push(file.id === 'index' ? '/' : `/${file.id}`);
409+
} else {
410+
staticUrls.push(file.urlPath);
411+
}
412+
}
413+
}
414+
// Add plugin routes (e.g., /blog index page)
415+
for (const routePath of Object.keys(routeManifest)) {
416+
if (!staticUrls.includes(routePath)) {
417+
staticUrls.push(routePath);
418+
}
419+
}
420+
const staticUrlsPath = path.join(root, 'src', '.generated', 'static-urls.json');
421+
await fs.mkdir(path.dirname(staticUrlsPath), { recursive: true });
422+
await fs.writeFile(staticUrlsPath, JSON.stringify(staticUrls, null, 2));
423+
if (debug) {
424+
console.log(` Generated static URL manifest: ${staticUrls.length} URLs`);
425+
}
426+
350427
// Step 16: Build with @marko/run
351428
console.log('🔨 Building with @marko/run...');
352429
const t14 = time('@marko/run build');
@@ -1724,10 +1801,11 @@ export async function generateCatchAllRoutes(
17241801
routesDir: string,
17251802
config: ResolvedConfig,
17261803
modules: ContentModule[],
1727-
debug: boolean
1804+
debug: boolean,
1805+
isBuild: boolean = true
17281806
): Promise<void> {
17291807
console.log(' Using catch-all dynamic routes...');
1730-
console.log(' Content will be rendered at request time');
1808+
console.log(` Mode: ${isBuild ? 'build (pre-compiled)' : 'dev (request-time rendering)'}`);
17311809

17321810
// Generate catch-all routes for each content directory from config
17331811
const contentDirs = config.content || {};
@@ -1750,6 +1828,7 @@ export async function generateCatchAllRoutes(
17501828
CONTENT_TYPE: 'pages',
17511829
CONFIG_PATH: '../_config.js',
17521830
VITE_PLUGIN_PATH: 'markopress/build',
1831+
IS_BUILD: isBuild ? 'true' : 'false',
17531832
});
17541833
await fs.writeFile(path.join(pagesDir, '+handler.js'), pagesHandler);
17551834

@@ -1770,6 +1849,7 @@ export async function generateCatchAllRoutes(
17701849
CONTENT_TYPE: moduleId,
17711850
CONFIG_PATH: '../../_config.js',
17721851
VITE_PLUGIN_PATH: 'markopress/build',
1852+
IS_BUILD: isBuild ? 'true' : 'false',
17731853
});
17741854
await fs.writeFile(path.join(moduleDir, '+handler.js'), handlerTemplate);
17751855

packages/markopress/src/build/vite-markdown-plugin.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'node:path';
2+
import { existsSync } from 'node:fs';
23

34
const GLOBAL_COMPONENTS_KEY = '__MARKOPRESS_MARKDOWN_COMPONENTS__';
45
const markdownComponents = ((globalThis as unknown) as Record<string, Map<string, string>>)[GLOBAL_COMPONENTS_KEY] ??= new Map();
@@ -65,6 +66,13 @@ export function markdownContentPlugin() {
6566
return undefined;
6667
}
6768

69+
// If the file exists on disk (pre-rendered during build), let Vite handle it normally
70+
const cleanPath = id.split('?', 1)[0];
71+
if (existsSync(cleanPath)) {
72+
debug('load_FILE_EXISTS', { contentId, cleanPath });
73+
return undefined;
74+
}
75+
6876
debug('load_MATCH', { contentId, storedKeys: Array.from(markdownComponents.keys()) });
6977

7078
const html = markdownComponents.get(contentId);
@@ -104,7 +112,7 @@ function escapeHtml(value: string): string {
104112
.replace(/'/g, '&#39;');
105113
}
106114

107-
function escapeMarkoText(value: string): string {
115+
export function escapeMarkoText(value: string): string {
108116
const input = String(value);
109117
let output = '';
110118
let inTag = false;
@@ -130,6 +138,13 @@ function escapeMarkoText(value: string): string {
130138
continue;
131139
}
132140

141+
// Escape // in text content - Marko treats it as a JS comment
142+
if (!inTag && char === '/' && input[i + 1] === '/') {
143+
output += '&#47;/';
144+
i++;
145+
continue;
146+
}
147+
133148
output += char;
134149
}
135150

packages/markopress/src/dev/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function startDevServer(options: DevServerOptions = {}) {
6262
}
6363

6464
if (routeMode) {
65-
await generateCatchAllRoutes(manifest, routesDir, config, modules, false);
65+
await generateCatchAllRoutes(manifest, routesDir, config, modules, false, false);
6666
console.log(' Using catch-all dynamic routes');
6767
} else {
6868
await generateRoutes(manifest, routesDir, config, modules, false);

0 commit comments

Comments
 (0)