#!/usr/bin/env node /** * PDF Generation Script * * Uses Puppeteer to render each HTML page to PDF. * Run with: node generate-pdfs.js * * Prerequisites: npm install puppeteer */ const puppeteer = require('puppeteer'); const http = require('http'); const fs = require('fs'); const path = require('path'); // Configuration const SITE_DIR = __dirname; // Running from /srv in Docker, which contains all site files const PDF_DIR = path.join(SITE_DIR, 'pdfs'); const PORT = 8765; // MIME types for static file serving const MIME_TYPES = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', }; /** * Find all HTML files in a directory recursively */ function findHtmlFiles(dir, baseDir = dir) { const files = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Skip pdfs directory, node_modules, and hidden directories if (entry.name === 'pdfs' || entry.name === 'node_modules' || entry.name.startsWith('.')) { continue; } files.push(...findHtmlFiles(fullPath, baseDir)); } else if (entry.isFile() && entry.name.endsWith('.html')) { // Skip template files if (entry.name.includes('template') || entry.name.includes('with-includes')) { continue; } const relativePath = path.relative(baseDir, fullPath); files.push(relativePath); } } return files; } /** * Create a simple static file server */ function createServer() { return http.createServer((req, res) => { let urlPath = req.url.split('?')[0]; if (urlPath === '/') urlPath = '/index.html'; const filePath = path.join(SITE_DIR, urlPath); const ext = path.extname(filePath); const contentType = MIME_TYPES[ext] || 'application/octet-stream'; fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end('Not found'); return; } res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); }); } /** * Ensure directory exists */ function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * Generate PDF for a single HTML file */ async function generatePdf(browser, htmlFile) { const page = await browser.newPage(); // Convert file path to URL path const urlPath = '/' + htmlFile.replace(/\\/g, '/'); const url = `http://localhost:${PORT}${urlPath}`; // Determine output PDF path const pdfRelativePath = htmlFile.replace(/\.html$/, '.pdf'); const pdfPath = path.join(PDF_DIR, pdfRelativePath); // Ensure output directory exists ensureDir(path.dirname(pdfPath)); try { // Navigate to the page and wait for content to load await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 }); // Wait for includes to load - check if header-include exists and has content try { await page.waitForFunction(() => { const headerInclude = document.getElementById('header-include'); // If header-include exists, wait for it to have content (includes loaded) // If it doesn't exist, that's fine too (page doesn't use includes) if (headerInclude) { return headerInclude.innerHTML.trim().length > 0 || document.querySelector('.main-nav') !== null; } return true; // No header-include, page is ready }, { timeout: 10000 }); } catch (waitError) { // If waiting for includes times out, continue anyway console.warn(`Warning: Includes may not have loaded for ${htmlFile}, continuing...`); } // Wait a bit more for any remaining JavaScript to finish await page.waitForTimeout(500); // Generate PDF await page.pdf({ path: pdfPath, format: 'A4', printBackground: true, margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' } }); console.log(`āœ“ Generated: ${pdfRelativePath}`); } catch (error) { console.error(`āœ— Failed: ${htmlFile} - ${error.message}`); // Don't re-throw - let the caller handle it } finally { try { await page.close(); } catch (closeError) { // Page might already be closed, ignore } } } /** * Main function */ async function main() { console.log('PDF Generation Script'); console.log('=====================\n'); // Find all HTML files const htmlFiles = findHtmlFiles(SITE_DIR); console.log(`Found ${htmlFiles.length} HTML files to process\n`); if (htmlFiles.length === 0) { console.log('No HTML files found. Exiting.'); return; } // Clean and create PDF directory if (fs.existsSync(PDF_DIR)) { fs.rmSync(PDF_DIR, { recursive: true }); } ensureDir(PDF_DIR); // Start local server const server = createServer(); await new Promise(resolve => server.listen(PORT, resolve)); console.log(`Local server started on port ${PORT}\n`); // Launch browser // Use system Chromium if available (in Docker), otherwise use Puppeteer's bundled Chrome const browser = await puppeteer.launch({ headless: 'new', executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--single-process', '--disable-gpu' ] }); let successCount = 0; let failCount = 0; try { // Generate PDFs for each HTML file for (const htmlFile of htmlFiles) { try { await generatePdf(browser, htmlFile); successCount++; } catch (error) { failCount++; console.error(`Failed to generate PDF for ${htmlFile}:`, error.message); // Continue with next file instead of stopping } } console.log(`\nāœ“ PDF generation complete!`); console.log(` Success: ${successCount}, Failed: ${failCount}`); console.log(` Files saved to: ${PDF_DIR}`); } finally { await browser.close(); server.close(); } } // Run the script main().catch(error => { console.error('Fatal error:', error); process.exit(1); });