90b35f9d0c
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>
83 lines
2.6 KiB
JavaScript
83 lines
2.6 KiB
JavaScript
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); });
|