CustomJS provides powerful PDF generation capabilities that allow you to create professional PDF documents directly from your Make.com workflows. You can generate PDFs from HTML content, templates, or dynamic data.
The PDF generation module uses Chromium-based rendering to convert HTML content into high-quality PDF documents. This ensures that your PDFs maintain consistent formatting, support modern CSS features, and handle complex layouts.
// Basic PDF generation from HTML string
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Invoice</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { text-align: center; margin-bottom: 30px; }
.invoice-details { margin-bottom: 20px; }
.items-table { width: 100%; border-collapse: collapse; }
.items-table th, .items-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.items-table th { background-color: #f2f2f2; }
.total { text-align: right; margin-top: 20px; font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
<p>Invoice #${input.invoiceNumber}</p>
<p>Date: ${new Date().toLocaleDateString()}</p>
</div>
<div class="invoice-details">
<p><strong>Bill To:</strong></p>
<p>${input.customer.name}</p>
<p>${input.customer.address}</p>
<p>${input.customer.email}</p>
</div>
<table class="items-table">
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
${input.items.map(item => `
<tr>
<td>${item.description}</td>
<td>${item.quantity}</td>
<td>$${item.price.toFixed(2)}</td>
<td>$${(item.quantity * item.price).toFixed(2)}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="total">
<p>Total: $${input.items.reduce((sum, item) => sum + (item.quantity * item.price), 0).toFixed(2)}</p>
</div>
</body>
</html>
`;
// Generate PDF
const pdfOptions = {
format: 'A4',
margin: {
top: '20mm',
right: '20mm',
bottom: '20mm',
left: '20mm'
},
printBackground: true
};
// Return the HTML content and options for PDF generation
return {
html: htmlContent,
options: pdfOptions,
filename: `invoice-${input.invoiceNumber}.pdf`
};
// Advanced PDF configuration
const pdfConfig = {
// Page format
format: input.pageFormat || 'A4', // A4, A3, A5, Letter, Legal, Tabloid
// Custom dimensions (if not using standard format)
width: input.customWidth || null, // e.g., '210mm'
height: input.customHeight || null, // e.g., '297mm'
// Margins
margin: {
top: input.marginTop || '20mm',
right: input.marginRight || '20mm',
bottom: input.marginBottom || '20mm',
left: input.marginLeft || '20mm'
},
// Print options
printBackground: true, // Include background colors and images
landscape: input.landscape || false, // Portrait or landscape
// Headers and footers
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; margin: 0 20mm;">
<span>${input.company.name} - ${input.document.title}</span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center; margin: 0 20mm;">
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>
`,
// Quality and performance
preferCSSPageSize: false, // Use CSS page size or format setting
scale: 1.0 // Scale factor (0.1 to 2.0)
};
return {
html: generateHTMLContent(input),
options: pdfConfig,
filename: `${input.document.type}-${Date.now()}.pdf`
};
// Generate dynamic PDF content based on template type
function generatePDFContent(data) {
const { type, content } = data;
switch (type) {
case 'report':
return generateReportPDF(content);
case 'certificate':
return generateCertificatePDF(content);
case 'invoice':
return generateInvoicePDF(content);
case 'contract':
return generateContractPDF(content);
default:
return generateGenericPDF(content);
}
}
function generateReportPDF(data) {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${data.title}</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
margin: -20px -20px 30px -20px;
}
.section { margin-bottom: 30px; }
.chart-placeholder {
background: #f8f9fa;
border: 2px dashed #dee2e6;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.data-table th, .data-table td {
border: 1px solid #dee2e6;
padding: 12px;
text-align: left;
}
.data-table th {
background-color: #f8f9fa;
font-weight: 600;
}
.summary-box {
background: #f8f9fa;
border-left: 4px solid #007bff;
padding: 20px;
margin: 20px 0;
}
@media print {
.page-break { page-break-before: always; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${data.title}</h1>
<p>Generated on ${new Date().toLocaleDateString()}</p>
<p>Report Period: ${data.period}</p>
</div>
<div class="section">
<h2>Executive Summary</h2>
<div class="summary-box">
<p>${data.summary}</p>
</div>
</div>
<div class="section">
<h2>Key Metrics</h2>
<table class="data-table">
<thead>
<tr>
<th>Metric</th>
<th>Current Period</th>
<th>Previous Period</th>
<th>Change</th>
</tr>
</thead>
<tbody>
${data.metrics.map(metric => `
<tr>
<td>${metric.name}</td>
<td>${metric.current}</td>
<td>${metric.previous}</td>
<td style="color: ${metric.change >= 0 ? 'green' : 'red'}">
${metric.change >= 0 ? '+' : ''}${metric.change}%
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="page-break"></div>
<div class="section">
<h2>Detailed Analysis</h2>
${data.sections.map(section => `
<h3>${section.title}</h3>
<p>${section.content}</p>
${section.hasChart ? '<div class="chart-placeholder">Chart: ' + section.title + '</div>' : ''}
`).join('')}
</div>
</div>
</body>
</html>
`;
return htmlContent;
}
// Main function
const pdfContent = generatePDFContent(input);
return {
html: pdfContent,
options: {
format: 'A4',
margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div></div>', // Empty header
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`
},
filename: `${input.type}-report-${new Date().toISOString().split('T')[0]}.pdf`
};
const printOptimizedCSS = `
<style>
/* Base styles */
* { box-sizing: border-box; }
body {
font-family: 'Arial', sans-serif;
font-size: 12pt;
line-height: 1.4;
color: #000;
margin: 0;
padding: 0;
}
/* Print-specific styles */
@media print {
/* Page breaks */
.page-break-before { page-break-before: always; }
.page-break-after { page-break-after: always; }
.page-break-inside { page-break-inside: avoid; }
/* Avoid breaking these elements */
h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
table, figure, img { page-break-inside: avoid; }
/* Hide elements not needed in print */
.no-print { display: none !important; }
/* Ensure backgrounds print */
* { -webkit-print-color-adjust: exact !important; }
}
/* Typography */
h1 { font-size: 24pt; margin: 0 0 16pt 0; }
h2 { font-size: 18pt; margin: 16pt 0 12pt 0; }
h3 { font-size: 14pt; margin: 12pt 0 8pt 0; }
p { margin: 0 0 8pt 0; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 12pt 0;
}
th, td {
border: 1pt solid #000;
padding: 6pt;
text-align: left;
vertical-align: top;
}
th {
background-color: #f0f0f0;
font-weight: bold;
}
/* Layout helpers */
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-bold { font-weight: bold; }
.mb-small { margin-bottom: 8pt; }
.mb-medium { margin-bottom: 16pt; }
.mb-large { margin-bottom: 24pt; }
</style>
`;
// Use in your HTML template
const htmlWithOptimizedCSS = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${input.title}</title>
${printOptimizedCSS}
</head>
<body>
<!-- Your content here -->
</body>
</html>
`;
// Validate PDF generation inputs
function validatePDFInputs(input) {
const errors = [];
// Check required fields
if (!input.content && !input.html) {
errors.push('Either content or html must be provided');
}
// Validate page format
const validFormats = ['A4', 'A3', 'A5', 'Letter', 'Legal', 'Tabloid'];
if (input.format && !validFormats.includes(input.format)) {
errors.push(`Invalid format. Must be one of: ${validFormats.join(', ')}`);
}
// Validate margins
if (input.margin) {
const marginPattern = /^\d+(\.\d+)?(mm|cm|in|px)$/;
['top', 'right', 'bottom', 'left'].forEach(side => {
if (input.margin[side] && !marginPattern.test(input.margin[side])) {
errors.push(`Invalid ${side} margin format. Use format like '20mm', '1in', etc.`);
}
});
}
// Validate scale
if (input.scale && (input.scale < 0.1 || input.scale > 2.0)) {
errors.push('Scale must be between 0.1 and 2.0');
}
return errors;
}
// Main PDF generation with validation
const validationErrors = validatePDFInputs(input);
if (validationErrors.length > 0) {
return {
error: 'Validation failed',
details: validationErrors,
timestamp: new Date().toISOString()
};
}
try {
const htmlContent = generateHTMLContent(input);
const pdfOptions = buildPDFOptions(input);
return {
success: true,
html: htmlContent,
options: pdfOptions,
filename: input.filename || `document-${Date.now()}.pdf`,
generatedAt: new Date().toISOString()
};
} catch (error) {
return {
error: 'PDF generation failed',
details: error.message,
timestamp: new Date().toISOString()
};
}
// Generate multi-page document with sections
function generateMultiPageDocument(data) {
const sections = data.sections || [];
const sectionHTML = sections.map((section, index) => `
<div class="section ${index > 0 ? 'page-break-before' : ''}">
<h2>${section.title}</h2>
<div class="section-content">
${section.content}
</div>
${section.data ? generateSectionData(section.data) : ''}
</div>
`).join('');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${data.title}</title>
<style>
/* Your styles here */
.page-break-before { page-break-before: always; }
</style>
</head>
<body>
<div class="document">
<div class="cover-page">
<h1>${data.title}</h1>
<p>Generated: ${new Date().toLocaleDateString()}</p>
</div>
${sectionHTML}
</div>
</body>
</html>
`;
}
// Generate tables from dynamic data
function generateDataTable(data, config = {}) {
const { columns, rows, title } = data;
const { striped = true, bordered = true } = config;
const tableClass = [
'data-table',
striped ? 'striped' : '',
bordered ? 'bordered' : ''
].filter(Boolean).join(' ');
return `
${title ? `<h3>${title}</h3>` : ''}
<table class="${tableClass}">
<thead>
<tr>
${columns.map(col => `<th>${col.label}</th>`).join('')}
</tr>
</thead>
<tbody>
${rows.map(row => `
<tr>
${columns.map(col => `
<td>${formatCellValue(row[col.key], col.type)}</td>
`).join('')}
</tr>
`).join('')}
</tbody>
</table>
`;
}
function formatCellValue(value, type) {
switch (type) {
case 'currency':
return `$${parseFloat(value || 0).toFixed(2)}`;
case 'percentage':
return `${parseFloat(value || 0).toFixed(1)}%`;
case 'date':
return new Date(value).toLocaleDateString();
default:
return value || '';
}
}
// Optimize PDF generation for large documents
function optimizePDFGeneration(input) {
const { content, options = {} } = input;
// Limit content size
const maxContentLength = 1000000; // 1MB
if (content.length > maxContentLength) {
return {
error: 'Content too large',
maxSize: maxContentLength,
currentSize: content.length
};
}
// Optimize options for performance
const optimizedOptions = {
...options,
// Reduce quality for faster generation
scale: Math.min(options.scale || 1.0, 1.0),
// Disable expensive features if not needed
printBackground: options.printBackground !== false,
// Use efficient page format
format: options.format || 'A4'
};
return {
html: content,
options: optimizedOptions,
optimized: true
};
}
We also provide a legacy PDF generation function in our CustomJS Make.com module that generates PDF files from an HTML string or URL. This version uses a real headless Chromium browser in the background and is ideal for automating the conversion of web pages or document templates into high-quality PDFs, streamlining Make.com workflows that require document generation.
<h1>Hello World</h1>
You can also generate PDFs directly from URLs by providing the URL instead of HTML content.
The V1 module returns a binary file buffer that can be directly used with other Make.com modules like Google Drive, Dropbox, or email attachments.
You can directly access input field values using the 'input' JavaScript variable, or construct JSON objects for complex cases, as detailed in our JSON Parameter guide.
PDF generation with CustomJS provides a powerful way to create professional documents directly from your Make.com workflows, enabling automated report generation, invoice creation, and document processing at scale.