Certificates of completion, achievement awards, and participation diplomas are essential for online courses, workshops, conferences, and corporate training programs. Instead of designing each certificate manually in a tool like Canva, you can build a web application that generates them automatically from a template. In this tutorial, we will build a React-based certificate generator that produces professional PDF certificates using TongoRender.
What We Will Build
Our application will have three parts:
- A certificate template designed with HTML and CSS
- A React form for entering recipient details
- An API route that renders the certificate as a PDF
Step 1: Design the Certificate Template
Certificates need to look elegant. We use landscape A4 orientation with a decorative border, script fonts, and a formal layout:
// templates/certificate.js
export function certificateTemplate({ recipientName, courseName, completionDate, instructorName, certificateId }) {
return `
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&family=Montserrat:wght@300;400;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: 297mm; height: 210mm; font-family: 'Montserrat', sans-serif; }
.certificate {
width: 100%; height: 100%;
border: 3px solid #1a365d;
padding: 15mm;
position: relative;
background: linear-gradient(135deg, #fefefe 0%, #f7fafc 100%);
}
.inner-border {
width: 100%; height: 100%;
border: 1px solid #c9a84c;
padding: 15mm;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
}
.title { font-family: 'Great Vibes', cursive; font-size: 48pt; color: #1a365d; }
.subtitle { font-size: 14pt; color: #4a5568; letter-spacing: 4px; text-transform: uppercase; margin: 10mm 0 5mm; }
.recipient { font-family: 'Great Vibes', cursive; font-size: 36pt; color: #c9a84c; margin: 5mm 0; }
.description { font-size: 12pt; color: #4a5568; max-width: 500px; line-height: 1.6; margin: 5mm 0; }
.signatures { display: flex; justify-content: space-between; width: 80%; margin-top: 15mm; }
.signature-block { text-align: center; }
.signature-line { width: 150px; border-top: 1px solid #1a365d; margin-bottom: 5px; }
.cert-id { position: absolute; bottom: 10mm; right: 15mm; font-size: 8pt; color: #a0aec0; }
</style>
</head>
<body>
<div class="certificate">
<div class="inner-border">
<div class="title">Certificate of Completion</div>
<div class="subtitle">This is proudly presented to</div>
<div class="recipient">${recipientName}</div>
<div class="description">
For successfully completing the course
<strong>${courseName}</strong>
on ${completionDate}.
</div>
<div class="signatures">
<div class="signature-block">
<div class="signature-line"></div>
<div>${instructorName}</div>
<div style="color:#718096;font-size:10pt">Instructor</div>
</div>
<div class="signature-block">
<div class="signature-line"></div>
<div>Academy Director</div>
<div style="color:#718096;font-size:10pt">Authorized Signatory</div>
</div>
</div>
</div>
<div class="cert-id">Certificate ID: ${certificateId}</div>
</div>
</body>
</html>`;
}
Step 2: Build the React Form
Create a form component that collects the required information:
import { useState } from 'react';
export default function CertificateForm() {
const [formData, setFormData] = useState({
recipientName: '', courseName: '', completionDate: '', instructorName: '',
});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
const response = await fetch('/api/generate-certificate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `certificate-${formData.recipientName.replace(/\s+/g, '-')}.pdf`;
a.click();
URL.revokeObjectURL(url);
setLoading(false);
};
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto">
<input placeholder="Recipient Name" required
value={formData.recipientName}
onChange={e => setFormData({...formData, recipientName: e.target.value})} />
<input placeholder="Course Name" required
value={formData.courseName}
onChange={e => setFormData({...formData, courseName: e.target.value})} />
<input type="date" required
value={formData.completionDate}
onChange={e => setFormData({...formData, completionDate: e.target.value})} />
<input placeholder="Instructor Name" required
value={formData.instructorName}
onChange={e => setFormData({...formData, instructorName: e.target.value})} />
<button type="submit" disabled={loading}>
{loading ? 'Generating...' : 'Generate Certificate'}
</button>
</form>
);
}
Step 3: Create the API Route
In your Next.js API route (or Express endpoint), render the certificate HTML as a landscape PDF:
// pages/api/generate-certificate.js (Next.js)
import { certificateTemplate } from '../../templates/certificate';
import { randomUUID } from 'crypto';
export default async function handler(req, res) {
const { recipientName, courseName, completionDate, instructorName } = req.body;
const certificateId = randomUUID().split('-')[0].toUpperCase();
const html = certificateTemplate({
recipientName, courseName, completionDate, instructorName, certificateId,
});
const pdfResponse = await fetch('https://api.tongorender.io/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.TONGORENDER_API_KEY,
},
body: JSON.stringify({
html,
format: 'A4',
landscape: true,
margin: { top: '0mm', bottom: '0mm', left: '0mm', right: '0mm' },
printBackground: true,
}),
});
const pdfBuffer = Buffer.from(await pdfResponse.arrayBuffer());
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="certificate-${certificateId}.pdf"`);
res.send(pdfBuffer);
}
Step 4: Batch Generation
For courses with dozens or hundreds of students, generate certificates in batch from a CSV file:
const csv = require('csv-parse/sync');
const fs = require('fs');
async function batchGenerate(csvPath) {
const records = csv.parse(fs.readFileSync(csvPath), { columns: true });
for (const record of records) {
const pdf = await generateCertificate(record);
fs.writeFileSync(`certificates/${record.name.replace(/\s+/g, '-')}.pdf`, pdf);
console.log(`Generated certificate for ${record.name}`);
}
}
Design Tips for Certificates
- Use decorative fonts sparingly — Script fonts like Great Vibes work for names and titles, but body text should be clean and readable.
- Include a unique ID — Makes certificates verifiable. You can link the ID to a verification page on your site.
- Print background colors — Set
printBackground: truein the API options. Without this, background gradients and colors will be invisible in the PDF. - Test with long names — Some recipients have very long names. Use responsive font sizing or text truncation.
- Add a QR code — Link it to a verification URL for added authenticity.
TongoRender renders web fonts, CSS gradients, and complex layouts faithfully, making it ideal for certificate generation. The zero-margin landscape mode gives you full control over the page design.
Build your certificate generator with TongoRender — 100 free renders per month, no credit card required.