Guide
HTML to PDF in Svelte
Browser-based PDF libraries run in the client, which means fonts load inconsistently, page breaks are unreliable, and the output depends on the user's browser version. Moving generation server-side is better, but bundling a native binary into a SvelteKit deployment is painful on most hosts. The alternative is one fetch call from a +server.ts route.
Server route that streams the PDF back
Create a POST route that accepts an HTML string, forwards it to the rendering API, and pipes the binary response directly to the browser. No buffering, no temp files:
// src/routes/pdf/+server.ts
import type { RequestHandler } from "./$types";
import { error } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
export const POST: RequestHandler = async ({ request }) => {
const { html, filename = "document.pdf" } = await request.json();
const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${env.PDFPIPE_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="${filename}"`,
},
});
};The route streams the PDF body straight from the upstream response, so memory usage stays flat regardless of document size. A specific error is returned when the render fails rather than a silent 500.
Calling the route from a Svelte 5 component
The component below uses Svelte 5 runes: $props() for typed inputs and $state() for loading and error state. The click handler posts to the server route and triggers a browser download:
<!-- src/lib/PdfButton.svelte (Svelte 5 runes) -->
<script lang="ts">
let { html, filename = "document.pdf" }: {
html: string;
filename?: string;
} = $props();
let loading = $state(false);
let errorMsg = $state("");
async function downloadPdf() {
loading = true;
errorMsg = "";
try {
const res = await fetch("/pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html, filename }),
});
if (!res.ok) {
errorMsg = "PDF generation failed. Please try again.";
return;
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch {
errorMsg = "An unexpected error occurred.";
} finally {
loading = false;
}
}
</script>
<button onclick={downloadPdf} disabled={loading}>
{loading ? "Generating..." : "Download PDF"}
</button>
{#if errorMsg}
<p class="error">{errorMsg}</p>
{/if}Drop PdfButton anywhere in your app and pass in the HTML string you want rendered. The component handles the full lifecycle: fetch, blob conversion, anchor click, and URL cleanup.
Storing the API key
Add the key to .env at the project root. Do not prefix it with VITE_ or PUBLIC_: SvelteKit only exposes unprefixed variables to server-side code accessed via $env/dynamic/private.
# .env
# Never prefix with VITE_ or PUBLIC_ — this key stays server-side only.
PDFPIPE_KEY=your_key_hereGetting a hosted URL for email sharing
Pass store: true in the request body and the API returns a JSON envelope with a hosted document URL instead of a raw binary stream. Use this when you need to email the PDF, save the link in a database, or open it in a new tab without creating a blob URL on the client:
// src/routes/pdf/+server.ts
// Pass store: true to get back a JSON envelope with a hosted URL.
import type { RequestHandler } from "./$types";
import { error, json } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
export const POST: RequestHandler = async ({ request }) => {
const { html, filename = "document.pdf" } = await request.json();
const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${env.PDFPIPE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
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 documentUrl = upstream.headers.get("X-PDFPipe-Document-Url") ?? "";
const documentId = upstream.headers.get("X-PDFPipe-Document-Id") ?? "";
return json({ documentUrl, documentId });
};
// In your component, call the route and read the URL:
// const { documentUrl } = await res.json();
// — then email it, persist it, or open it in a new tab.Pricing
The Hobby plan gives 500 free documents per month with no credit card required. Paid plans add higher limits, a longer document archive, and email support:
Plan Documents/month Price
────────────────────────────────
Hobby 500 $0
Starter 5,000 $19 / mo
Growth 20,000 $49 / mo
Scale 50,000 $149 / mo
Business Unlimited $499 / moPDFPipe is the API used above. Try it in the live playground with no signup, or browse the full API reference.
See pricing →