Guide
HTML to PDF in Astro
Astro is primarily a static site builder, but its SSR mode and API endpoints make server-side PDF generation straightforward. There are no PDF libraries on npm that fit Astro's edge-friendly model. PDFPipe is a single fetch call from any API route or SSR page, with no binary dependencies.
Astro API endpoint
Add a .ts file under src/pages/api/ and export a named HTTP method handler. Requires output: "server" or prerender = false in hybrid mode:
// src/pages/api/pdf.ts
// POST /api/pdf — Astro API endpoint (requires SSR or hybrid output)
// astro.config.mjs: output: "server" or export const prerender = false
import type { APIRoute } from "astro";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export const POST: APIRoute = async ({ request }) => {
const apiKey = import.meta.env.PDFPIPE_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: "API key not configured" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
let body: { html?: string; filename?: string };
try {
body = await request.json();
} catch {
return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const { html, filename = "document.pdf" } = body;
if (!html) {
return new Response(JSON.stringify({ error: "html is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { format: "A4" } }),
});
if (!upstream.ok) {
const err = await upstream.json().catch(() => ({})) as Record<string, unknown>;
return new Response(JSON.stringify({ error: String(err.detail ?? "render failed") }), {
status: 502,
headers: { "Content-Type": "application/json" },
});
}
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
};SSR route generating a PDF
An SSR page with a .pdf.ts suffix handles GET requests for a specific document. Navigating to /invoices/42.pdf triggers a browser download directly:
// src/pages/invoices/[id].pdf.ts
// GET /invoices/42.pdf — SSR route that generates the PDF on request.
// Add export const prerender = false at the top for hybrid mode.
import type { APIRoute } from "astro";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export const prerender = false;
export const GET: APIRoute = async ({ params }) => {
const { id } = params;
const apiKey = import.meta.env.PDFPIPE_API_KEY;
if (!apiKey) {
return new Response("Server misconfigured", { status: 500 });
}
const html = buildInvoiceHtml(id!);
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: {
format: "A4",
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
},
}),
});
if (!upstream.ok) {
return new Response("PDF generation failed", { status: 502 });
}
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
},
});
};
function buildInvoiceHtml(id: string): string {
return `
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; padding: 40px">
<h1>Invoice #${id}</h1>
<p>Thank you for your order.</p>
</body>
</html>
`;
}
// Link from any .astro page:
// <a href={`/invoices/${id}.pdf`} download>Download PDF</a>Store and return a CDN URL
Pass store: true to persist the PDF on the CDN. The endpoint returns JSON with the document ID, URL, and expiry so your client-side island or script can open it directly without routing through your server:
// src/pages/api/pdf/store.ts
// POST /api/pdf/store — stores the PDF and returns the CDN metadata as JSON.
import type { APIRoute } from "astro";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const apiKey = import.meta.env.PDFPIPE_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: "API key not configured" }), {
status: 500, headers: { "Content-Type": "application/json" },
});
}
const { html, filename = "document.pdf", format = "A4" } =
await request.json() as { html: string; filename?: string; format?: string };
if (!html) {
return new Response(JSON.stringify({ error: "html is required" }), {
status: 400, headers: { "Content-Type": "application/json" },
});
}
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format },
store: true,
filename,
}),
});
if (!upstream.ok) {
return new Response(JSON.stringify({ error: "PDF render failed" }), {
status: 502, headers: { "Content-Type": "application/json" },
});
}
const docId = upstream.headers.get("X-PDFPipe-Document-Id");
const docUrl = upstream.headers.get("X-PDFPipe-Document-Url");
const expiresAt = upstream.headers.get("X-PDFPipe-Document-Expires");
return new Response(JSON.stringify({ docId, docUrl, expiresAt }), {
headers: { "Content-Type": "application/json" },
});
};
// Client-side usage in an Astro island or vanilla script:
// const res = await fetch("/api/pdf/store", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ html, filename: "report.pdf" }),
// });
// const { docUrl } = await res.json();
// window.open(docUrl, "_blank");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 →