diff --git a/docker/resume/Dockerfile b/docker/resume/Dockerfile index dde1a56..53c75e3 100644 --- a/docker/resume/Dockerfile +++ b/docker/resume/Dockerfile @@ -1,7 +1,7 @@ FROM caddy:2.7-alpine # Install dependencies -RUN apk add --no-cache nodejs bash +RUN apk add --no-cache nodejs npm bash # Set working directory WORKDIR /srv @@ -9,11 +9,17 @@ WORKDIR /srv # Copy website files COPY . /srv +# Install npm dependencies for PDF generation +RUN cd /srv && npm install --production + # Run all update scripts (sitemap, navigation, stories, CSP hashes, accessibility fixes) RUN cd /srv && \ chmod +x update-all.sh && \ ./update-all.sh +# Generate PDFs for all pages +RUN cd /srv && npm run generate-pdfs + # Expose port EXPOSE 8080 diff --git a/docker/resume/generate-pdfs.js b/docker/resume/generate-pdfs.js new file mode 100644 index 0000000..b97261c --- /dev/null +++ b/docker/resume/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 = __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 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/resume/package.json b/docker/resume/package.json new file mode 100644 index 0000000..759fbf3 --- /dev/null +++ b/docker/resume/package.json @@ -0,0 +1,16 @@ +{ + "name": "docker", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "generate-pdfs": "node generate-pdfs.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "puppeteer": "^21.0.0" + } +} diff --git a/docker/resume/update-all.sh b/docker/resume/update-all.sh index b382b83..b39a246 100755 --- a/docker/resume/update-all.sh +++ b/docker/resume/update-all.sh @@ -53,28 +53,33 @@ fi echo "=== Ensuring accessibility fixes are applied ===" # Check if stories.css exists if [ -f "$SCRIPT_DIR/stories/stories.css" ]; then - # Check if the file already has the accessibility fixes - if ! grep -q "color: #004494;" "$SCRIPT_DIR/stories/stories.css"; then - echo "Applying accessibility fixes to stories.css..." - # Use sed to add the color property to story-nav-link class - sed -i '' 's/\.story-nav-link {/\.story-nav-link {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css" - sed -i '' 's/\.story-nav-link:hover {/\.story-nav-link:hover {\n color: #003366; \/* Even darker on hover for better visibility *\//g' "$SCRIPT_DIR/stories/stories.css" - - # Update placeholder-notice links - sed -i '' 's/\.placeholder-notice a {/\.placeholder-notice a {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css" - - # Add hover state for placeholder-notice links if it doesn't exist - if ! grep -q "\.placeholder-notice a:hover" "$SCRIPT_DIR/stories/stories.css"; then - echo -e "\n.placeholder-notice a:hover {\n color: #003366; /* Even darker on hover */\n}" >> "$SCRIPT_DIR/stories/stories.css" + # Check if the file already has the accessibility fixes + if ! grep -q "color: #004494;" "$SCRIPT_DIR/stories/stories.css"; then + echo "Applying accessibility fixes to stories.css..." + # Use sed to add the color property to story-nav-link class + sed -i '' 's/\.story-nav-link {/\.story-nav-link {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css" + sed -i '' 's/\.story-nav-link:hover {/\.story-nav-link:hover {\n color: #003366; \/* Even darker on hover for better visibility *\//g' "$SCRIPT_DIR/stories/stories.css" + + # Update placeholder-notice links + sed -i '' 's/\.placeholder-notice a {/\.placeholder-notice a {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css" + + # Add hover state for placeholder-notice links if it doesn't exist + if ! grep -q "\.placeholder-notice a:hover" "$SCRIPT_DIR/stories/stories.css"; then + echo -e "\n.placeholder-notice a:hover {\n color: #003366; /* Even darker on hover */\n}" >> "$SCRIPT_DIR/stories/stories.css" + fi + + echo "Accessibility fixes applied to stories.css" + else + echo "Accessibility fixes already present in stories.css" fi - - echo "Accessibility fixes applied to stories.css" - else - echo "Accessibility fixes already present in stories.css" - fi else - echo "⚠️ stories/stories.css not found, skipping accessibility fixes" + echo "⚠️ stories/stories.css not found, skipping accessibility fixes" fi +# Generate PDFs (this is now done in Dockerfile, but kept here for manual runs) +echo "=== PDF generation ===" +echo "Note: PDFs are generated during Docker build. Skipping in update-all.sh" +echo "To generate PDFs manually, run: npm run generate-pdfs" + echo "=== All updates completed successfully ===" echo "To apply changes, restart the server using: ./caddy.sh"