churl

Fetch a URL through a real browser so JavaScript runs, then print the rendered HTML, text, or JSON — with an optional HAR alongside.

churl is shaped like curl, but every request runs in a real browser. That is the whole point: a page that renders its content from JavaScript comes back rendered, not as the empty shell curl would print.

churl https://example.com
<html lang="en"><head><title>Example Domain</title>

The default output is the rendered HTML — the DOM after scripts ran, not the bytes the server sent.

Output formats

churl --output-format=text https://example.com
churl --output-format=json https://example.com
churl --output-format=pdf -o page.pdf https://example.com
churl --har capture.har https://example.com     # any format, plus a HAR
churl -o page.html https://example.com

json returns url, title, and content. text is the extracted text, and it includes the contents of <style> elements, so a page’s CSS appears as a line of text near the top. Pipe it through your own filter if that matters.

pdf prints the rendered page through Chrome’s own print pipeline, so it is the page as the browser would print it — backgrounds included, JavaScript already run. It is binary, so churl refuses to write it to a terminal; pass -o or redirect:

churl --output-format=pdf https://example.com > page.pdf

The waiting flags matter more here than for other formats, because whatever has not rendered when the print happens is simply absent from the file:

churl --output-format=pdf --wait-for '#report-ready' -o report.pdf https://app.example.com

--har works alongside any format.

PDF print settings

Print geometry decides whether a generated document is usable or has to be reprinted by hand, so all of it is reachable — but through one flag rather than ten:

churl --output-format=pdf --pdf 'page=a4,margin=0.75' -o page.pdf URL
churl --output-format=pdf --pdf 'page=210mmx297mm,margin=20mm 15mm,outline' -o doc.pdf URL
churl --output-format=pdf --pdf 'page=tabloid,landscape,scale=0.8' -o wide.pdf URL
Setting Meaning
page= letter (default), legal, tabloid, ledger, a0a6, or WxH
margin= 1, 2, or 4 space-separated lengths, in CSS order (default 0.4in)
scale= render scale (default 1)
ranges= print only these one-based pages, e.g. ranges=1-5 8
landscape landscape orientation
outline PDF bookmarks from the headings; implies tagged
tagged tagged (accessible) PDF
css-page-size honour @page size from the document’s own CSS

Lengths accept in, mm, cm, and px suffixes and are inches when unsuffixed, so a4 and 210mmx297mm describe the same page. Values may not contain commas — that is why margin and ranges take space-separated lists.

outline is the one worth reaching for when printing a docs tree: it turns the heading structure into PDF bookmarks. It only works alongside tagged, because Chrome derives the outline from the tag tree, so asking for outline turns tagging on for you.

A document that declares @page size in its own CSS overrides page= and landscape, even without css-page-size. This is quiet — the flag simply has no effect — so check the document’s CSS before concluding the flag is broken.

Headers, footers, and page numbers

--pdf-header and --pdf-footer stay separate flags, because the templates are HTML and would not survive a comma-separated list. Chrome substitutes any element carrying the classes date, title, url, pageNumber, or totalPages:

churl --output-format=pdf --pdf 'margin=1' \
  --pdf-footer '<div style="font-size:9px;width:100%;text-align:center">page <span class="pageNumber"></span> of <span class="totalPages"></span></div>' \
  -o report.pdf https://example.com

The catch: the template renders inside the margin, so a footer with the default 0.4in margin is clipped or invisible. Give the edge room — margin=1 is a safe starting point — and set the font size explicitly, because the template does not inherit the page’s styles.

Images in the PDF

There is no image-quality or resampling setting. Page.printToPDF has none, and deviceScaleFactor does not affect its output — the same page printed at scale factor 1, 2, and 3 produces byte-identical PDFs.

Images are embedded at their source resolution, not at the size they are displayed. The same page with a 1600px-wide source image produced a 24 KB PDF where a 200px source produced 2.5 KB, both shown at the same width. If you need sharper images in the PDF, load sharper images.

Waiting for the page to settle

A rendered page is only worth reading once it has stopped changing. churl waits for network idle by default, and takes two more explicit signals:

churl --wait-for '#app-root' https://app.example.com
churl --stable-timeout 60 https://slow.example.com

--wait-for-challenge is on by default and waits out anti-bot interstitials such as Cloudflare’s.

Proxies

Full coverage is on its own page — including the <-loopback> behavior that makes localhost URLs fail as soon as you set --proxy.

churl behind a proxy

Blocking, scripts, and WebSockets

churl can drop requests inside the browser (--block-ads, --block-tracking, --block-domain, --block-regex), inject JavaScript before or after load (--script-before, --script-after, and their --script-file-* forms), and watch WebSocket traffic (--ws-enabled, --ws-send, --ws-wait-for). These are documented flag-by-flag in go doc ./cmd/churl; no page here has verified their workflows end to end.

What does not work

churl fetches one page per run. It does not implement wget’s recursion and mirroring flags — -r, -m, -l, -np, -P and the rest — and rejects them as undefined, exiting 2. To crawl, drive churl from a shell loop over URLs you enumerate yourself.

Exit status

There is no finer failure classification; do not branch on a specific code beyond these three. Verified against this tree: a proxy that refuses the connection (churl --proxy http://127.0.0.1:9 https://example.com) exits 1, and an unknown flag exits 2.

When to use something else

Next steps

Last updated: 2026-09-23