Learn how to build a certificate generator using Replit. Follow this step-by-step guide to create and share custom certificates easily online.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
You can build a simple and fully working certificate generator on Replit by combining a small Flask (Python) backend with an HTML frontend form, and a Pillow image processing script that takes a base certificate image and dynamically writes a user’s name onto it. Once complete, users can fill in their name, generate a certificate, and download it directly from your running Repl. Everything can be done entirely inside one Replit project.
In your Replit workspace, create these files (right-click in the file tree):
Paste the code below into main.py. This is your Flask server, which will handle form submissions and generate images dynamically. Replit preinstalls Flask by default, but if not, you can add it using the “Packages” tab and search for flask and pillow (the image library).
from flask import Flask, render_template, request, send_file
from PIL import Image, ImageDraw, ImageFont
import io
app = Flask(__name__)
@app.route('/')
def index():
// Serve the main form page
return render_template('index.html')
@app.route('/generate', methods=['POST'])
def generate():
// Get the name from form
name = request.form.get('name')
// Open the certificate template
certificate = Image.open('certificate_template.png')
draw = ImageDraw.Draw(certificate)
// Choose a font — Replit often runs without local fonts,
// so use a built-in PIL font for reliability
font = ImageFont.load_default()
// Get text size to center it on image
text_width, text_height = draw.textsize(name, font=font)
width, height = certificate.size
x = (width - text_width) / 2
y = height / 2
// Draw the name
draw.text((x, y), name, fill='black', font=font)
// Save to an in-memory file
img_io = io.BytesIO()
certificate.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png', as_attachment=True, download_name='certificate.png')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=81)
This code uses Flask’s render\_template to show the front page, and when the form is submitted to /generate, it loads your image, writes the name, and sends it as a downloadable file. You don’t need to manage a database for this simple use case.
Create a folder named templates (Flask requires this exact folder name) and inside it create index.html. Paste this code inside:
<!DOCTYPE html>
<html>
<head>
<title>Certificate Generator</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
input {
padding: 10px;
font-size: 16px;
}
button {
padding: 10px 20px;
margin-left: 10px;
font-size: 16px;
}
</style>
</head>
<body>
<h1>Generate Your Certificate</h1>
<form action="/generate" method="post">
<input type="text" name="name" placeholder="Enter your name" required />
<button type="submit">Generate</button>
</form>
</body>
</html>
This form sends the entered name to your /generate route when the user clicks “Generate”. The server then returns a PNG file to be downloaded.
If you have layout positioning issues (for example, the name appears off-center or too high/low), adjust the y coordinate in the code or add more logic to calculate placement based on image design.
When deployed properly, your live Replit app will let anyone input their name and instantly download a personalized certificate image. It’s lightweight, reliable, and works within Replit’s runtime limits, making it an ideal demonstration of combining Python, HTML, and Replit’s built-in hosting to get a usable production-style output in just a few files.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certificate Generator</title>
</head>
<body>
<form id="certForm">
<input type="text" id="name" placeholder="Participant Name" required />
<input type="text" id="course" placeholder="Course Title" required />
<button type="submit">Generate Certificate</button>
</form>
<canvas id="certificate" width="800" height="600" style="display:none;"></canvas>
<script>
const form = document.getElementById('certForm');
const canvas = document.getElementById('certificate');
const ctx = canvas.getContext('2d');
async function generateCertificate(data) {
const response = await fetch('/api/certificate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const { backgroundUrl, name, course, date } = await response.json();
const bg = new Image();
bg.crossOrigin = 'anonymous';
bg.onload = () => {
ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);
ctx.font = '30px Arial';
ctx.fillStyle = '#333';
ctx.fillText(name, 300, 250);
ctx.fillText(course, 300, 300);
ctx.font = '20px Arial';
ctx.fillText(date, 300, 350);
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = `certificate_${name}.png`;
link.click();
};
bg.src = backgroundUrl;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
name: document.getElementById('name').value,
course: document.getElementById('course').value
};
await generateCertificate(formData);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Certificate to Cloudinary</title>
</head>
<body>
<form id="uploadForm">
<input type="file" id="certFile" accept="image/png,image/jpeg" required>
<button type="submit">Upload Certificate</button>
</form>
<div id="result"></div>
<script>
const form = document.getElementById('uploadForm');
const result = document.getElementById('result');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const file = document.getElementById('certFile').files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const uploadUrl = '/api/upload-certificate';
const res = await fetch(uploadUrl, { method: 'POST', body: formData });
const data = await res.json();
if (data.url) {
result.innerHTML = `<a href="${data.url}" target="_blank">View Uploaded Certificate</a>`;
} else {
result.textContent = 'Upload failed.';
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Certificate Generator with Dynamic QR</title>
</head>
<body>
<form id="certForm">
<input type="text" id="name" placeholder="Participant Name" required>
<input type="email" id="email" placeholder="Email Address" required>
<button type="submit">Generate Certificate with QR</button>
</form>
<a id="downloadLink" style="display:none;">Download Certificate</a>
<script>
const form = document.getElementById('certForm');
const link = document.getElementById('downloadLink');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
name: document.getElementById('name').value,
email: document.getElementById('email').value
};
const res = await fetch('/api/generate-with-qr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const file = await res.blob();
const url = URL.createObjectURL(file);
link.href = url;
link.download = `certificate_${data.name}.png`;
link.style.display = 'block';
link.click();
});
</script>
</body>
</html>

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
A solid, reliable certificate generator on Replit works best when it has two parts: a frontend (a simple HTML form where someone enters their name and course) and a backend (a Node.js or Python server that takes the name, merges it into a certificate image or PDF, and returns the file). Replit makes this extremely easy to prototype, but you must structure it cleanly: organized file layout, controlled dependencies, and safe handling of secrets if you later email or store files. Don’t treat Replit like a local machine—store files properly, use the built-in “Files” tab for static assets like certificate templates, and ensure your generation code runs only on request, not on every page load.
Create a new Replit with the “Node.js” template. Inside the root, make these files and folders:
When you deploy on Replit, static assets in public/ will be directly served by Express, and the dynamic generation will be handled by the backend.
// index.js
import express from "express"
import bodyParser from "body-parser"
import { createCanvas, loadImage } from "canvas"
import fs from "fs"
const app = express()
app.use(bodyParser.json())
app.use(express.static("public"))
// POST endpoint to generate certificate
app.post("/generate", async (req, res) => {
const { name, course } = req.body
if (!name || !course) {
return res.status(400).json({ error: "Name and course are required" })
}
// Load base image
const baseImage = await loadImage("./templates/certificate-base.png")
const canvas = createCanvas(baseImage.width, baseImage.height)
const ctx = canvas.getContext("2d")
// Draw base certificate
ctx.drawImage(baseImage, 0, 0)
// Add name and course text
ctx.font = "bold 48px Arial"
ctx.fillStyle = "#000"
ctx.fillText(name, 400, 300)
ctx.fillText(course, 400, 380)
// Save file
const filePath = `./generated/${Date.now()}-${name}.png`
const buffer = canvas.toBuffer("image/png")
fs.writeFileSync(filePath, buffer)
// Send back file URL or data
res.json({ message: "Certificate created!", file: filePath })
})
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Server running on port ${port}`))
Place this file in the root of your Replit project. Make sure to install dependencies directly in the Replit shell:
npm install express body-parser canvas
<!DOCTYPE html>
<html>
<head>
<title>Certificate Generator</title>
</head>
<body>
<h2>Generate Your Certificate</h2>
<form id="certForm">
<input type="text" id="name" placeholder="Your Name" required />
<input type="text" id="course" placeholder="Course Name" required />
<button type="submit">Generate</button>
</form>
<div id="result"></div>
<script>
const form = document.getElementById("certForm")
form.addEventListener("submit", async (e) => {
e.preventDefault()
const name = document.getElementById("name").value
const course = document.getElementById("course").value
const res = await fetch("/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, course })
})
const data = await res.json()
document.getElementById("result").innerText = data.message
})
</script>
</body>
</html>
Save this file inside the public/ directory. When the user submits the form, it sends a JSON request to your backend, which draws the text onto the certificate image.
With this setup, you’ll have a functioning, real, and maintainable certificate generator that runs smoothly on Replit. It’s simple enough for learning and clean enough to scale or deploy later.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.