Add PDF generation to Docker build process
ci/woodpecker/push/woodpecker Pipeline failed Details

- Update Dockerfile to install npm and run PDF generation during build
- Move generate-pdfs.js and package.json to docker/resume/ for Docker context
- Update generate-pdfs.js to work from /srv directory in container
- PDFs are now generated automatically during Docker build before deployment
- PDFs will be available at /pdfs/ path matching HTML file structure
This commit is contained in:
Colin 2025-11-30 16:11:30 -05:00
parent 83c0ae74f7
commit 19182d6d21
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
4 changed files with 247 additions and 20 deletions

View File

@ -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

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 = __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);
});

View File

@ -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"
}
}

View File

@ -76,5 +76,10 @@ else
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"