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

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