feat: cloudflare browser rendering for pdf generation (#562)

* feat: cloudflare browser rendering for pdf generation

* feat: sync env

* fix: imports correction & remove puppeteer

* update return type

* update dependencies

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2025-06-23 08:55:34 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 179faf0d2c
commit 8f26399741
5 changed files with 3486 additions and 4932 deletions
-2
View File
@@ -38,7 +38,6 @@
"marked": "^7.0.3",
"nanoid": "^5.1.5",
"nodemailer": "^6.9.5",
"puppeteer": "^24.10.0",
"zeptomail": "^6.0.0",
"zod": "^3.21.4"
},
@@ -46,7 +45,6 @@
"@types/katex": "^0.16.7",
"@types/node": "^20.11.19",
"@types/nodemailer": "^6.4.17",
"@types/puppeteer": "^7.0.4",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"@vitest/coverage-v8": "^1.2.2",
+74 -68
View File
@@ -1,5 +1,5 @@
import type { TCertificateDownload } from '$src/types/course/lesson';
import { getPdfBuffer } from '$src/utils/puppeteer';
import { getCloudflarePdfBuffer } from '$src/utils/cloudflare';
export interface CertificateData {
theme: string;
@@ -17,81 +17,19 @@ export interface CertificateGenerationResult {
error?: string;
}
export const generateCertificate = async (data: TCertificateDownload): Promise<Buffer> => {
const html = generateCertificateHtml(data);
return getPdfBuffer(html);
export const generateCertificate = async (data: TCertificateDownload):Promise<Buffer> => {
const { html, styles } = generateCertificateHtml(data);
return getCloudflarePdfBuffer(html, styles);
};
function generateCertificateHtml(data: TCertificateDownload): string {
function generateCertificateHtml(data: TCertificateDownload): { html: string; styles: string } {
const { studentName, courseName, courseDescription, orgLogoUrl, orgName, facilitator } = data;
return `
<!DOCTYPE html>
const html = `
<html>
<head>
<meta charset="utf-8">
<title>Certificate of Completion</title>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.certificate {
width: 1123px;
height: 794px;
margin: 0 auto;
background-color: white;
position: relative;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.header {
text-align: center;
padding: 20px;
}
.logo {
max-width: 200px;
margin-bottom: 20px;
}
.title {
font-size: 36px;
color: #333;
margin-bottom: 10px;
}
.content {
text-align: center;
padding: 40px;
}
.student-name {
font-size: 48px;
color: #2c3e50;
margin-bottom: 20px;
}
.course-name {
font-size: 24px;
color: #34495e;
margin-bottom: 10px;
}
.description {
font-size: 18px;
color: #7f8c8d;
margin-bottom: 30px;
}
.footer {
position: absolute;
bottom: 40px;
width: 100%;
text-align: center;
}
.signature {
margin-top: 20px;
}
.date {
margin-top: 10px;
color: #95a5a6;
}
</style>
</head>
<body>
<div class="certificate">
@@ -115,4 +53,72 @@ function generateCertificateHtml(data: TCertificateDownload): string {
</body>
</html>
`;
const styles = `
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.certificate {
width: 1123px;
height: 794px;
margin: 0 auto;
background-color: white;
position: relative;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.header {
text-align: center;
padding: 20px;
}
.logo {
max-width: 200px;
margin-bottom: 20px;
}
.title {
font-size: 36px;
color: #333;
margin-bottom: 10px;
}
.content {
text-align: center;
padding: 40px;
}
.student-name {
font-size: 48px;
color: #2c3e50;
margin-bottom: 20px;
}
.course-name {
font-size: 24px;
color: #34495e;
margin-bottom: 10px;
}
.description {
font-size: 18px;
color: #7f8c8d;
margin-bottom: 30px;
}
.footer {
position: absolute;
bottom: 40px;
width: 100%;
text-align: center;
}
.signature {
margin-top: 20px;
}
.date {
margin-top: 10px;
color: #95a5a6;
}
`;
const pdf = {
html: html,
styles: styles
};
return pdf;
}
+27
View File
@@ -0,0 +1,27 @@
import { CLOUDFLARE } from '$src/constants';
export const getCloudflarePdfBuffer = async (html: string, styles?: string) => {
console.log('Generating PDF with Cloudflare API...');
try {
const pdfResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE.CONFIGS.ACCOUNT_ID}/browser-rendering/pdf`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${CLOUDFLARE.CONFIGS.ACCESS_KEY}`
},
body: JSON.stringify({
html: html,
addStyleTag: [{ content: `${styles}` }]
})
}
);
console.log('PDF response status:', pdfResponse.status);
return pdfResponse;
} catch (error) {
console.error('Error generating PDF:', error);
throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF');
}
};
-75
View File
@@ -1,75 +0,0 @@
import puppeteer, { type PDFOptions } from 'puppeteer';
export interface PdfParams extends Partial<PDFOptions> {
width?: string;
height?: string;
format?:
| 'Letter'
| 'Legal'
| 'Tabloid'
| 'Ledger'
| 'A0'
| 'A1'
| 'A2'
| 'A3'
| 'A4'
| 'A5'
| 'A6';
landscape?: boolean;
printBackground?: boolean;
margin?: {
top?: string;
right?: string;
bottom?: string;
left?: string;
};
}
export interface PdfGenerationResult {
success: boolean;
buffer?: Buffer;
error?: string;
}
export const getPdfBuffer = async (html: string, pdfParams?: PdfParams) => {
const args = ['--disable-web-security', '--no-sandbox', '--disable-setuid-sandbox'];
const defaultPdfParams: PdfParams = {
width: '1123px',
height: '794px',
format: 'A4',
printBackground: true,
margin: {
top: '0',
right: '0',
bottom: '0',
left: '0'
}
};
try {
// launch a new chrome instance
const browser = await puppeteer.launch({
headless: true,
args
});
// create a new page
const page = await browser.newPage();
await page.setViewport({ width: 1123, height: 794 });
await page.goto('data:text/html,' + html, { waitUntil: 'networkidle2' });
await page.addStyleTag({ content: '@page { size: auto; }' });
await page.emulateMediaType('screen');
// create a pdf buffer
const pdfBuffer = await page.pdf(pdfParams || defaultPdfParams);
// close the browser
await browser.close();
return Buffer.from(pdfBuffer);
} catch (error) {
console.error('Error generating PDF:', error);
throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF');
}
};
+3385 -4787
View File
File diff suppressed because it is too large Load Diff