@@ -14,7 +14,8 @@ import type { ResolvedConfig } from '../config/types.js';
1414import { getDesignSystem , getDarkModeOverride , type DesignSystem } from '../theme/default/design-systems/index.js' ;
1515import { globalTagValidator , formatValidationError } from '../markdown/tag-validator.js' ;
1616import { 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
1920const BUILD_MODULE_DIR = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
2021const 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 ( / < s p a n c l a s s = " l i n e " > < \/ s p a n > ( \s * < \/ c o d e > ) / 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
0 commit comments