DEV Community

Rajasegar Chandran
Rajasegar Chandran

Posted on

Using TailwindCSS in Common Lisp web apps without Node.js tooling

In this post, we are going to take a look at how to use TailwindCSS like utility class CSS library in a Common Lisp web application without using any Node.js or JavaScript tooling.

I have already written a post about using TailwindCSS in Common Lisp, but the tooling setup is kind of messy. You need to have Node.js installed in your machine. You need to install additional npm libraries and setup some config around it to make Tailwind CSS work in your Common Lisp web applications.

But in this post, we are going to look at a cleaner and simple way to use the CSS utility classes in a Common Lisp web application using the native platform capabilities of Common Lisp and its tooling ecosystem.

cl-djula-tailwind

cl-djula-tailwind is a Common Lisp package which helps you to use TailwindCSS classes in your Djula templates without any JavaScript or Node.js tooling

Create a new Caveman project

We are going to create a new Caveman project in Common Lisp. So fire up your favourite Lisp interpreter and type the below commands to setup a new Common Lisp web application project using Caveman.

(ql:quickload :caveman2)
(caveman2:make-project #P"~/quicklisp/local-projects/cl-tw-demo")
Enter fullscreen mode Exit fullscreen mode

If you want to know more about building Common Lisp web applications you can refer to my previous post.

Now we need to add cl-djula-tailwind as system dependency for our newly created project. Open your system definition file for the project called cl-tw-demo.asd and add cl-djula-tailwind to the :depends-on section.

(defsystem "cl-tw-demo"
  :version "0.1.0"
  :author "Rajasegar Chandran"
  :license ""
  :depends-on ("clack"
               "lack"
               "caveman2"
               "envy"
               "cl-ppcre"
               "uiop"

               ;; for @route annotation
               "cl-syntax-annot"

               ;; HTML Template
               "djula"

               ;; for DB
               "datafly"
               "sxql"

        ;; Djula Tailwind
        "cl-djula-tailwind")
  :components ((:module "src"
                :components
                ((:file "main" :depends-on ("config" "view" "db"))
                 (:file "web" :depends-on ("view"))
                 (:file "view" :depends-on ("config"))
                 (:file "db" :depends-on ("config"))
                 (:file "config"))))
  :description ""
  :in-order-to ((test-op (test-op "cl-tw-demo-test"))))

Enter fullscreen mode Exit fullscreen mode

Injecting the CSS in templates

Add a placeholder for the stylesheet generated by cl-djula-tailwind in the default template templates/layouts/default.html. We also need to add the safe filter to the tailwind variable to mark it string as not requiring further HTML escaping prior to output. Otherwise Djula will try to escape or encode the quotes and other symbols in the generated css. More information about the safe filter can be found in the Djula documentation

<!DOCTYPE html>
  <html class="h-full bg-gray-100">
<head>
  <meta charset="utf-8">
  <title>{% block title %}{% endblock %}</title>
  <link rel="stylesheet" type="text/css" media="screen" href="/css/main.css">
    <style>{{ tailwind | safe }}</style>
</head>
Enter fullscreen mode Exit fullscreen mode

Generate the stylesheet

In order to generate the stylehsheet for our route templates, we are going to call the default exported function get-stylesheet from cl-djula-tailwind and give it the template name and the template directory as arguments.

And we need to set the value for the template variable tailwind which we have already used in the default layout template previously. This variable is going to be modified for each and every route, since the CSS for every route is going to be generated on the fly and sent to the template. So we need to make use the *default-template-arugments*.

In Djula, you can use the *DEFAULT-TEMPLATE-ARGUMENTS* variable to store arguments that will be available for all templates. It is a plist, so use getf to add arguments, like this:

(setf (getf djula:*default-template-arguments* :tailwind) 'some-value)
Enter fullscreen mode Exit fullscreen mode

And now, you can access {{ tailwind }} in your template.

This is useful when you have many templates that rely on the same set of variables. In our case the variable is the generated stylesheet using Tailwind classes.

You can find more information about default template arguments in the Djula documentation

Now define a new function called render-stylesheet in src/web.lisp like below

(defun render-stylesheet (template)
    (setf (getf djula:*default-template-arguments* :tailwind) (cl-djula-tailwind:get-stylesheet template *template-directory*)))
Enter fullscreen mode Exit fullscreen mode

Routes

The next step is to tell our web application to render the generated stylesheet into our templates. Call the render-stylesheet function in your routes with the template name as the argument before the standard render function of Djula.

It is important that we need to call this function before render because we need to ensure that the generated stylesheet content is set in the default template arguments for Djula.

(defroute "/" ()
  (render-stylesheet #P"index.html")
  (render #P"index.html"))
Enter fullscreen mode Exit fullscreen mode

See the demo app in action

Now load our demo app and start the server to see the web app in action to look at how our Tailwind CSS generated on the fly works

(ql:quickload :cl-tw-demo)
(cl-tw-demo:start :port 3000)
Enter fullscreen mode Exit fullscreen mode

Below are some of the sample screenshots of the demo app.

Image description

Image description

How it works

Let's see how this thing works. Whenever you request a page in your web application, Djula will try to read your route template and compile it and then send the HTML response to the browser. This is how normally things work in a Caveman application. On top of this what the cl-djula-tailwind package does is, it also parse the Tailwind class names used in your templates, layout templates and partials used in your markup and then construct a minified CSS class definitions list and put them in a placeholder in the default layout template through a <style> tag so that it will be automatically picked up by the browser and your HTML content is properly formatted according to the CSS classes you have used.

The CSS is generated using another Common Lisp package called cl-css and minified using cl-minify-css. The class names from the template files are parsed using cl-ppcre using various regular expression patterns for different class names ranging from simple, responsive, pseudo-classes, dark mode and so on.

So whenever every route is rendered, you will get a generated custom CSS on the fly injected via internal stylesheets. The beauty of this approach is that you will only get the minimal and required CSS for a particular route instead of getting all the CSS at once for your whole application. From a performance standpoint this is a huge gain from the traditional approach of delivering all your CSS beforehand. Because the critical rendering path can be optimized to a large extent by delivering only the required CSS for a web page.

Using the CSS Overview extension in Chrome browser, I have collected some stats of the unused CSS. Below is the data of the TailwindCSS website

Image description

You can still see some unused CSS with the conventional approach. And using cl-djula-tailwind in our demo app, this is the result

Image description

With our new approach of sending only the required CSS, there are no unused CSS at all.

Source code

The source code for the package cl-djula-tailwind is hosted in Github here.

Demo

There is also a demo app making use the cl-djula-tailwind package to test and improve the package. It is hosted in Github here

What's next?

The cl-djula-tailwind package is still in beta stage and not yet ready for production. But you can play around with it. Most of the util classes used in Tailwind are covered, but still there is some work needs to be done to get it production ready.

Below are some of the things that need to be taken care of:

  • Provide a capability for Tailwind CSS like config
  • Cover all the utilities in Tailwind
  • Handle deeply nested partials in Djula templates (at present it can handle only top-level partials in your route templates)

If you find anything that is missing from Tailwind, that can be made part of cl-djula-tailwind, please raise an issue at the repository.

Hope you enjoyed this Common Lisp tooling and the ability to generate CSS util classes on the fly for your web pages. Please let me know for any queries or feedback in the comments section.

Top comments (0)