Running Clojure as a CGI-bin Script Example

Example Project

A Metal Subgenre Sample Platter

Hello

This page was served by a Clojure script running through Babashka. This gives us the functional programming paradigm we love with the accessibility of PHP!

How it Works

Your browser is requesting this file from the server. The server, running Apache, is processing it as a cgi-bin script. The script's contents is piped as input to the static Babashka binary, which then interprets the code, runs the program, and outputs the printed HTML. The Apache web server then returns the HTML as a response which the browser pieces together into the page you are seeing now.

The Code

metal.clj

Initial setup code. Pulls in libraries and adds source directories to source path. Might be able to make a generic bash script for this but works for now.

#!/bin/env /home1/<username>/bin/bb

(ns cgi.metal
  (:require
   [babashka.classpath :refer [add-classpath]]
   [babashka.fs :as fs]
   [babashka.pods :as pods]
   [clojure.java.shell :refer [sh]]
   [clojure.string :as s]
   [hiccup2.core :refer [html]]))

;; Dynamic Libs
(def LIB-DIR "/home1/<username>/lib/")
(def CWD
  (if-let [filename (System/getenv "SCRIPT_FILENAME")]
    (str (fs/parent filename))
    (System/getenv "PWD")))


(defn lib
  "
  Create an absolute path to a jar file in sibling lib directory
  Takes a string filename like \"honeysql.jar\"
  Returns a string like \"/path/to/dir/lib/honeysql.jar\".
  "
  [path]
  (str LIB-DIR path))

;; Add jars and current directory to classpath to import library code

(add-classpath (s/join ":" [CWD
                            (lib "gaka.jar")
                            (lib "honeysql.jar")]))
(pods/load-pod (lib "pod-babashka-postgresql"))

;; Require our main page code
;; Must be placed here after updating the class path

(require
 '[metal.core :as metal])

;; CGI scripts must print headers then body

(println "Content-type:text/html\r\n")
(println (str (html metal/content)))

(System/exit 0)

metal/core.clj

The example code. Fetches each band from the database and displays them in HTML using the hiccup library that comes with Babashka 2.8.

(ns metal.core
  (:require
   [clojure.string :as s]
   [hiccup.util :refer [raw-string]]
   [honeysql.core :as sql]
   [pod.babashka.postgresql :as pg]
   [gaka.core :refer [css]]
   [metal.guide :as guide]
   [metal.style :refer [rems]]))

;; Load secrets for the db
(def secrets (read-string (slurp "prod.secret.edn")))

;; Connect to the database
(def db {:dbtype   "postgresql"
         :host     "localhost"
         :dbname   (:db/name secrets)
         :user     (:db/user secrets)
         :password (:db/password secrets)
         :port     5432})

;; Fetch bands from the database
(def bands (pg/execute! db (sql/format {:select [:*]
                                        :from [:metal_bands]
                                        :order-by [[:rank :desc]]})))

(defn embed-url
  "Transforms a public youtube URL to the embedded URL"
  [yt-url]
  (as-> yt-url $
    (s/split $ #"=")
    (drop 1 $)
    (s/join "" $)
    (str "https://youtube.com/embed/" $)))

(defn popularity
  [pop-rank]
  (str (s/join "" (repeat pop-rank "★"))
       (s/join "" (repeat (- 5 pop-rank) "☆"))))

(def style
  (css
      [:body
       {:padding 0
        :margin 0
        :font-family :sans-serif
        :background-color "#E5E5E5"}]
      [:h1
       {:font-size (rems 32)}]
      [:h2
       {:font-size (rems 24)
        :font-family "\"Metal Mania\", sans-serif"}]
      [:h3
       {:font-size (rems 20)}]
      [:h4
       {:font-size (rems 18)}]
      [:.example
       {:padding "2rem"
        :text-align :center
        :margin-bottom "4rem"}]
      [:.cards
       {:list-style "none"
        :display "flex"
        :flex-flow "row wrap"
        :align-items "stretch"
        :justify-content "space-evenly"
        :margin "0 auto"
        :padding "0"
        :max-width "1100px"}]
      [:.cards__item
       {:background-color "#FFF"
        :flex "0 0 320px"
        :box-sizing "border-box"
        :position "relative"
        :margin "1rem"
        :box-shadow "0 0 10px 0 rgba(0, 0, 0, 0.2)"}]
      [:.card
       {:box-sizing :border-box
        :width "320px"
        :display "block"
        :position :relative
        :text-align :left
        :border-bottom-left-radius "5px"
        :border-bottom-right-radius "5px"}]
      [:.rank
       {:position :absolute
        :right "-16px"
        :top "-16px"
        :z-index 100
        :border-radius "50%"
        :width "32px"
        :height "32px"
        :background "#fff"
        :line-height "32px"
        :text-align :center
        :font-size (rems 14)
        :font-style :italic
        :color "#666"}]
      [:.card__body
       {:padding "1rem"}]
      [:.detail
       {:margin-bottom "1rem"}]
      [:.label
       {:display "block"
        :font-weight "bold"}]
      [:.comment
       {:font-size (rems 14)
        :line-height 1.4}]))

(def example
  [:section.example
   [:h1 "Running Clojure as a CGI-bin Script Example"]
   [:h2 "Example Project"]
   [:p "A Metal Subgenre Sample Platter"]
   [:ul.cards
    (for [band bands]
      [:li.cards__item
       [:div.card
        [:span.rank
         (inc (- (count bands) (:metal_bands/rank band)))]
        [:iframe
         {:width "320"
          :height "180"
          :src (embed-url (:metal_bands/music_video band))
          :frameborder "0"
          :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
          :allowfullscreen true}]
        [:div.card__body
         [:h3 (:metal_bands/name band)]
         [:div.detail
          [:span.label "Genre"]
          [:span (:metal_bands/genre band)]]
         [:div.detail
          [:span.label "Popularity"]
          [:span (popularity (:metal_bands/popularity band))]]
         [:div.detail
          [:span.label "Recommended Album"]
          [:span
           {:style {:font-style "italic"}}
           (:metal_bands/recommended_album band)]]
         [:p.comment
          (:metal_bands/comment band)]]]])]])

(def content
  [:html
   [:head
    [:title "Live Clojure CGI Script Example"]
    [:meta {:charset "UTF-8"}]
    [:link {:rel "preconnect"
            :href "https://fonts.gstatic.com"}]
    [:link {:rel "stylesheet"
            :href "https://fonts.googleapis.com/css2?family=Metal+Mania&family=Sriracha&display=swap"}]
    [:link {:rel "stylesheet"
            :href "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/atelier-cave-dark.min.css"}]
    [:script
      {:src "https://kit.fontawesome.com/8c366f1e4e.js"
       :crossorigin "anonymous"}]
    [:style (raw-string style)]
    [:style (raw-string guide/styles)]]
   [:body
    [:div#page
     example
     guide/content]
    [:script {:src "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"}]
    [:script {:src "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/languages/clojure.min.js"}]
    [:script "hljs.initHighlightingOnLoad();"]]])

Questions

Yes but why?

Writing code is more than just a language's features and popularity. It's how it all comes together, it's how it feels to do the day-to-day tasks, and the thought and care put into that is what makes Clojure such a fun language to work with. The forms just flow, I'm not buried in docs, and the libraries make use of common data structures that are often inherently compatible with each other. Now hacking together a webapp can leverage Clojure even on the cheapest of hosting providers if they allow cgi-scripts!

Do I have to build a metal genre sampler with this too?

Probably not!

Where can I learn more?

Check out my article to explain the process and rationale at
https://eccentric-j.com/blog/clojure-like-its-php.html

Check out this example repo on github at
https://github.com/eccentric-j/clj-cgi-example

Thanks to

A huge amount of credit goes to Taco for figuring out tools like Clojure, Babashka, and cgi-scripts can be combined. Also thanks to rushsteve1 for making the problem approachable, borkdude for all your work on babashka and support for running it on a shared host, and didibus who helped me debug the first test script.

Doom Emacs Discord

Clojurians Slack

Happy Hacking!

Made byEccentric JPowered by jaunty narcissism