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 <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
179faf0d2c
commit
8f26399741
@@ -38,7 +38,6 @@
|
|||||||
"marked": "^7.0.3",
|
"marked": "^7.0.3",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"nodemailer": "^6.9.5",
|
"nodemailer": "^6.9.5",
|
||||||
"puppeteer": "^24.10.0",
|
|
||||||
"zeptomail": "^6.0.0",
|
"zeptomail": "^6.0.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
@@ -46,7 +45,6 @@
|
|||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/puppeteer": "^7.0.4",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||||
"@typescript-eslint/parser": "^7.0.1",
|
"@typescript-eslint/parser": "^7.0.1",
|
||||||
"@vitest/coverage-v8": "^1.2.2",
|
"@vitest/coverage-v8": "^1.2.2",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TCertificateDownload } from '$src/types/course/lesson';
|
import type { TCertificateDownload } from '$src/types/course/lesson';
|
||||||
import { getPdfBuffer } from '$src/utils/puppeteer';
|
import { getCloudflarePdfBuffer } from '$src/utils/cloudflare';
|
||||||
|
|
||||||
export interface CertificateData {
|
export interface CertificateData {
|
||||||
theme: string;
|
theme: string;
|
||||||
@@ -17,81 +17,19 @@ export interface CertificateGenerationResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateCertificate = async (data: TCertificateDownload): Promise<Buffer> => {
|
export const generateCertificate = async (data: TCertificateDownload):Promise<Buffer> => {
|
||||||
const html = generateCertificateHtml(data);
|
const { html, styles } = generateCertificateHtml(data);
|
||||||
return getPdfBuffer(html);
|
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;
|
const { studentName, courseName, courseDescription, orgLogoUrl, orgName, facilitator } = data;
|
||||||
|
|
||||||
return `
|
const html = `
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Certificate of Completion</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="certificate">
|
<div class="certificate">
|
||||||
@@ -115,4 +53,72 @@ function generateCertificateHtml(data: TCertificateDownload): string {
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</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