Add PDF generation for static site pages
ci/woodpecker/push/woodpecker Pipeline was successful
Details
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:
parent
73b83fc3fd
commit
83c0ae74f7
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,4 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue