DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to take screenshots and generate PDFs in Clojure

How to Take Screenshots and Generate PDFs in Clojure

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"}}}
Enter fullscreen mode Exit fullscreen mode
(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})))
Enter fullscreen mode Exit fullscreen mode
;; 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)))
Enter fullscreen mode Exit fullscreen mode

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"}}}
Enter fullscreen mode Exit fullscreen mode
(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})))
Enter fullscreen mode Exit fullscreen mode

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)}))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

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)}))}}]])
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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.


Try it free — 100 requests/month, no credit card. → Get started in 2 minutes

Top comments (0)