Skip to content

§ Django HTML to PDF

Django HTML to PDF: Convert a Django Template to a Real PDF

Turn a Django template into a searchable, pixel accurate PDF with one API call. Managed Chromium renders your real template with full CSS, web fonts and JavaScript charts, so the file matches the page, no WeasyPrint CSS limits and no headless browser to run and scale yourself. Try it on any URL or HTML below.

  • Early access, launching soon
  • No card required
  • Your HTML stays yours

§ Live demo

Runs in your browser. Nothing is uploaded.

Your PDF is downloading. Want this as one API call, with the page archived too?

§ Short answer

To convert HTML to PDF in Django, render your template to an HTML string with render_to_string, then hand that HTML to something that lays it out. WeasyPrint does this in pure Python and is the best free choice for styled templates, but it has no JavaScript engine, so client-side charts and maps come out blank. When the page depends on JavaScript, or you want output identical to a real browser and do not want to install and scale a renderer, post the HTML to a rendering API instead. Sitepdf runs that final step in managed Chromium and returns a true, selectable vector PDF, plus an optional timestamped archive of exactly what it rendered.

Last updated July 2026. Written and fact checked by the Sitepdf team.

§ 00

Three ways to make a PDF in Django, compared

Django ships no PDF export, so every project picks one of these three routes. They differ most in whether a real browser does the rendering, which decides your CSS fidelity, whether JavaScript runs, and what you have to install and run.

Approach How it works CSS and JavaScript What you run Best for
WeasyPrint
django-weasyprint
Renders your template HTML and CSS to a PDF in pure Python, no browser Strong CSS including @page paged media, but no JavaScript engine at all A Python library plus its Pango and Cairo system libraries Server-rendered, styled templates with no client-side JavaScript
xhtml2pdf, ReportLab
code path
xhtml2pdf converts simple HTML on top of ReportLab; ReportLab draws the page in code Limited CSS in xhtml2pdf; ReportLab ignores HTML and CSS by design A pip-only, binary-free Python install Simple templated receipts, or documents you build in code
Server-side Chromium API
Sitepdf
Renders your real template or route in a managed browser Full modern CSS and JavaScript, it is a real current browser One HTTP call, no renderer or browser in your stack Production documents, JavaScript charts, exact fidelity, a dated record

The honest read: WeasyPrint is genuinely excellent and free, and if your invoices and reports are server-rendered HTML with CSS and no JavaScript, it may be all you ever need. A rendering API earns its place in three cases: the page draws content with JavaScript (Chart.js, a map, a live widget), you want the PDF to match a real browser pixel for pixel, or you do not want to install Pango and Cairo and then scale a renderer under load.

§ 01

The three real options in Django, and what separates them

Django deliberately leaves PDF generation to you, so the question is never "which Django feature" but "which rendering engine". There are really three families. The first is WeasyPrint, usually wired in with django-weasyprint: it reads your HTML and CSS and produces a print-quality PDF entirely in Python, with no browser involved. The second is the ReportLab family, where xhtml2pdf converts a limited subset of HTML on top of ReportLab, and ReportLab itself lets you draw the document in code. The third is a headless browser, either one you run yourself with Playwright, or a hosted rendering API, which is the only route that runs your JavaScript and matches a real browser exactly.

The line that matters runs between "a browser renders it" and "a library renders it". A library like WeasyPrint is fast, free and has no browser to babysit, but it will never run the JavaScript that draws a Chart.js graph, and its CSS support, while good, is a subset of what Chromium does. A browser runs everything your users see, at the cost of being a browser: memory, cold starts and scaling. Picking the right option is mostly about whether your document depends on JavaScript and how close to the on-screen page it has to look.

§ 02

WeasyPrint: the best free choice, until JavaScript enters

For a server-rendered invoice, statement or report, WeasyPrint is hard to beat on price and simplicity. You render the template to a string and hand it over, and you get clean PDF bytes back with real, selectable text.

from django.template.loader import render_to_string
from weasyprint import HTML

html = render_to_string("invoices/invoice.html", {"invoice": invoice})
pdf = HTML(string=html, base_url=request.build_absolute_uri()).write_pdf()
return HttpResponse(pdf, content_type="application/pdf")

Two things trip people up. Pass base_url so WeasyPrint can resolve your static CSS, images and fonts, and remember that its layout comes from print CSS, so use the @page rule for margins and page size. The hard limit is JavaScript: WeasyPrint has no JavaScript engine, so a page that builds a chart, a table or a map in the browser will render blank or half-formed. The official advice is to render that data to static HTML or SVG on the server first, or move to a headless-Chrome pipeline. If your documents are pure server-side HTML and CSS, none of that applies and WeasyPrint is the right call. As of its February 2026 release it needs Python 3.10 or newer, plus the Pango and Cairo system libraries on every machine that runs it.

§ 03

The server-side path: render your real template in a browser

When the page has JavaScript, or you simply want the PDF to look exactly like the page in Chrome, let a real browser do the work. You do not have to change how Django builds the page. Render the template to a string as usual, then post that HTML to the render endpoint, or point it at a live route in your app and let it load the page, run the JavaScript and wait for the charts.

from django.template.loader import render_to_string
import requests

html = render_to_string("invoices/invoice.html", {"invoice": invoice})

resp = requests.post(
    "https://api.sitepdf.com/v1/render",
    headers={"Authorization": f"Bearer {settings.SITEPDF_KEY}"},
    data={"html": html, "format": "Letter", "archive": "true"},
)
pdf_url = resp.json()["pdf_url"]

Because a current browser does the layout, your CSS grid, web fonts, background colors and JavaScript-drawn charts all render the way they do on screen, and the text stays real and searchable. There is no Pango, Cairo or Chromium to install in your image and no browser process eating memory inside your Django workers. The same call works from Celery for background jobs and from a management command for batch runs. Our HTML to PDF in Python guide shows the plain requests version outside Django, and the PDF generator API page lists every parameter, including margins, page size, headers and the wait signal for charts.

§ 04

Keep a dated copy of every document you generated

A generated invoice, a signed quote or a compliance report leaves your app and nothing verifiable stays behind. Add archive=true to the render call and Sitepdf also stores a timestamped, immutable snapshot of exactly what it rendered, retrievable by id later, so you can prove what a given document said on a given date. That provenance is often the reason to render server side at all. The website archiving page explains how the record works, and the Laravel HTML to PDF page walks the same trade offs for the other big server framework. Renders are transient by default; leave archiving off and we return the PDF and store nothing.

Post your rendered Django template, or point the API at a live route. A real browser prints it.
curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/invoices/8842 \
  -F format=Letter \
  -F wait_for="#invoice-total" \
  -F archive=true

{
  "pdf_url": "https://api.sitepdf.com/v1/documents/doc_dj42k.pdf",
  "pages": 2,
  "rendered_in_ms": 1642,
  "archive": {
    "id": "arc_dj7px",
    "captured_at": "2026-07-19T21:04:12Z",
    "retrieve_url": "https://api.sitepdf.com/v1/archives/arc_dj7px"
  }
}

The API is in early access; this is the documented call shape it opens with. Full request and response walkthrough.

§ 05

Questions about this job

How do I convert HTML to PDF in Django?
Render your template to an HTML string with render_to_string, then pass that HTML to a renderer. WeasyPrint does it in pure Python for styled pages with no JavaScript. For pages with JavaScript charts, or output that must match a real browser, post the HTML to a rendering API that runs managed Chromium and returns a searchable PDF.
Does WeasyPrint support JavaScript?
No. WeasyPrint renders HTML and CSS only and has no JavaScript engine, so any content a page draws with JavaScript, such as a Chart.js graph or an interactive map, comes out blank. The fix is to render that data to static HTML or SVG on the server first, or to render the page in a real browser through a headless-Chrome pipeline or an API.
WeasyPrint, xhtml2pdf or ReportLab, which should I use in Django?
Use WeasyPrint for styled, server-rendered templates with real CSS and no JavaScript, since it is free and produces print-quality output. Use xhtml2pdf for simple templated documents where a pure-Python, binary-free install matters more than CSS fidelity. Use ReportLab when you would rather build the document in code than maintain an HTML template.
How do I render a Django template with charts to PDF?
If the chart is drawn by JavaScript, WeasyPrint will not run it, so either pre-render the chart to an SVG or image on the server, or render the page in a real browser. A rendering API loads the route in managed Chromium, runs the JavaScript, waits for the chart to finish, then prints it, so the graph appears in the PDF exactly as on screen.
How do I generate a PDF in Django without installing a browser?
Send your rendered template HTML to a hosted rendering API. The browser runs on their infrastructure, so you install nothing beyond an HTTP client, avoid the Pango, Cairo and Chromium system dependencies, and keep your Django workers free of a memory-hungry browser process. You get back a true vector PDF with selectable text in one call.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API