You shipped a real app this weekend. v0 scaffolded the UI, you wired up the data, it looks great. Then your first user asks for the one thing the AI didn't build: "Can I download this as a PDF?"
So you reach for the obvious options, and they all disappoint:
window.print()— gives you the browser's print dialog, page margins you don't control, and a result that looks like a 2009 web page, not a document.- A client-side library (
jspdf,html2canvas) — rasterizes your DOM into a blurry image, breaks on page boundaries, ignores half your CSS, and balloons your bundle. - Puppeteer — the right rendering engine, but it doesn't deploy on serverless, and now you've lost the afternoon.
The clean answer is to render the PDF on the server, with a real browser engine, via one API call. Here's the whole thing.
1. Get a key
Sign up at leafwright.app, create a project, copy your lw_live_… key into .env:
LEAFWRIGHT_API_KEY=lw_live_xxxxxxxx
2. Add one route
Your app already renders the thing on screen as HTML. Send that HTML to the API and hand back a download link:
// app/api/export/route.ts
export async function POST(req: Request) {
const { html } = await req.json();
const res = await fetch("https://leafwright.co/api/v1/pdfs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LEAFWRIGHT_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
source: { type: "html", html },
delivery: { return: "url" }
})
});
const { download_url } = await res.json();
return Response.json({ download_url });
}
3. Wire up the button
async function downloadPdf(html: string) {
const res = await fetch("/api/export", {
method: "POST",
body: JSON.stringify({ html })
});
const { download_url } = await res.json();
window.open(download_url, "_blank");
}
That's it. Server-side Chromium renders your HTML exactly as a browser would — real fonts, real CSS, proper page breaks — and returns a signed URL.
Don't want to write print CSS?
Screen CSS and print CSS aren't the same, and getting page breaks, headers, and margins right is fiddly. So skip it: describe the document and let AI draft a clean, branded template for you, then render it with your data:
// Render a published template with JSON instead of raw HTML
await fetch("https://leafwright.co/api/v1/templates/tmpl_receipt/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LEAFWRIGHT_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ data: { order_id: "1024", total: "$59.00" } })
});
Lock your fonts, colors, and footer into a brand kit once, and every export — whether a user clicks the button or your backend generates 10,000 receipts overnight — comes out on-brand.
Ship it
PDF export is the feature that makes a weekend project feel like a product. It shouldn't take more than the five minutes above.