jakesphotos/scripts/build-content.js
2026-03-04 10:58:43 -08:00

118 lines
4.1 KiB
JavaScript

#!/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/<slug>/
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(', ')}`);