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.
+ + + +