30 Jan 2025
Planet Lisp
Neil Munro: Ningle Tutorial 3: Static Files Middleware
Contents
- Part 1 (Hello World)
- Part 2 (Basic Templates)
- Part 3 (Introduction to middleware and Static File management)
Introduction
Welcome back to this tutorial series, in this chapter we will be looking at the topic of static files
, before we begin, we need to come to an understanding on just what static files
are. Static files are files that do not need to be further processed by your application; images, videos, css, JavaScript files all generally do not need to be processed by your web applications, and thus can simply be served as is. Files that must be processed by your web application (like the templates from the previous tutorial) typically need further processing by your web application and thus are not static files, and could be called dynamic files (although this term isn't really used).
While developing an application there's often a requirement to de-couple these static files
from the main application code, you might want to serve these separately in production and many web servers help you do this for performance reasons (in fact NGINX is very good at this), however you might not need the scalability locally while you are working on your application, and so Ningle has a middleware module to serve these static files when you don't need a dedicated static file server.
Another, more practical consideration of serving static files is that if you don't have a way to serve these files for you, you would have to write a controller for every image, or css file in your application, this wont scale well at all, and you'll spend most of the time writing code to serve files rather than building your application. Static file management allows you to serve a directory of files and reference them by path, but you must set it up correctly in the first place.
Note: We will be using djula
, as we did before, however as of 2025-01-15 this has not yet been merged into quicklisp
, you may need to clone
the latest djula
from github
into your quicklisp/local-projects
directory to gain access to the latest version needed for this tutorial.
Introducing Middleware
In reality Ningle deligates the responsibility of serving static files to the underlying lack
package by way of the lack middleware
. There are a number of different lack
middleware modules available by default and throughout this tutorial we will look at most (if not all) of them.
In most web frameworks (Ningle included) middleware runs between the request
being accepted and the code in your controller running. It is similar to a controller in that it has access to the request
and response
objects, and it may pass its response onto the next middleware function or a controller, it depends on what the middleware function is written to do.
In the case of static files here, the end goal will be that a request for a file will come to your webserver, and the static middleware module will run before any controllers, and if the static resource is found, the middleware function will return a response and with our not-found
method, if the url couldn't be found, our not-found
method runs instead.
Simple Middleware Example
To illustrate how this works in practice, we will write a piece of custom middleware that will add a new variable to the request environment, which we will then extract in a controller and display in a template, we'll use a number that gets incremented each time the middleware is run. In effect we will implement a hit counter in middleware!
Please note: This will not actually be used in the tutorial overall and serves only as a guide for how to write custom middleware generally, please follow this section to complete your understanding and feel free to include it (if you wish), but it will not be featured in the accompanying github code or used anywhere else in this tutorial.
In our main application code we define an app
objects, under this we will define a new variable to track our count.
(defvar *app* (make-instance 'ningle:app))
(defvar *count* 0)
Now in order to take advantage of using middleware we must restructure how we built the ningle
app, you may recall writing a start
function that looked like the following.
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(clack:clackup
*app*
:server server
:address address
:port port))
We will need to edit this and introduce the idea of a lack builder
. This is a way of building an application with more capabilities. Instead of simply passing our *app*
object to the clackup
function, we instead wrap our *app*
object in the lack builder
function which allows us to plug in middleware.
(clack:clackup
(lack.builder:builder *app*)
:server server
:address address
:port port)
It may not be immediately obvious, but where previously the first argument to clackup
was our *app*
object, we instead call lack.builder.builder
passing in *app*
, it is in this builder
call that we will hook in our middleware. Before we can do that however, we must write some middleware!
Above our start function I will write our middleware function:
(defun hit-counter-middleware (app)
(lambda (env)
(setf (getf env :hit-counter) (incf *count*))
(funcall app env)))
This is all it needs, we need to define a function that first accepts a ningle application object, and it returns a function (a lambda
in this instance) that accepts the env
(the request
environment), because there may be a chain of middleware functions that potentially terminate with our controller
, the lambda
must return the result of calling the next middleware function with the app and environment.
Within the body of the lambda
, however, we are free to begin doing whatever we want!
In this example, we only do one thing, we add a new entry into the environment and assign it to be the incremented (incf
) value of *count*
with this line (setf (getf env :hit-counter) (incf *count*))
.
We next must edit the controller
to retrieve this stored value and render it into the template (which means we'll also need to edit the template).
Thankfully editing our controller
is easy, we need only add a new keyword
argument to the render-template*
function.
(setf (ningle:route *app* "/")
(lambda (params)
(let ((user (list :username "NMunro"))
(posts (list (list :author (list :username "Bob") :content "Experimenting with Dylan" :created-at "2025-01-24 @ 13:34")
(list :author (list :username "Jane") :content "Wrote in my diary today" :created-at "2025-01-24 @ 13:23"))))
(djula:render-template* "index.html" nil :title "Home"
:user user
:posts posts
:hit-counter (getf (lack/request:request-env ningle:*request*) :hit-counter)))))
The only addition is the :hit counter (getf (lack/request:request-env ningle:*request*) :hit-counter)
line. This will retrieve the :hit-counter
value from the request
environment.
In our index.html
template, in the div
with the class="container"
, we will add the following:
<div class="row">
<div class="col-12">
<h4>Hits</h4>
<p>{{ hit-counter }}</p>
</div>
</div>
The last thing we must do is return to the lack.builder
section of our start
function and hook the middleware into the app.
(lack.builder:builder #'hit-counter-middleware *app*)
It must be included before *app*
as the hit-counter-middleware
will be wrapping our application and run before anything in our app does. As this tutorial (and your own applications) grow, this line and the different middleware modules will change as requirements do.
If you save and load the project, you should see that there is a div
in your template that updates a count every time the page is refreshed. At this point you may notice that the counter is incremented by 2 each time, this is not a mistake, this is because your web browser will request the page itself, and a favicon.ico
file (and hit the not-found
controller).
For clarity here is the edited main.lisp
file:
(defpackage ningle-tutorial-project
(:use :cl)
(:export #:start
#:stop))
(in-package ningle-tutorial-project)
(defvar *app* (make-instance 'ningle:app))
(defvar *count* 0)
(setf (ningle:route *app* "/")
(lambda (params)
(let ((user (list :username "NMunro"))
(posts (list (list :author (list :username "Bob") :content "Experimenting with Dylan" :created-at "2025-01-24 @ 13:34")
(list :author (list :username "Jane") :content "Wrote in my diary today" :created-at "2025-01-24 @ 13:23"))))
(djula:render-template* "index.html" nil :title "Home"
:user user
:posts posts
:hit-counter (getf (lack/request:request-env ningle:*request*) :hit-counter)))))
(defmethod ningle:not-found ((app ningle:<app>))
(declare (ignore app))
(setf (lack.response:response-status ningle:*response*) 404)
(djula:render-template* "error.html" nil :error "Not Found"))
(defun hit-counter-middleware (app)
(lambda (env)
(setf (getf env :hit-counter) (incf *count*))
(funcall app env)))
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(clack:clackup
(lack.builder:builder #'hit-counter-middleware *app*)
:server server
:address address
:port port))
(defun stop (instance)
(clack:stop instance))
Understanding how to write custom middleware is very important, and I hope that this has served as a good foundation, however, as mentioned at the beginning of this section we will not be using this piece of custom middleware in our application. You are free to include it if you wish, but it will not feature in the companion code in github.
Aceesslog Middleware
Now that we have discussed what middleware is, work it does, how it works, and how to implement it, we will look at some of the middleware modules included in lack
which ningle
therefore has access to.
We will start with what is known as accesslog
middleware, it's a very simple piece of middleware that just logs requests as they come in.
As we did in the previous section, we must adjust the lack.builder
line, however, this time we do not need to write any function, the middleware that comes with lack
uses some simplified syntax.
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(clack:clackup
(lack.builder:builder :accesslog *app*)
:server server
:address address
:port port))
If you recompile and run your application, and view the terminal output, you will see information about the incoming requests to the web server.
This is a simple example, but it highlights an important distinction that the bundled lack
middleware isn't a reference to a function
, it's a keyword
, as we will see in the next section, they can be a little more complicated than just a keyword
, but this particular piece of middleware, it is just a keyword
to turn it on. Other pieces of middleware may be a list
that include configuration options, if needed.
Static Files Middleware
What we would like to do, when we write our templates is to be able to tell our template that a file is a static file and must be served from the static location. We will need to use a special djula
tag to inform our templates that a file is a static file, which may seem a little counter intuitive, however, if for some reason we need to change where static files are served from (for example we may initially host them locally, but then switch to s3 or cloudflare or something), we'd have to go through all our templates changing the url, whereas using static file middleware, we'd set up a base once, and if we need to change it, we change it in one place and then our templates wouldn't need to change at all.
While this sounds like a lot of work, remarkably, it isn't!
There's only really three steps to setting up static file handling in Ningle!
As we are using djula
(and a reminder quicklisp
may not yet have the latest version of djula
, you may need to use git to clone it into your quicklisp/local-projects
), we must configure djula
to be aware of where our static files will be mounted. So, just as we added a template directory, we must also add a static directory, in our example this is in the start
function:
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
This second line is the one we have added, when we use the static
tag
later on, it will know to use "/public/" as our static path.
NOTE: Be mindful to ensure there's a trailing slash when calling set-static-url
!
The second thing we must do is hook into the lack
static middleware.
(lack.builder:builder :accesslog
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
Mentioned previously, some middleware setup will be lists, in this instance, a list where the first item is a keyword
naming the lack
middleware module to use (this will be a common pattern with other lack
middleware) and then any arguments that the middleware module uses. In this case, we need to define where on our host file system we will be storing our static files, this is the :root
argument and we specify that relative to our project, static files will be stored in /src/static
and we need to have these mounted on a path
which is exactly what the :path
argument does, we will hide the physical location of our static files (good security) and state that they're available behind "/public/"
.
For clarity, this is what the start function should look like:
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder :accesslog
(:static
:root (asdf:system-relative-pathname :ningle-tutorial-project "src/static/")
:path "/public/")
*app*)
:server server
:address address
:port port))
The final thing we need to do is, in our templates, use the static tag to load a given static file. In the base.html
file, you might want to display an image. You can use whatever image you like, but if you want to use the one I've created, you can use this.
You should put this file (or the image of your choice) in the src/static/images/
directory (and create it, if you have not), I have called the image logo.jpg
and have stored it in src/static/logo.jpg
. This will exposed it as /public/images/logo.jpg
and from here we can place these into our templates.
<img src='{% static "images/logo.jpg" %}' alt='Logo'>
If you save, reload, and view this project in your web browser, you should see the image rendered as you might expect. Inspecting the page you will see that the src
attribute will be src="/public/images/logo.jpg"
. The image is being served without writing having to write a controller, and is served from the root you defined.
Tidying Up
Now that we have the ability to serve images, css etc, we might want to take this time to writing some css (although I personally hate writing CSS), and making the site look good. Although it is beyond this tutorial to teach bootstrap
or other css
frameworks (although I will use bootstrap), I will be using bootstrap to make my site look a little nicer, you can refer to the github code to see exactly what I have done regarding frontend
styling.
There is something I will do to help our application look a little better...
I will create a nicer looking error page that will take advantage of our new staticfiles
middleware, so the contents of src/templates/error.html
will be:
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>{{ error }}</h1>
<img src="{% static "images/lua.jpg" %}" alt="A picture of a dog looking sad and confused" class="error-404">
</div>
</div>
</div>
{% endcontent %}
I will save this photo to src/static/images/lua.jpg
.
And in the main.lisp
file, I will modify the not-found
method:
(defmethod ningle:not-found ((app ningle:<app>))
(declare (ignore app))
(setf (lack.response:response-status ningle:*response*) 404)
(djula:render-template* "error.html" nil :error "Not Found"))
I have also edited the controller
for the index page:
(setf (ningle:route *app* "/")
(lambda (params)
(let ((user (list :username "NMunro"))
(posts (list (list :author (list :username "Bob") :content "Experimenting with Dylan" :created-at "2025-01-24 @ 13:34")
(list :author (list :username "Jane") :content "Wrote in my diary today" :created-at "2025-01-24 @ 13:23"))))
(djula:render-template* "index.html" nil :title "Home"
:user user
:posts posts))))
In my frontend I have edited the html to include a created-at
attribute to the posts and included it as we did before with the post author and content:
<h5 class="card-title">{{ post.author.username }}</h5>
<p class="card-text">{{ post.content }}</p>
<p class="text-muted small">Posted on: {{ post.created-at }}</p>
The exact styling I leave up to you, but I wanted to be clear that there is a small content change to the html.
Conclusion
To recap, after working your way though this tutorial you should be able to:
- Describe what static files are.
- Describe what application middleware is.
- Explain why it is advantagous to handle static files differently.
- Explain how middleware works.
- Create and use simple middleware functions.
- Incorporate lack static middleware into your application.
- Incorporate djula static tags in your html templates to serve static content.
Github
The link for this tutorial is available here.
Resources
- Clack
- class
- div
- djula
- djula documentation
- favicon
- function
- getf
- incf
- keyword
- quicklisp
- setf
- Lack
- ningle
- format
- lambda
- middleware
- NGINX
30 Jan 2025 11:55pm GMT
27 Jan 2025
Planet Lisp
Zach Beane: Maxima in the browser with ECL and wasm
Via Raymond Toy on the ecl mailing list, Maxima in the browser.
27 Jan 2025 4:37pm GMT
18 Jan 2025
Planet Lisp
Joe Marshall: Valid Use Case for Copilot
Our compay proides us with GitHub copilot, which is yet another example of an "AI" engine. I've reviewed it before and I wasn't particulary impressed. It writes mediocre code.
However, I found a use case where it really shines. I was setting up the blank project repositories for our upcoming hackathon. This involved creating a bunch of otherwise empty repositories with a customize README.md file. The simple solution was to automate this with an ad hoc shell script.
I don't make a practice of writing shell scripts. I write a few every now and then, but I don't live and breathe bash
. It takes me a while because I forget the flags. I have to look up the exact syntax for things like the find
command. Then I have to fight with the shell to get the quoting and escaping right. It's a tedious chore. No doubt I would become adept at it if it were something I did daily, but it's not.
So I decided to ask GitHub copilot to write some scripts for me. I gave it my problem statement: start with a file with a list of repository and description pairs. For each pair, look for the subdirectory with that name and descend into it. Use the sed command to modify the README.md file in the subdirectory to add the repository discription. Then use git to commit and push the change.
Copilot gave this a couple of seconds thought and came up with this script:
while IFS= read -r line; do dir_name=$(echo "$line" | cut -d' ' -f1) description=$(echo "$line" | cut -d' ' -f2-) if [ -d "$dir_name" ]; then sed -i "s/\[\[github_repo_description\]\]/$description/g" "$dir_name/README.md" else echo "Directory $dir_name does not exist." fi done < descriptions.txt find . -type d -name "hack*" -exec sh -c ' cd "$0" if [ -f README.md ]; then git add README.md git commit -m "Update description." git push fi ' {} \;
It isn't a script that'd I'd write home about, but it got the job done. I ran the script and had the repositories set up in couple of minutes. I could have written the script myself, but it would have take me longer. I had forgotten the options to the find
command. I had forgotten the arguments to the cut
command. I would have had to look them up. Copilot saved me that time.
A co-worker of mine questioned the engineering tradeoff of using a resource hog like generative AI to write crappy, throwaway shell scripts. From the standpoint of an indiviual developer, though, this is the first use case for copilot that I've where it actualy saved me time and effort.
18 Jan 2025 5:38am GMT