Rendering index.html on any route with Reitit in Clojure
September 20, 2023It's a common practice for Single Page Applications (SPAs) to render the index.html
page for any route on the server to establish the basic frontend setup. In this article, we'll explore how to achieve this in Clojure using the Reitit router.
The simplest approach is to handle /*
with a resource handler pointed to index.html
. While this approach works well, it can potentially lead to conflicts in routes due to the default wildcard route. So, we have to use :conflicts nil
router option. Route "/health"
is added just for illustration purpose.
(ns myprj.router
(:require [ring.util.response :as ring-response]
[reitit.ring :as ring-reitit]))
(defn handler
"Return main application handler."
[]
(ring-reitit/ring-handler
(ring-reitit/router
[["/health" {:name ::health-check
:get {:handler (fn [_] (ring-response/response "OK"))}}]
["/*" {:name ::index
:get {:handler (fn [_]
(-> "index.html"
(ring-response/resource-response {:root "public"})
(ring-response/content-type "text/html")))}}]]
{:conflicts nil})))
Fortunately, in Reitit, there's a way to add a defalut route without encountering conflicts. For instance, you can utilize existing create-resource-handler
, intended to render files from the resources directory of the project. It even has an :index-files
parameter where you can specify the path to index.html
. However, it redirects to /index.html
on every request and (as far as I know) there is no way to avoid it.
To render index.html
on any request to the server without redirection, we can reuse a handler from above wrapped as a common handler:
(ns myprj.util.handler
(:require [ring.util.response :as ring-response]))
(defn create-index-handler
"Create a handler to render index.html on any request."
([]
(create-index-handler {}))
([{:keys [index-file root]
:or {index-file "index.html"
root "public"}}]
(letfn [(index-handler-fn
[_request]
(-> index-file
(ring-response/resource-response {:root root})
(ring-response/content-type "text/html")))]
(fn
([request]
(index-handler-fn request))
([request respond _]
(respond (index-handler-fn request)))))))
Now you can use this handler as follows:
(ns myprj.router
(:require [ring.util.response :as ring-response]
[reitit.ring :as ring-reitit]
[myprj.util.handler :as util-handler]))
(defn handler
"Return the main application handler."
[]
(ring-reitit/ring-handler
(ring-reitit/router
[["/health" {:name ::health-check
:get {:handler (fn [_] (ring-response/response "OK"))}}]])
(ring-reitit/routes
(util-handler/create-index-handler))))
If you have any other common resource handlers, be sure to place them before the index handler in your setup.