Creating ToryAnderson.com with ShadowCLJS
Table of Contents
After years of a languishing example of The Cobbler’s Children , I finally got around to renovating my own website: toryanderson.com. Admittedly I have a lot to learn about visual design, but I had a good time on the technical front. I took the opportunity to use technologies I love and have been pleased with the result – both in the product and the cost constraints. In other words, it was fun to build and cheap to host: Clojure[script] meets shared hosting.
The full code of the live site is available at https://gitlab.com/toryanderson/toryanderson.com , and the version matching this blog post is on the branch at https://gitlab.com/toryanderson/toryanderson.com/-/tree/blog .
Constraints & Motivation
My desires and technical targets provided some constraints, partly to do with having common inexpensive shared hosting (the kind you see with basic Namecheap, Hostgator, Bluehost, GoDaddy, etc).
- Shared hosting: no sudo, only PHP for server-side rendering
- Sandboxed Apache config with .htaccess capabilities
- I desired a no-database solution
- I desired a client-side application (SPA) instead of server-side (SSR) since my only SS language available was PHP
Technical Features
-
Shadow-CLJS makes for the best pure-Clojurescript development I’ve dealt with and, together with Reagent, makes it simple to develop SPAs.
-
For a personal website, unadorned Reagent makes an appropriate solution; although my work usually needs re-frame, there is no reason for that degree of state-management here. Reagent just allows for nice hot-loading dev experience, as well as being part of the CSS and Accordion solutions below.
-
CSS-in-[CL]JS
This project gave me opportunity to employ Garden for CSS-in-JS, maximizing a functional data-driven approach to styling to complement Bulma. I explain my approach more here.
-
Client-side XML-Parsing
For another upcoming project I need to take a swing at javascript XML parsing, so I thought I’d use my site as an opportunity to learn the way of it by aggregating four of my blogs onto my front page. This brought some revelation about the benefit of a data-oriented approach to XML parsing. Check out the code for the xml here and its invocation here.
-
Accordions
I knew from past experience that there is no need to search for an additional library to achieve expandable accordions in a Clojurescript site. That solution is detailed here.
-
Reitit-Frontend for the routing
I performed my frontend routing with Reitit-Frontend. This is possibly overkill for a personal website with less than a handful of pages, but it is exactlyh what will be needed on more elaborate webapps and what we use on our larger apps for work, so learning to use it with shadow.cljs was useful. However, unlike our larger apps, I was content to use vanilla reagent since re-frame was just way too much for a little thing like this. The important thing to understand here is the model: reitit works by instrumenting vector-map pairs. In my app they are simple: the first item in a vector is a string matching a requested route, and the the second item is map containing information necesary for the dispatch. If the second item were another vector, instead of the map, we’d have a deep directory structure (like
toryanderson.com/a/b
. I don’t need any of those here, though.
More on Frontend Routing
It’s helpful to understand how frontend routing works. This is routing that is handled by your browser: the javascript that is our app checks the URL in the browser and decides what to do based on what it finds there. For this to work, the javascript needs to actually be loaded, which means whatever kind of server you have (mine is the simplest standard Apache deal) serves a bare template that includes at least my app.js (which is what shadowcljs transpiles from my Clojurescript) and a div with an id of “app”, which is where I’ve instructed reagent (re: react.js) to mount itself and start working. I am also mounting two other locations, the header and the footer. In some apps it’s useful to also have a “modal” item. The reason for this frontend routing is that it front-loads (haha. Pun only slightly intended) the load time so that once the user is done with the initial payload, navigating the site will be crisp and fast (besides any additional downloads that are needed). Take care, though, because that speed is due to new views being created when you are navigating by clicking on stuff and it might not be the same result if you hit “refresh” on any of these pages, which causes your browser to issue a new request to the server and effectively jettisons your already-loaded app.
-
URL Fragments
When I first started with SPAs, I was taught what was referred to as the “URL Trick,” more accurately referred to with “URL Fragments.” This trick would use the HTTP syntax ending in
/#/some-location
, where the/#*
part causes the browser to look for a named anchor like<div id
"some-location">= and, if it doesn’t find it, it just renders the base URL before the#
part. This guarantees something will appear and gives the javascript enough to read and decide how to route. This is the easiest way to set things up. It looks more professional not to use the “trick”, though (not to mention it plays havoc with search engines, which can’t parse into JavaScript-instrumented URL fragments); it just takes a little more work on the server side of things to go without fragments. In this case I copied myindex.html
– remember, that’s the one that loads my app.js code and sets the stage – into an index.html under each of my sub-pages. Then, by requiring each path to actually end in a/
, they load without any#
in the URL. A side effect of this is that I could now use anchors for navigating in-page if I so desired, since they wouldn’t collide with the router.On toryanderson.com site I have only four routes, including my base route. None of the paths are deep. I need to conclude each with a
/
in order to let the Apache server handle refresh without issue in addition to the js handler, which doesn’t care about details of the URLS. See them here: routes.cljs.(def routes (rf/router ["/" ["" {:name ::toryanderson :view #'main/toryanderson}] ["cv/" {:name ::cv :view #'cv/render}] ["contact/" {:name ::contact :view #'contact/render}] ["portfolio/" {:name ::portfolio :view #'portfolio/render}]]))
:name
is needed by reitit-frontend in order to allow you to generate href to a route without hardcoding (great for avoiding mistypes and accommodating later refactorings).:view
is just something I made up, which gets put on the route when matched and is handled by my router:(defn init-routes! "Start the routing" [] (rfe/start! routes (fn [m] (reset! current-view (get-in m [:data :view]))) {:use-fragment false}))
This has three parts:
- It loads up the vectors and maps I called
routes
. Each route string ends in/
, like"cv/"
. This causes the routes the SPA recognizes to be the same as those the server will give to refreshes or copy-pasted URLs. - It has a function, which receives any particular route I called
m
, and says what to do with it. It grabs the:view
key I put on it above and sticks it in thecurrent-view
reagent atom, which triggers a view re-render and so implements the changing pgae. - It takes a map of settings; the only one I’m using tells it not to
:use-fragment
and instead expect a normal-looking/URL
.
To finish this picture, here is how we’ve instrumented the actual display and chancing of views in pure
reagent
:-
toryanderson.core
The entry-ns for the [cl]js app is toryanderson.core, with the whole code below:
(ns toryanderson.core (:require [reagent.core :as r] [toryanderson.views.styles :as style] [toryanderson.routes :as routes] [toryanderson.views.base :as base] [toryanderson.views.components.nav :as nav] [toryanderson.views.message :as message])) (defn mount-components [] (r/render-component [#'nav/navbar] (.getElementById js/document "tsa-nav")) (r/render-component [#'base/main-view] (.getElementById js/document "app")) ;; our primary mount location, into which our main content goes. Mounts into our index.html's "<div id=app>" (r/render-component [message/message-view] (.getElementById js/document "message"))) (defn init! [] (style/mount-style (style/toryanderson)) ;; for all my css-in-js (routes/init-routes!) ;; start the router and recognize the routes (mount-components) ;; Connect our app. Now we're ready to go. )
-
view.base
We saw this mounted into the
div id=app
page item by the core. Here is the actual component: it gets the:view
function that the routes put into ourcurrent-view
reagent atom and simply fires it. Since it’s a reagent-atom, it will reload whenever it changes, hence making single-page app navigation.(ns toryanderson.views.base "Container of the main view which facilitates CLJS app re-rendering and routing." (:require [toryanderson.routes :refer [current-view]])) (defn main-view "Placeholder to render the main view" [] (let [cfn @current-view] (cfn)))
- It loads up the vectors and maps I called
-
The actual file structure
To make this work, note my index files (the default thing served at URLs in most servers) under each directory:
.: fontawesome-all.min.css index.html manifest.edn webfonts ./contact: index.html ./cv: index.html ./portfolio: index.html
They are each identical, each containing the
<div id=app>
and the loading of my apps json, css, etc. This way people navigating to any of these routes will still work, irrespective of what kind of navigating they are doing.
Development Process
Most of the development was reading up and learning the various components. Coding itself was as great as hotloading dev is (other of my projects use Figwheel, but for me the Shadow-CLJS experience was the same). REPL development was an important addition as I performed data exploration with the XML parsing, allowing close feedback as I stepped through the XML payloads and determined the right data-queries.
I am an emacs user so just fired up the superb Cider, instrumented with flycheck-joker, and got going. Simply starting a Clojurescript process from within one of my files using C-x C-j s
(cider-jack-in-cljs
) recognizes that I’m in a shadow.cljs project and connects appropriately, even offering to open up a browser to the site.
Deployment and Publication Process
-
shadow-cljs.edn
Instead of a Leiningen
project.cljs
, shadow uses ashadow-cljs.edn
more after the manner of deps.edn. It’s gloriously short for my app:https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/shadow-cljs.edn
;; shadow-cljs configuration {:source-paths ["src/dev" "src/main" "src/test"] :dependencies [[reagent "0.8.1"] [cljs-ajax "0.8.0"] [metosin/reitit-frontend "0.3.9"] [garden "1.3.9"] [cljs-bean "1.6.0"] [hickory "0.7.1"] [org.clojure/data.xml "0.2.0-alpha6"]] :dev-http {8080 "target/"} :builds {:app {:output-dir "target/" :asset-path "/" ; was . :target :browser :modules {:main {:init-fn toryanderson.core/init!}} :devtools {:after-load toryanderson.core/init! :http-root "target" :http-port 8080}}}}
I copied most of this from another project without worrying about it. The one “gotcha!” I had was when I found that my sub-directory routes all failed when loaded directly (not via in-app navigation) because they were trying to load javascript resources relative to any directory it was in, so it would try to find dependencies under
/cv/*
if I was trying to loadtoryanderson.com/cv/
. This traced back to the:asset-path
key, which had been"."
meaning “here”. When I fixed it to"/"
everything worked great. -
deploy.sh
Shadow-CLJS has a deployment option that outputs code suitable for immediate uploading to my shared hosting provider. I added a quick script to run whenever I want to place it on my server:
#!/bin/bash shadow-cljs release :app && rsync -av target/ torys:www/toryanderson echo "Deployment process finished. Visit https://toryanderson.com to verify."
Simple as that. Have shadow-cljs make a release, rsync the target directory straight to my server, and done. Apache will serve it just fine and you see the results at https://toryanderson.com .
Output
Shadowcljs actually runs the magical Closure :advanced optimizations by default when you tell to to produce the release – perfect since I’m only using Clojure, not Javascript, libraries and they are all automagically converted to Closure-compatible code. Final result: 606K
, which includes (according to my usage) the entire Clojurescript language, the Google Closure code I’m using for accordions, my inline styles, and the XML library I use. As I add more content and pages I would expect this to grow slowly since the bulk here will be Clojurescript itself. Note that the Clojurescript 1.10.773 core js file is 1.3M
when pulled out of the Maven repo, so I can rest assured that Closure is doing its magic.
Resources
- https://gitlab.com/toryanderson/toryanderson.com Live-version of toryanderson.com
- https://github.com/thheller/shadow-cljs shadow-cljs, a great way for pure ClojureScript programs
- http://day8.github.io/re-frame/ re-frame, not used here, Clojurescript/React state-management for more serious applications
- https://tech.toryanderson.com/2020/03/14/my-garden-css-has-ascended/ Post on css-in-cljs
- https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/src/main/toryanderson/views/main.cljs#L70 Entry Code for XML-parsing
- https://tech.toryanderson.com/2020/09/13/accordions-out-of-the-box-with-clojurescript-and-closure/ Post on Closure accordions in Clojurescript
- https://cljdoc.org/d/metosin/reitit/0.5.5/doc/frontend Official site for reitit frontend for routing
- https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/src/main/toryanderson/routes.cljs TSA Code implementing router
- https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/src/main/toryanderson/core.cljs TSA Code for the entry point of my app
- https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/target/index.html#L12 HTML including the required mount-point
- https://github.com/candid82/flycheck-joker Emacs integration with the Joker Clojure[script] linter and warning system
- https://clojure.org/reference/deps_and_cli Official deps.clj, a dependency loader some use instead of Leiningen
- https://gitlab.com/toryanderson/toryanderson.com/-/blob/blog/shadow-cljs.edn my deps-like shadow-cljs file defining my project
- https://search.maven.org/search?q=g:org.clojure%20AND%20a:clojurescript Maven’s download for the Java package of Clojurescript itself
Comments?
As always, comments and suggestions are more than welcome.