PDF Generation

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.

Overview

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.

Key Features

  • HTML to PDF conversion with full CSS support
  • Custom page formatting (A4, Letter, Legal, custom sizes)
  • Headers and footers with dynamic content
  • Page numbering and metadata
  • Print-friendly styling with media queries
  • Base64 or binary output options

Basic PDF Generation

Simple HTML to PDF

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

Custom Page Settings

// 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`
};

Dynamic Content Generation

// 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`
};

PDF Styling Best Practices

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>
`;

Error Handling and Validation

Input Validation

// 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()
    };
}

Common PDF Generation Patterns

1. Multi-Page Documents

// 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>
    `;
}

2. Data-Driven Tables

// 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 || '';
    }
}

Performance Considerations

Optimizing Large Documents

// 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
    };
}

Legacy PDF Generation (V1)

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.

Simple HTML to PDF (V1)

<h1>Hello World</h1>

URL Input

You can also generate PDFs directly from URLs by providing the URL instead of HTML content.

Output

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.

Screenshots

Make.com PDF Generation V1
Make.com PDF Generation V1

Make.com PDF Generation V1 Configuration
Make.com PDF Generation V1 Configuration

Make.com PDF Generation V1 Output
Make.com PDF Generation V1 Output


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.