Use cases
Certificados
Gere certificados de conclusão, participação e reconhecimento automaticamente
Este guia mostra como implementar geração automática de certificados profissionais usando RenderHub.
Visão Geral
Certificados são perfeitos para:
- ✅ Plataformas de educação online (cursos, treinamentos)
- ✅ Eventos (conferências, workshops, webinars)
- ✅ Programas de reconhecimento (funcionário do mês, etc)
- ✅ Certificações profissionais
Estrutura de Dados
interface Certificate {
// Identificação
id: string;
certificateNumber: string; // CERT-2024-001
type: 'completion' | 'participation' | 'achievement' | 'recognition';
// Participante
recipient: {
name: string;
email: string;
documentId?: string; // CPF para validação
};
// Curso/Evento
course: {
name: string;
description?: string;
duration?: number; // horas
startDate?: Date;
endDate?: Date;
};
// Instituição
institution: {
name: string;
logo?: string;
website?: string;
};
// Assinaturas
signatures: Array<{
name: string;
title: string;
signature?: string; // imagem da assinatura
}>;
// Metadados
issueDate: Date;
validUntil?: Date;
score?: number; // 0-100
grade?: string; // A+, A, B, etc
validationUrl?: string; // URL para validar autenticidade
}
Template HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4 landscape;
margin: 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Georgia', serif;
width: 297mm;
height: 210mm;
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.certificate {
width: 275mm;
height: 190mm;
background: white;
border: 15px solid #f8f9fa;
box-shadow: 0 0 50px rgba(0,0,0,0.3);
position: relative;
padding: 40px 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.border-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 3px solid #667eea;
margin: 25px;
pointer-events: none;
}
.logo {
max-width: 150px;
margin-bottom: 20px;
}
.certificate-title {
font-size: 48px;
font-weight: bold;
color: #667eea;
text-transform: uppercase;
letter-spacing: 4px;
margin-bottom: 15px;
text-align: center;
}
.certificate-subtitle {
font-size: 18px;
color: #6b7280;
margin-bottom: 40px;
text-align: center;
}
.recipient-section {
text-align: center;
margin-bottom: 30px;
}
.awarded-text {
font-size: 16px;
color: #4b5563;
margin-bottom: 10px;
}
.recipient-name {
font-size: 42px;
font-weight: bold;
color: #1f2937;
border-bottom: 2px solid #667eea;
padding-bottom: 5px;
margin-bottom: 25px;
display: inline-block;
min-width: 400px;
text-align: center;
}
.course-info {
text-align: center;
max-width: 700px;
margin-bottom: 30px;
}
.completion-text {
font-size: 16px;
line-height: 1.8;
color: #4b5563;
margin-bottom: 10px;
}
.course-name {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin: 15px 0;
}
.course-details {
font-size: 14px;
color: #6b7280;
margin-top: 10px;
}
.performance {
background: #f0f9ff;
border: 2px solid #667eea;
border-radius: 8px;
padding: 15px 30px;
margin: 20px 0;
display: inline-block;
}
.performance-label {
font-size: 14px;
color: #4b5563;
}
.performance-value {
font-size: 28px;
font-weight: bold;
color: #667eea;
}
.footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: auto;
}
.date-section {
text-align: left;
}
.date-label {
font-size: 12px;
color: #9ca3af;
text-transform: uppercase;
}
.date-value {
font-size: 14px;
color: #4b5563;
margin-top: 5px;
}
.signatures {
display: flex;
gap: 60px;
justify-content: center;
}
.signature {
text-align: center;
min-width: 180px;
}
.signature-image {
max-width: 150px;
max-height: 60px;
margin-bottom: 10px;
}
.signature-line {
border-top: 2px solid #1f2937;
padding-top: 8px;
}
.signature-name {
font-size: 14px;
font-weight: bold;
color: #1f2937;
}
.signature-title {
font-size: 12px;
color: #6b7280;
margin-top: 2px;
}
.validation {
text-align: right;
font-size: 10px;
color: #9ca3af;
}
.certificate-number {
font-weight: bold;
color: #4b5563;
}
.qr-code {
width: 80px;
height: 80px;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="certificate">
<div class="border-decoration"></div>
{{#if institution.logo}}
<img src="{{institution.logo}}" alt="{{institution.name}}" class="logo">
{{/if}}
<div class="certificate-title">Certificado</div>
<div class="certificate-subtitle">{{typeLabel type}}</div>
<div class="recipient-section">
<div class="awarded-text">Certificamos que</div>
<div class="recipient-name">{{recipient.name}}</div>
</div>
<div class="course-info">
<div class="completion-text">
{{#if type "completion"}}
concluiu com êxito o curso
{{else if type "participation"}}
participou do evento
{{else}}
recebeu reconhecimento por
{{/if}}
</div>
<div class="course-name">{{course.name}}</div>
{{#if course.description}}
<div class="course-details">{{course.description}}</div>
{{/if}}
{{#if course.duration}}
<div class="course-details">
Carga horária: {{course.duration}} horas
{{#if course.startDate}}
| Período: {{formatDate course.startDate}} a {{formatDate course.endDate}}
{{/if}}
</div>
{{/if}}
</div>
{{#if score}}
<div class="performance">
<div class="performance-label">Nota Final</div>
<div class="performance-value">
{{score}}{{#if grade}} ({{grade}}){{/if}}
</div>
</div>
{{/if}}
<div class="footer">
<div class="date-section">
<div class="date-label">Data de Emissão</div>
<div class="date-value">{{formatDate issueDate}}</div>
{{#if validUntil}}
<div class="date-label" style="margin-top: 10px;">Válido até</div>
<div class="date-value">{{formatDate validUntil}}</div>
{{/if}}
</div>
<div class="signatures">
{{#each signatures}}
<div class="signature">
{{#if signature}}
<img src="{{signature}}" alt="Assinatura" class="signature-image">
{{/if}}
<div class="signature-line">
<div class="signature-name">{{name}}</div>
<div class="signature-title">{{title}}</div>
</div>
</div>
{{/each}}
</div>
<div class="validation">
<div class="certificate-number">Nº {{certificateNumber}}</div>
{{#if validationUrl}}
<div style="margin-top: 8px;">Valide em:</div>
<div>{{validationUrl}}</div>
{{#if qrCode}}
<img src="{{qrCode}}" alt="QR Code" class="qr-code">
{{/if}}
{{/if}}
</div>
</div>
</div>
</body>
</html>
Implementação
1. Serviço de Certificados
// src/services/certificate.service.ts
import { RenderHubClient } from '@renderhub/sdk';
import QRCode from 'qrcode';
import { Certificate } from '../models/Certificate';
export class CertificateService {
private renderHub: RenderHubClient;
constructor() {
this.renderHub = new RenderHubClient({
apiKey: process.env.RENDERHUB_API_KEY!,
});
}
async generateCertificate(certificate: Certificate): Promise<Buffer> {
// 1. Gerar QR Code para validação
const qrCodeDataUrl = await this.generateQRCode(certificate.validationUrl);
// 2. Preparar dados
const templateData = {
...certificate,
issueDate: certificate.issueDate.toLocaleDateString('pt-BR'),
validUntil: certificate.validUntil?.toLocaleDateString('pt-BR'),
qrCode: qrCodeDataUrl,
};
// 3. Gerar PDF
const pdf = await this.renderHub.generatePDF(
'certificate-v1',
templateData
);
return pdf;
}
private async generateQRCode(url?: string): Promise<string | undefined> {
if (!url) return undefined;
return await QRCode.toDataURL(url, {
width: 200,
margin: 1,
});
}
async issueOnCompletion(userId: string, courseId: string): Promise<Certificate> {
// 1. Verificar se completou o curso
const progress = await this.getCourseProgress(userId, courseId);
if (progress.completionPercentage < 100) {
throw new Error('Curso não completado');
}
// 2. Criar certificado
const user = await this.getUser(userId);
const course = await this.getCourse(courseId);
const certificateNumber = await this.generateCertificateNumber();
const validationUrl = `${process.env.APP_URL}/validate/${certificateNumber}`;
const certificate: Certificate = {
id: crypto.randomUUID(),
certificateNumber,
type: 'completion',
recipient: {
name: user.name,
email: user.email,
documentId: user.cpf,
},
course: {
name: course.name,
description: course.description,
duration: course.durationHours,
startDate: progress.startedAt,
endDate: progress.completedAt!,
},
institution: {
name: 'Minha Plataforma',
logo: 'https://cdn.example.com/logo.png',
website: 'https://example.com',
},
signatures: [
{
name: 'João Silva',
title: 'Diretor de Educação',
signature: 'https://cdn.example.com/signature-joao.png',
},
],
issueDate: new Date(),
score: progress.finalScore,
grade: this.calculateGrade(progress.finalScore),
validationUrl,
};
// 3. Salvar no banco
await this.saveCertificate(certificate);
// 4. Gerar PDF
const pdf = await this.generateCertificate(certificate);
// 5. Upload para S3
await this.uploadCertificate(certificate.id, pdf);
// 6. Enviar email
await this.sendCertificateEmail(certificate, pdf);
return certificate;
}
private calculateGrade(score: number): string {
if (score >= 95) return 'A+';
if (score >= 90) return 'A';
if (score >= 85) return 'B+';
if (score >= 80) return 'B';
if (score >= 75) return 'C+';
if (score >= 70) return 'C';
return 'D';
}
private async generateCertificateNumber(): Promise<string> {
const year = new Date().getFullYear();
const count = await this.getCertificateCount(year);
return `CERT-${year}-${String(count + 1).padStart(6, '0')}`;
}
// Métodos auxiliares (implementar conforme seu banco de dados)
private async getCourseProgress(userId: string, courseId: string): Promise<any> {
throw new Error('Not implemented');
}
private async getUser(userId: string): Promise<any> {
throw new Error('Not implemented');
}
private async getCourse(courseId: string): Promise<any> {
throw new Error('Not implemented');
}
private async saveCertificate(certificate: Certificate): Promise<void> {
throw new Error('Not implemented');
}
private async uploadCertificate(id: string, pdf: Buffer): Promise<void> {
throw new Error('Not implemented');
}
private async sendCertificateEmail(certificate: Certificate, pdf: Buffer): Promise<void> {
throw new Error('Not implemented');
}
private async getCertificateCount(year: number): Promise<number> {
throw new Error('Not implemented');
}
}
2. Endpoint de Validação
// src/routes/certificates.routes.ts
import { Router } from 'express';
import { CertificateService } from '../services/certificate.service';
const router = Router();
const certificateService = new CertificateService();
// Validar autenticidade de certificado
router.get('/validate/:number', async (req, res) => {
try {
const certificate = await db.certificates.findOne({
where: { certificateNumber: req.params.number },
});
if (!certificate) {
return res.status(404).render('validation-not-found');
}
res.render('validation-success', { certificate });
} catch (error) {
console.error('Error validating certificate:', error);
res.status(500).render('validation-error');
}
});
// Download de certificado
router.get('/certificates/:id/download', async (req, res) => {
try {
const certificate = await db.certificates.findByPk(req.params.id);
if (!certificate) {
return res.status(404).json({ error: 'Certificado não encontrado' });
}
const pdf = await certificateService.generateCertificate(certificate);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader(
'Content-Disposition',
`attachment; filename="Certificado-${certificate.certificateNumber}.pdf"`
);
res.send(pdf);
} catch (error) {
console.error('Error downloading certificate:', error);
res.status(500).json({ error: 'Erro ao baixar certificado' });
}
});
export default router;
3. Webhook de Conclusão
// src/webhooks/course.webhook.ts
import { CertificateService } from '../services/certificate.service';
const certificateService = new CertificateService();
export async function handleCourseCompleted(data: {
userId: string;
courseId: string;
}) {
try {
// Emitir certificado automaticamente
const certificate = await certificateService.issueOnCompletion(
data.userId,
data.courseId
);
console.log(`Certificate issued: ${certificate.certificateNumber}`);
} catch (error) {
console.error('Error issuing certificate:', error);
}
}
Variações de Certificados
Certificado de Participação em Evento
const eventCertificate: Certificate = {
type: 'participation',
course: {
name: 'DevConf Brasil 2024',
description: 'Conferência de Desenvolvimento de Software',
startDate: new Date('2024-11-15'),
endDate: new Date('2024-11-17'),
},
// ... outros campos
};
Certificado de Reconhecimento
const recognitionCertificate: Certificate = {
type: 'recognition',
course: {
name: 'Funcionário do Mês - Novembro 2024',
description: 'Reconhecimento por excelência em atendimento ao cliente',
},
// ... outros campos
};
Proteção Contra Fraude
Blockchain/Hash
import crypto from 'crypto';
class CertificateBlockchain {
async registerCertificate(certificate: Certificate): Promise<string> {
// Criar hash único do certificado
const hash = crypto
.createHash('sha256')
.update(JSON.stringify({
number: certificate.certificateNumber,
recipient: certificate.recipient.name,
course: certificate.course.name,
issueDate: certificate.issueDate,
}))
.digest('hex');
// Armazenar hash em blockchain ou banco imutável
await this.storeHash(hash, certificate.id);
return hash;
}
async verifyCertificate(certificateNumber: string): Promise<boolean> {
const certificate = await db.certificates.findOne({
where: { certificateNumber },
});
if (!certificate) return false;
const storedHash = await this.getStoredHash(certificate.id);
const calculatedHash = await this.calculateHash(certificate);
return storedHash === calculatedHash;
}
private async storeHash(hash: string, certificateId: string): Promise<void> {
// Implementar storage (banco, blockchain, etc)
}
private async getStoredHash(certificateId: string): Promise<string> {
// Implementar retrieval
throw new Error('Not implemented');
}
private async calculateHash(certificate: any): Promise<string> {
// Implementar cálculo de hash
throw new Error('Not implemented');
}
}
Próximos Passos
- Veja como gerar Relatórios
- Confira exemplos de Contratos
- Volte para Faturas