How to Take Screenshots and Generate PDFs in Clojure
Clojure inherits the JVM's headless browser problem — Selenium needs ChromeDriver, etaoin needs a running browser. Here's the simpler path: one clj-http call, binary response. Fits naturally into Clojure's data-oriented style.
Clojure has no native headless browser. Options on the JVM include Java interop to Selenium WebDriver, calling a Node.js subprocess for Puppeteer, or etaoin (a WebDriver library). All require a running browser and complicate deployment.
Here's the simpler path: one HTTP call, binary response. Fits naturally into Clojure's data-oriented style.
Using clj-http
clj-http is the most common synchronous HTTP client in the Clojure ecosystem:
;; deps.edn
{:deps {clj-http/clj-http {:mvn/version "3.13.0"}
cheshire/cheshire {:mvn/version "5.13.0"}}}
(ns myapp.pagebolt
(:require [clj-http.client :as http]
[cheshire.core :as json]))
(def ^:private base-url "https://pagebolt.dev/api/v1")
(defn- api-key [] (System/getenv "PAGEBOLT_API_KEY"))
(defn screenshot [url & {:keys [full-page? block-banners?]
:or {full-page? true block-banners? true}}]
(:body (http/post (str base-url "/screenshot")
{:headers {"x-api-key" (api-key)}
:content-type :json
:body (json/generate-string {:url url
:fullPage full-page?
:blockBanners block-banners?})
:as :byte-array})))
(defn pdf-from-url [url]
(:body (http/post (str base-url "/pdf")
{:headers {"x-api-key" (api-key)}
:content-type :json
:body (json/generate-string {:url url :blockBanners true})
:as :byte-array})))
(defn pdf-from-html [html]
(:body (http/post (str base-url "/pdf")
{:headers {"x-api-key" (api-key)}
:content-type :json
:body (json/generate-string {:html html})
:as :byte-array})))
;; Usage
(require '[myapp.pagebolt :as pagebolt])
(require '[clojure.java.io :as io])
(let [image (pagebolt/screenshot "https://example.com")]
(with-open [out (io/output-stream "screenshot.png")]
(.write out image)))
Using hato (async, built on Java 11 HttpClient)
;; deps.edn
{:deps {hato/hato {:mvn/version "1.0.0"}
cheshire/cheshire {:mvn/version "5.13.0"}}}
(ns myapp.pagebolt
(:require [hato.client :as hato]
[cheshire.core :as json]))
(def ^:private base-url "https://pagebolt.dev/api/v1")
(defn screenshot [url]
(:body (hato/post (str base-url "/screenshot")
{:headers {"x-api-key" (System/getenv "PAGEBOLT_API_KEY")}
:content-type :application/json
:body (json/generate-string {:url url :fullPage true :blockBanners true})
:as :byte-array})))
Ring handler — PDF download
(ns myapp.handlers.invoices
(:require [myapp.pagebolt :as pagebolt]
[myapp.templates :as templates]
[ring.util.response :as response]))
(defn download-pdf [request]
(let [id (get-in request [:path-params :id])
invoice (myapp.invoices/get-by-id id)
html (templates/render "invoice" {:invoice invoice})
pdf-bytes (pagebolt/pdf-from-html html)]
{:status 200
:headers {"Content-Type" "application/pdf"
"Content-Disposition" (str "attachment; filename=\"invoice-" id ".pdf\"")}
:body (java.io.ByteArrayInputStream. pdf-bytes)}))
Compojure route
(ns myapp.routes
(:require [compojure.core :refer [GET defroutes]]
[myapp.handlers.invoices :as invoices]))
(defroutes app-routes
(GET "/invoices/:id/pdf" [id :as request]
(invoices/download-pdf (assoc-in request [:path-params :id] id))))
Reitit route (Ring + http-kit)
(def routes
[["/invoices/:id/pdf"
{:get {:parameters {:path {:id string?}}
:handler (fn [{{:keys [id]} :path-params}]
(let [html (templates/render "invoice" {:id id})
pdf (pagebolt/pdf-from-html html)]
{:status 200
:headers {"Content-Type" "application/pdf"
"Content-Disposition"
(str "attachment; filename=\"invoice-" id ".pdf\"")}
:body (java.io.ByteArrayInputStream. pdf)}))}}]])
With error handling
(defn safe-screenshot [url]
(try
{:ok (screenshot url)}
(catch clojure.lang.ExceptionInfo e
{:error (ex-message e) :status (:status (ex-data e))})
(catch Exception e
{:error (ex-message e)})))
;; Usage
(let [{:keys [ok error]} (safe-screenshot "https://example.com")]
(if ok
(spit "screenshot.png" ok)
(println "Failed:" error)))
No Selenium, no WebDriver, no browser process. clj-http is the idiomatic synchronous HTTP client in Clojure — :as :byte-array returns binary directly, no additional processing needed.
Get Started Free
100 requests/month, no credit card
Screenshots, PDFs, and video recordings from your Clojure app — no Selenium, no browser process, works in Ring, Compojure, Reitit, and http-kit.
Get Your Free API Key →