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:
co-authored by
Rotimi Best
parent
179faf0d2c
commit
8f26399741
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Generated
+3385
-4787
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user