#!/usr/bin/env node /** * Prebuild script: reads content/ files and generates src/generated/content.js * Run automatically via npm prestart / prebuild hooks. */ const fs = require('fs'); const path = require('path'); const CONTENT_DIR = path.resolve(__dirname, '../content'); const OUTPUT_FILE = path.resolve(__dirname, '../src/generated/content.js'); // --- Frontmatter parser --- function parseFrontmatter(raw) { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n)?([\s\S]*)$/); if (!match) return { frontmatter: {}, content: raw.trim() }; const lines = match[1].split('\n'); const frontmatter = {}; for (const line of lines) { const m = line.match(/^(\w[\w-]*)\s*:\s*(.*)/); if (m) frontmatter[m[1]] = m[2].trim(); } return { frontmatter, content: match[2].trim() }; } // --- Route derivation --- function routeFromDir(dirName) { // content/index.md -> / // content/about/index.md -> /about if (dirName === '') return '/'; return '/' + dirName; } // --- Load site config --- const configPath = path.join(CONTENT_DIR, '_config.json'); if (!fs.existsSync(configPath)) { console.error('Missing content/_config.json'); process.exit(1); } const siteConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); // --- Discover page dirs --- // Root-level index.md lives directly in content/ // Subdir index.md lives in content// const pages = {}; function processDir(dirPath, slug) { const indexPath = path.join(dirPath, 'index.md'); if (!fs.existsSync(indexPath)) return; const raw = fs.readFileSync(indexPath, 'utf8'); const { frontmatter, content } = parseFrontmatter(raw); // Resolve images: check dirPath/images.json first, then dirPath/images/ subfolder let images = []; const localImagesJsonPath = path.join(dirPath, 'images.json'); if (fs.existsSync(localImagesJsonPath)) { images = JSON.parse(fs.readFileSync(localImagesJsonPath, 'utf8')); } else { const imagesDirPath = path.join(dirPath, 'images'); if (fs.existsSync(imagesDirPath)) { const files = fs.readdirSync(imagesDirPath); images = files.map(f => `/content-images/${slug}/${f}`); } } const route = routeFromDir(slug); pages[route] = { frontmatter, content, images }; } function walkContent(dirPath, slug) { processDir(dirPath, slug); const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith('_')) continue; const childSlug = slug ? `${slug}/${entry.name}` : entry.name; walkContent(path.join(dirPath, entry.name), childSlug); } } walkContent(CONTENT_DIR, ''); // --- Auto-discover collections --- // If collectionRoot is set, find all direct sub-pages and populate siteConfig.collections // so templates can consume them without manual markdown links. if (siteConfig.collectionRoot) { const rootRoute = '/' + siteConfig.collectionRoot; siteConfig.collections = Object.entries(pages) .filter(([route]) => { const prefix = rootRoute + '/'; if (!route.startsWith(prefix)) return false; const rest = route.slice(prefix.length); return !rest.includes('/'); // direct children only, no deeper nesting }) .map(([route, page]) => ({ slug: route.split('/').pop(), label: page.frontmatter.title || route.split('/').pop(), description: page.frontmatter.description || '', previewImage: page.images[0] || null, path: route, })); console.log(`[build-content] Collections: ${siteConfig.collections.map(c => c.slug).join(', ')}`); } // --- Write output --- const output = `// AUTO-GENERATED by scripts/build-content.js — do not edit by hand. export const siteConfig = ${JSON.stringify(siteConfig, null, 2)}; export const pages = ${JSON.stringify(pages, null, 2)}; `; fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); fs.writeFileSync(OUTPUT_FILE, output, 'utf8'); console.log(`[build-content] Generated ${OUTPUT_FILE}`); console.log(`[build-content] Pages: ${Object.keys(pages).join(', ')}`);