Add PDF generation for static site pages
ci/woodpecker/push/woodpecker Pipeline was successful Details

- 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
This commit is contained in:
Colin 2025-11-30 15:46:21 -05:00
parent 73b83fc3fd
commit 83c0ae74f7
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
5 changed files with 240 additions and 2 deletions

3
.gitignore vendored
View File

@ -13,3 +13,6 @@ node_modules/
# OS files # OS files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Generated PDFs (created at deploy time)
docker/resume/pdfs/

200
docker/generate-pdfs.js Normal file
View File

@ -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);
});

View File

@ -3,10 +3,14 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"generate-pdfs": "node generate-pdfs.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "" "description": "",
"dependencies": {
"puppeteer": "^21.0.0"
}
} }

View File

@ -1,6 +1,36 @@
<hr> <hr>
<p class="accessibility-notice"><strong>Accessibility:</strong> 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.</p> <p class="accessibility-notice"><strong>Accessibility:</strong> 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.</p>
<p id="pdf-download-link" class="pdf-download" style="display: none;">
<a href="#" id="pdf-link" download>Download this page as PDF</a>
</p>
<script>
(function() {
// Determine PDF path based on current page URL
var path = window.location.pathname;
if (path === '/' || path === '') path = '/index.html';
// Convert .html to .pdf and prepend /pdfs/
var pdfPath = '/pdfs' + path.replace(/\.html$/, '.pdf');
// Check if PDF exists, then show link
var xhr = new XMLHttpRequest();
xhr.open('HEAD', pdfPath, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var container = document.getElementById('pdf-download-link');
var link = document.getElementById('pdf-link');
if (container && link) {
link.href = pdfPath;
container.style.display = 'block';
}
}
};
xhr.send();
})();
</script>
</div> </div>
</body> </body>
</html> </html>

View File

@ -65,3 +65,4 @@