From 83c0ae74f78175e0b99be1afd000eb53543c343d Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 30 Nov 2025 15:46:21 -0500 Subject: [PATCH] Add PDF generation for static site pages - Add generate-pdfs.js script using Puppeteer to render HTML pages to PDF - Update package.json with puppeteer dependency and npm script - Add dynamic PDF download link to footer (checks if PDF exists, shows link) - Add docker/resume/pdfs/ to .gitignore (generated at deploy time) Run 'npm install && npm run generate-pdfs' during deployment --- .gitignore | 3 + docker/generate-pdfs.js | 200 +++++++++++++++++++++++++++++ docker/package.json | 8 +- docker/resume/includes/footer.html | 30 +++++ docker/resume/markdown-loader.js | 1 + 5 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 docker/generate-pdfs.js diff --git a/.gitignore b/.gitignore index e9fbcb7..077e047 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ node_modules/ # OS files .DS_Store Thumbs.db + +# Generated PDFs (created at deploy time) +docker/resume/pdfs/ diff --git a/docker/generate-pdfs.js b/docker/generate-pdfs.js new file mode 100644 index 0000000..788a429 --- /dev/null +++ b/docker/generate-pdfs.js @@ -0,0 +1,200 @@ +#!/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 = path.join(__dirname, 'resume'); +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 a bit for any JavaScript to finish + await page.waitForTimeout(1000); + + // 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}`); + } finally { + await page.close(); + } +} + +/** + * 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 + const browser = await puppeteer.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + try { + // Generate PDFs for each HTML file + for (const htmlFile of htmlFiles) { + await generatePdf(browser, htmlFile); + } + + console.log(`\nāœ“ PDF generation complete! 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); +}); + diff --git a/docker/package.json b/docker/package.json index b736e01..759fbf3 100644 --- a/docker/package.json +++ b/docker/package.json @@ -3,10 +3,14 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "generate-pdfs": "node generate-pdfs.js" }, "keywords": [], "author": "", "license": "ISC", - "description": "" + "description": "", + "dependencies": { + "puppeteer": "^21.0.0" + } } diff --git a/docker/resume/includes/footer.html b/docker/resume/includes/footer.html index c5598e7..d1bf2c7 100644 --- a/docker/resume/includes/footer.html +++ b/docker/resume/includes/footer.html @@ -1,6 +1,36 @@

Accessibility: This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.

+ + + + diff --git a/docker/resume/markdown-loader.js b/docker/resume/markdown-loader.js index 8690ded..7c96311 100644 --- a/docker/resume/markdown-loader.js +++ b/docker/resume/markdown-loader.js @@ -65,3 +65,4 @@ +