Guide
HTML to PDF in SvelteKit
SvelteKit has no built-in PDF output. The common workaround of running a headless browser inside the server process adds a large binary dependency and complicates containerisation. PDFPipe is a single fetch call from a +server.ts or form action, using the same $env/dynamic/private pattern you already use for secrets.
Server route returning a PDF
A +server.ts file exports a GET handler that calls PDFPipe and pipes the response stream back to the browser. Navigation to the route triggers a download:
// src/routes/invoices/[id]/pdf/+server.ts
// GET /invoices/42/pdf
import type { RequestHandler } from "./$types";
import { error } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export const GET: RequestHandler = async ({ params }) => {
const { id } = params;
const html = `
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; padding: 40px">
<h1>Invoice #${id}</h1>
<p>Thank you for your order.</p>
</body>
</html>
`;
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { format: "A4" } }),
});
if (!upstream.ok) {
const body = await upstream.json().catch(() => ({})) as Record<string, unknown>;
throw error(502, String(body.detail ?? "PDF render failed"));
}
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
},
});
};Form action that generates a PDF
A form action in +page.server.ts reads form data, calls PDFPipe with store: true, and redirects the user to the CDN URL. This avoids buffering the binary on your server:
// src/routes/reports/+page.server.ts
// Form action that generates a PDF and redirects to its download URL.
import type { Actions } from "./$types";
import { fail, redirect } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export const actions: Actions = {
generate: async ({ request }) => {
const data = await request.formData();
const title = String(data.get("title") ?? "Untitled");
const html = `
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; padding: 40px">
<h1>${title}</h1>
<p>Generated at ${new Date().toLocaleString()}.</p>
</body>
</html>
`;
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4" },
store: true,
filename: `${title.toLowerCase().replace(/\s+/g, "-")}.pdf`,
}),
});
if (!upstream.ok) {
return fail(502, { error: "PDF generation failed. Please try again." });
}
const docUrl = upstream.headers.get("X-PDFPipe-Document-Url");
if (!docUrl) return fail(502, { error: "No document URL returned." });
throw redirect(303, docUrl);
},
};
// src/routes/reports/+page.svelte (corresponding form)
// <form method="POST" action="?/generate">
// <input name="title" placeholder="Report title" required />
// <button type="submit">Generate PDF</button>
// </form>Shared storage utility
For apps that generate PDFs from multiple routes, extract the storage logic into a $lib/server utility. Returns a typed object with the document ID, URL, and expiry that you can persist to a database or return as JSON:
// src/lib/server/pdf.ts
// Shared utility: store a PDF and return the CDN metadata.
import { env } from "$env/dynamic/private";
import { error } from "@sveltejs/kit";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export interface StoredDocument {
id: string;
url: string;
expiresAt: string;
}
export async function storePdf(
html: string,
filename: string,
options: { format?: string } = {}
): Promise<StoredDocument> {
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: options.format ?? "A4" },
store: true,
filename,
}),
});
if (!upstream.ok) {
const body = await upstream.json().catch(() => ({})) as Record<string, unknown>;
throw error(502, String(body.detail ?? "PDF render failed"));
}
const id = upstream.headers.get("X-PDFPipe-Document-Id") ?? "";
const url = upstream.headers.get("X-PDFPipe-Document-Url") ?? "";
const expiresAt = upstream.headers.get("X-PDFPipe-Document-Expires") ?? "";
return { id, url, expiresAt };
}
// Usage in any +page.server.ts or +server.ts:
// import { storePdf } from "$lib/server/pdf";
// const doc = await storePdf(html, "report.pdf");
// return json({ downloadUrl: doc.url, expiresAt: doc.expiresAt });Getting an API key
The Hobby plan gives 500 free documents a month with no credit card required. Paid plans start at $19 and include a longer document archive, higher limits, and email support.
Full API reference on the docs page. Try it now in the live playground with no signup.
Get an API key →