Set up book project: Markdown→CSS→PDF pipeline for KDP
Adds the full authoring and build toolchain for "Das Kaleidoskop der Schlummerwelten" — all 12 story content files in Markdown, Nunjucks HTML templates, CSS print layout, and Puppeteer-based PDF generation targeting Amazon KDP (8.5×8.5 in, 0.125in bleed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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 build() {
|
||||
const { stories, finale } = await loadStories();
|
||||
|
||||
const html = env.render('book.html', { stories, finale });
|
||||
|
||||
const outPath = join(root, 'output', 'book.html');
|
||||
await writeFile(outPath, html, 'utf-8');
|
||||
|
||||
const pageCount = stories.reduce((acc, s) => acc + s.scenes.length * 2, 0) + 5;
|
||||
console.log(`Built output/book.html — ${stories.length} stories, ~${pageCount} pages`);
|
||||
}
|
||||
|
||||
build().catch(err => { console.error(err); process.exit(1); });
|
||||
@@ -0,0 +1,51 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { access } from 'fs/promises';
|
||||
|
||||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function fileExists(path) {
|
||||
try { await access(path); return true; } catch { return false; }
|
||||
}
|
||||
const root = resolve(__dir, '..');
|
||||
const inputPath = resolve(root, 'output', 'book.html');
|
||||
const outputPath = resolve(root, 'output', 'kaleidoskop.pdf');
|
||||
|
||||
async function generate() {
|
||||
try {
|
||||
await access(inputPath);
|
||||
} catch {
|
||||
console.error('output/book.html not found — run `pnpm build` first');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// On ARM64 (e.g. WSL2 on Apple Silicon / Raspberry Pi), Puppeteer's bundled
|
||||
// Chrome is x86-64 and won't run. Use the system Chromium instead:
|
||||
// sudo apt-get install -y chromium
|
||||
const systemChromium = '/usr/bin/chromium';
|
||||
const useSystem = await fileExists(systemChromium);
|
||||
|
||||
console.log(`Launching Puppeteer (${useSystem ? 'system Chromium' : 'bundled Chrome'})…`);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
...(useSystem ? { executablePath: systemChromium } : {}),
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto(`file://${inputPath}`, { waitUntil: 'networkidle0' });
|
||||
|
||||
// 8.75 × 8.75 inches = trim (8.5×8.5) + 0.125in bleed on each side
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
width: '8.75in',
|
||||
height: '8.75in',
|
||||
printBackground: true,
|
||||
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log(`PDF written to output/kaleidoskop.pdf`);
|
||||
}
|
||||
|
||||
generate().catch(err => { console.error(err); process.exit(1); });
|
||||
Reference in New Issue
Block a user