Skip to content

Instantly share code, notes, and snippets.

@SippieCup
Last active December 31, 2025 23:41
Show Gist options
  • Select an option

  • Save SippieCup/78edd78f8229882171061cc486755cce to your computer and use it in GitHub Desktop.

Select an option

Save SippieCup/78edd78f8229882171061cc486755cce to your computer and use it in GitHub Desktop.
Handlebars pdf generation
import fs from 'fs';
import * as path from 'path';
import puppeteer from 'puppeteer';
import handlebars from 'handlebars';
import { CreatePdfRequest } from './types';
import { MILLISECONDS_IN_MINUTE } from '../../timeUtils';
import { PartialTemplate } from '../../../handlebars/types';
/**
* @returns the created pdf as a buffer
*/
export const createPdf = async (createPdfRequest: CreatePdfRequest): Promise<Buffer> => {
const { templatePath, cssPath, templateData, partials, margin, headerTemplate, footerTemplate, width, height, landscape } = createPdfRequest;
// Resolve all paths; when running tests we may not have built assets in dist/, so fall back to src/ equivalents.
const resolveCandidatePath = (p: string): string => {
const normalized = path.normalize(p);
if (fs.existsSync(normalized)) return normalized;
// try swapping dist/handlebars -> src/handlebars
const alt = normalized.replace(path.normalize('dist/handlebars'), path.normalize('src/handlebars'));
return fs.existsSync(alt) ? alt : normalized;
};
const resolvedTemplatePath = resolveCandidatePath(templatePath);
const resolvedCssPath = cssPath ? resolveCandidatePath(cssPath) : null;
const resolvedPartials = (partials ?? []).map((p) => ({
...p,
templatePath: resolveCandidatePath(p.templatePath),
cssPath: p.cssPath ? resolveCandidatePath(p.cssPath) : undefined,
}));
const pathsToCheck: string[] = getFilePathsToCheck(resolvedTemplatePath, resolvedCssPath ?? null, resolvedPartials as any);
const paths = pathsToCheck.map((filepath) => path.normalize(filepath));
console.log(JSON.stringify(pathsToCheck, null, 2));
// pwd
console.log(`Current directory: ${process.cwd()}`);
for (const filepath of paths) {
if (!fs.existsSync(filepath)) {
throw new Error(`File ${filepath} does not exist`);
}
}
// register partials
for (const partial of resolvedPartials ?? []) {
const partialTemplate = fs.readFileSync(path.normalize(partial.templatePath), 'utf8');
handlebars.registerPartial(partial.partialName, partialTemplate);
}
const template = fs.readFileSync(path.normalize(resolvedTemplatePath), 'utf8');
const compiledTemplate = handlebars.compile(template);
const htmlResult: string = compiledTemplate(templateData, {
allowProtoPropertiesByDefault: true,
});
const browser = await puppeteer.launch({
headless: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH ?? undefined,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
try {
const page = await browser.newPage();
await page.setContent(htmlResult, {
waitUntil: ['load'],
});
// add css styles from given files
if (resolvedCssPath) await page.addStyleTag({ path: resolvedCssPath });
await page.addStyleTag({
content: '@page { size: auto; }',
});
for (const partial of resolvedPartials ?? []) {
if (partial.cssPath) {
await page.addStyleTag({ path: partial.cssPath });
}
}
const pdfBuffer = await page.pdf({
headerTemplate,
footerTemplate,
displayHeaderFooter: headerTemplate || footerTemplate ? true : false,
printBackground: true,
timeout: MILLISECONDS_IN_MINUTE * 5, // timeout after 5 minutes
margin,
width,
height,
landscape,
});
return Buffer.from(pdfBuffer);
} finally {
await browser.close();
}
};
const getFilePathsToCheck = (mainTemplatePath: string, mainCssPath: string | null, partials: PartialTemplate[]) => {
const paths = [mainTemplatePath];
if (mainCssPath) paths.push(mainCssPath);
for (const partial of partials ?? []) {
paths.push(partial.templatePath);
if (partial.cssPath) paths.push(partial.cssPath);
}
return paths;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment