How to Convert a Web Page to PDF in SvelteKit
Choose browser printing, client-side capture, Playwright, or a hosted API to create PDFs from SvelteKit pages, with runnable examples and troubleshooting.

To convert a web page to PDF in SvelteKit, choose where the conversion should run. For a user who can save from the browser’s print dialog, add print styles and call window.print() from a client-side event. For an in-app download of one DOM element, use a browser-only library such as html2pdf.js. For automated or server-generated PDFs, render the page with Playwright and return its PDF buffer. A hosted conversion API is another server-side option.
SvelteKit does not provide a built-in HTML-to-PDF API. It supports server, browser, and build-time rendering, so code that uses browser globals must run only in the browser. Pick the method based on who starts the export, how closely the output must match the page, whether the user needs a dialog, and whether your deployment can run a browser process. See the SvelteKit documentation on page options.
1. Choose a PDF generation method
| Method | Best for | Main constraint |
|---|---|---|
| Browser print | A user-triggered “Print or save PDF” action | The user controls the print destination and dialog. |
| Client-side HTML capture | Downloading a particular component from the browser | Canvas conversion can differ from browser printing, especially for complex content. |
| Playwright | Automated exports and server-generated files | The host must support a browser runtime and its installation. |
| Hosted conversion API | Server-side generation without maintaining your own browser | Review data handling, cost, access requirements, and rendering dependencies. |
There is no universal quality or speed winner. Test representative pages with the fonts, images, charts, and page breaks your application actually uses. The implementation examples below use a dedicated SvelteKit route called /reports/[id]/print; replace it with your own route and authorization checks.

2. Browser print: the simplest user-initiated flow
Use browser printing when a user can click a button, inspect the print preview, and choose “Save as PDF.” A print stylesheet gives you control over page size, margins, hidden controls, and page breaks while leaving the browser in charge of producing the PDF.

Add print styles
<!-- src/routes/reports/[id]/+page.svelte -->
<script>
let { data } = $props();
function printReport() {
// This handler runs after a user action in the browser.
window.print();
}
</script>
<svelte:head>
<title>{data.report.title}</title>
</svelte:head>
<button class="no-print" onclick={printReport}>Print or save PDF</button>
<article class="report">
<h1>{data.report.title}</h1>
<p>{data.report.summary}</p>
{#each data.report.sections as section}
<section class="report-section">
<h2>{section.title}</h2>
<p>{section.body}</p>
</section>
{/each}
</article>
<style>
@page { size: A4; margin: 16mm; }
.report { max-width: 70rem; margin: 2rem auto; }
@media print {
.no-print { display: none !important; }
.report { max-width: none; margin: 0; }
.report-section { break-inside: avoid; }
h1, h2 { break-after: avoid; }
a { color: inherit; text-decoration: none; }
}
</style>
In older Svelte versions, use the component syntax and event directives supported by your installed version. The essential constraint is unchanged: do not call window.print() while the component is being rendered on the server. Calling it inside a browser click handler naturally defers it until the user interacts.
Print-flow checklist
- Hide navigation, buttons, cookie controls, and other screen-only UI with
@media print. - Use
@pagefor paper size and margins, then preview the result in the browsers you support. - Apply
break-inside: avoidselectively to cards or sections that should stay together; oversized elements may still need to split. - Check headers, footers, links, background colors, long tables, and images in print preview. Browser and platform behavior can differ.
- Provide a visible print button and a clear fallback for users whose browser or platform handles print destinations differently.
This approach does not create a PDF byte stream your server can attach to an email or store automatically. It opens the user’s browser print flow, where the user chooses the destination.
3. Client-side export of one element with html2pdf.js
If the user expects a download button that exports a specific element, html2pdf.js is one browser-side option. Its documented workflow composes html2canvas and jsPDF. It must run in a browser, not Node.js, so load it only when the user triggers export. Review the html2pdf.js project documentation for current options and supported versions.
<script>
let reportElement;
let exporting = $state(false);
let errorMessage = $state('');
async function downloadPdf() {
if (!reportElement || exporting) return;
exporting = true;
errorMessage = '';
try {
const { default: html2pdf } = await import('html2pdf.js');
await html2pdf()
.set({
margin: [12, 12, 12, 12],
filename: 'report.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'] }
})
.from(reportElement)
.save();
} catch (error) {
errorMessage = 'PDF export failed. Try printing the page or reload and retry.';
console.error(error);
} finally {
exporting = false;
}
}
</script>
<button onclick={downloadPdf} disabled={exporting}>
{exporting ? 'Preparing PDF…' : 'Download PDF'}
</button>
{#if errorMessage}<p role="alert">{errorMessage}</p>{/if}
<article bind:this={reportElement}>…report content…</article>
Install the package using your project’s package manager before running the example. The dynamic import keeps browser-only code out of server rendering. If your Svelte version does not support $state, use the corresponding reactive declaration syntax for that version. The library options shown configure page margins, filename, raster image type and quality, canvas scale, PDF paper format, orientation, and CSS page-break handling.
Canvas-based export is a rendering pipeline, not the browser’s native print engine. Large pages can consume substantial memory, and cross-origin images or complex CSS may not appear as expected. Verify output with real pages, particularly for charts, SVG, web fonts, fixed-position content, and long tables. When visual fidelity or predictable pagination matters more than a direct client-side download, compare the result with print CSS or server-side browser rendering.
4. Server-generated PDF with Playwright
Use Playwright when your application needs to generate a PDF on demand without asking the user to operate a print dialog. A server endpoint can load a stable print route, wait for the page to be ready, call page.pdf(), and return the bytes. Playwright documents that page.pdf() returns a PDF buffer and uses print CSS media by default. See the Playwright Page API.
Install and create a SvelteKit endpoint
npm install playwright
// src/routes/api/reports/[id]/pdf/+server.js
import { chromium } from 'playwright';
import { error } from '@sveltejs/kit';
export async function GET({ params, url, locals }) {
// Replace this with your application's real access check.
if (!locals.user) error(401, 'Sign in to export this report');
const origin = url.origin;
const printUrl = `${origin}/reports/${encodeURIComponent(params.id)}/print`;
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto(printUrl, { waitUntil: 'networkidle', timeout: 30_000 });
await page.locator('[data-pdf-ready="true"]').waitFor({ timeout: 10_000 });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '16mm', right: '16mm', bottom: '16mm', left: '16mm' }
});
return new Response(pdf, {
headers: {
'content-type': 'application/pdf',
'content-disposition': `attachment; filename="report-${params.id}.pdf"`
}
});
} finally {
await browser.close();
}
}
Add a readiness marker to the print route only after the content it needs is available:
<article data-pdf-ready="true">
…fully loaded report…
</article>
For an authenticated internal route, a server-side request to url.origin may not carry the original user’s session cookies. Do not remove the access check to make rendering work. Instead, design a trusted server-side data path or explicitly transfer narrowly scoped authentication to the rendering context using your application’s security model. Avoid putting long-lived secrets in a URL.
Useful Playwright PDF controls
| Option | Use |
|---|---|
format |
Select a paper preset such as A4 or Letter. |
width, height |
Specify page dimensions when a named format is not suitable. |
margin |
Set top, right, bottom, and left margins. |
printBackground |
Include background graphics and colors. |
preferCSSPageSize |
Prefer CSS @page dimensions over a scaled paper preset. |
pageRanges |
Export selected pages, for example 1-3. |
scale |
Adjust rendered content scale. |
displayHeaderFooter, templates |
Add configured header or footer templates. |
If the design is explicitly screen-oriented, Playwright documents emulating screen media before calling page.pdf(). For print-first reports, use print CSS and the default print media behavior. Keep the route deterministic: avoid relying on animations, transient notifications, or data that changes while the PDF is being assembled.
5. Hosted conversion API from a server route
A hosted API can perform the conversion when you do not want to install and maintain a browser runtime. For example, HTML2PDF.app documents an authenticated POST that accepts a public URL or raw HTML and returns PDF bytes. Its key should stay in server-side code, not browser JavaScript or a public repository. Consult the HTML2PDF.app documentation for its request schema and current service terms.
The following shows the shape of a server-side request; set the endpoint and JSON fields to the provider’s current documented schema and keep the key in a server-only environment variable:
// src/routes/api/reports/[id]/hosted-pdf/+server.js
import { error } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
export async function GET({ params, url, fetch, locals }) {
if (!locals.user) error(401, 'Sign in to export this report');
if (!env.PDF_API_KEY) error(503, 'PDF export is not configured');
const response = await fetch('https://api.html2pdf.app/v1/generate', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${env.PDF_API_KEY}`
},
body: JSON.stringify({
url: `${url.origin}/reports/${encodeURIComponent(params.id)}/print`,
media: 'print',
format: 'A4',
margin: '16mm'
})
});
if (!response.ok) error(502, 'PDF provider could not generate the document');
return new Response(await response.arrayBuffer(), {
headers: { 'content-type': 'application/pdf' }
});
}
Important: the provider’s exact endpoint, authentication header, body fields, and response behavior can change. Treat this as an integration outline, then copy the current documented schema before deployment. A provider must also be able to reach the requested URL; a private route needs a secure access arrangement. Review how document content is transmitted and processed before sending user data to an external service.
6. cURL, Python, and Node.js request examples
These generic examples call a hosted PDF service from a trusted server or shell. They illustrate sending a URL for conversion; adapt the endpoint, authentication, and request body to the provider you select. Do not expose API keys in client-side code.
cURL
curl -X POST 'https://api.html2pdf.app/v1/generate' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
--data '{"url":"https://example.com/report","media":"print","format":"A4"}' \
--output report.pdf
Python
import os
import requests
response = requests.post(
"https://api.html2pdf.app/v1/generate",
headers={
"Authorization": f"Bearer {os.environ['PDF_API_KEY']}",
"Content-Type": "application/json",
},
json={"url": "https://example.com/report", "media": "print", "format": "A4"},
timeout=90,
)
response.raise_for_status()
with open("report.pdf", "wb") as pdf_file:
pdf_file.write(response.content)
Node.js
const response = await fetch('https://api.html2pdf.app/v1/generate', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PDF_API_KEY}`,
'content-type': 'application/json'
},
body: JSON.stringify({
url: 'https://example.com/report',
media: 'print',
format: 'A4'
})
});
if (!response.ok) throw new Error(`PDF request failed: ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
await (await import('node:fs/promises')).writeFile('report.pdf', bytes);
7. Or skip the browser setup
If your task is capturing a web page as an image or PDF for a workflow, ScreenshotNeo offers a hosted screenshot API and MCP server. A screenshot is not the same as a paginated text PDF: use a PDF-capable route when you need selectable text, print pagination, or a document for filing. For screenshot capture, one GET request returns PNG, JPEG, WebP, or PDF. See ScreenshotNeo and the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.
Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card.
8. Troubleshooting common PDF problems
| Symptom | Likely cause | Fix |
|---|---|---|
window is not defined during build or server rendering |
Browser-only code ran while SvelteKit rendered on the server. | Call it from a user event, or dynamically import browser-only packages when the action runs. |
| PDF is blank or content is missing | The capture started before data, fonts, images, or client rendering finished. | Wait for an explicit ready marker; check failed network requests and make loading state deterministic. |
| Browser print has unwanted navigation or buttons | Screen styles also apply in print. | Hide controls under @media print and inspect the print preview. |
| Sections split in awkward places | Content exceeds a page or break rules are missing or too broad. | Apply break-inside: avoid to appropriately sized sections and add break rules around headings; test long content. |
| Images are missing in html2pdf.js | Cross-origin restrictions, late loading, or unsupported image behavior. | Use permitted CORS responses, wait for images, and test the target assets. Do not assume useCORS bypasses a server’s policy. |
| Playwright works locally but fails after deployment | The host may not include the required browser binary or support its execution model. | Check adapter and platform requirements, browser installation, memory, and process limits for the target host. |
| Hosted API reports an inaccessible page | The service cannot reach a private URL or lacks authentication. | Use a secure server-side access design supported by the provider; do not publish private report URLs or secrets. |
| Wrong paper size, scale, or missing backgrounds | CSS page size and API options may conflict, or background printing is disabled. | Choose a single paper-size source, set margins explicitly, and enable background printing where supported. |
| PDF request times out | Network-idle may never occur, a resource is stalled, or the page is expensive to render. | Wait for a specific readiness condition, set bounded timeouts, and remove unnecessary network dependencies. |
9. Performance, reliability, and cost
Browser printing delegates work to the user’s machine and avoids a server PDF-rendering queue, but the application cannot guarantee a saved file because the user controls the print flow. Client-side element capture keeps the conversion in the browser; large canvas renders can raise memory use and make mobile devices a concern. Reduce oversized export regions and test at the largest document size you support.
Server-side Playwright gives your application control over the returned bytes and options, while adding browser startup, resource use, and operational requirements. Reuse a browser process where the hosting model permits it, isolate each page context, close resources on every path, and place timeouts around navigation and readiness. A hosted API shifts browser operations to a provider but introduces service cost, network latency, availability, and data-handling considerations. The research does not establish comparative prices or benchmarks; measure your own documents and check current provider terms.
For any method, make the rendered page stable: use print CSS, wait for content readiness, avoid unnecessary animations, and validate representative cases. Treat a failed conversion as a recoverable operation, log a request identifier and failure stage without logging document secrets, and offer a browser print fallback when it fits the product.
10. Frequently asked questions
Does SvelteKit have a built-in PDF export function?
No. SvelteKit provides the application and rendering modes; the PDF comes from browser printing, a client library, browser automation, or a hosted service.
Can I generate a PDF during server-side rendering?
Server rendering HTML is not itself PDF generation. Generate PDF bytes with a server-side renderer or API, or let the user’s browser handle printing.
Which option keeps text selectable?
Browser print and browser-based PDF renderers are generally the paths to evaluate for document-style output. Confirm text selection and accessibility in the generated files your application actually produces; canvas-based captures can represent content as images.
Can I export a logged-in page?
Yes, if the renderer receives authorized data through a design appropriate to your application. A remote service cannot automatically access a private page, and credentials should not be exposed in public URLs or client bundles.
How do I make the PDF use screen styles?
For Playwright, emulate screen media before calling page.pdf(). For print-oriented reports, prefer print CSS and the default print media behavior.


