Files
kaleidoskop/scripts/build.js
T
valknar 90b35f9d0c Remove interior title page — cover replaces it
The title page is redundant since the KDP cover (cover.pdf) already
establishes title and author. Interior now opens directly on the
copyright page, followed by the TOC and stories (99 pages total).
Removed unused .page--title CSS from layout and typography.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:56:59 +02:00

83 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readdir, readFile, writeFile, access } from 'fs/promises';
import { join, resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import matter from 'gray-matter';
import { marked } from 'marked';
import nunjucks from 'nunjucks';
const __dir = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dir, '..');
// Nunjucks: load templates relative to project root
const env = nunjucks.configure(join(root, 'templates'), { autoescape: true });
async function fileExists(path) {
try { await access(path); return true; } catch { return false; }
}
async function loadStories() {
const contentDir = join(root, 'content');
const files = (await readdir(contentDir)).filter(f => f.endsWith('.md')).sort();
const stories = [];
let finale = null;
for (const file of files) {
const raw = await readFile(join(contentDir, file), 'utf-8');
const { data, content } = matter(raw);
if (data.type === 'front-matter') continue;
if (data.type === 'finale') {
finale = data;
continue;
}
// Split body by --- into individual scene texts
const sceneTexts = content.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
const scenes = await Promise.all(
(data.scenes || []).map(async (scene, i) => {
const imagePath = join(root, scene.image);
const imageExists = await fileExists(imagePath);
return {
...scene,
imageExists,
// Relative path from output/book.html back to project root
image: `../${scene.image}`,
html: marked.parse(sceneTexts[i] || ''),
};
})
);
stories.push({ ...data, scenes });
}
return { stories, finale: finale || {} };
}
async function loadFrontMatter() {
const raw = await readFile(join(root, 'content', '00-front-matter.md'), 'utf-8');
return matter(raw).data;
}
async function build() {
const [{ stories, finale }, frontMatter] = await Promise.all([loadStories(), loadFrontMatter()]);
const html = env.render('book.html', { stories, finale, frontMatter });
const outPath = join(root, 'output', 'book.html');
await writeFile(outPath, html, 'utf-8');
// copyright + TOC + (4 scenes × 2 pages per story) + finale
const pageCount = 2 + stories.reduce((acc, s) => acc + s.scenes.length * 2, 0) + 1;
await writeFile(
join(root, 'output', 'book-meta.json'),
JSON.stringify({ pageCount, storyCount: stories.length, builtAt: new Date().toISOString() }, null, 2),
'utf-8'
);
console.log(`Built output/book.html — ${stories.length} stories, ${pageCount} pages`);
}
build().catch(err => { console.error(err); process.exit(1); });