the first template

Our route only returns a string:

(easy-routes:defroute root ("/") ()
    "hello app")

Can we have it return a template?

We’ll use Djula HTML templates.

Usually, templates go into their templates/ directory. But, we will start out by defining our first template inside our .lisp file:

;; scr/myproject.lisp
(defparameter *template-root* "
<title> Lisp web app </title>
<body>
  hello app
</body>
")

Compile this variable as you go, with C-c C-c (or call again load from any REPL).

Now come back to the root route. We have to change it. Instead of “hello app”, we use Djula to:

  • compile our string to a Djula template: djula:compile-string
  • render the template: djula:render-template*

So let’s do it:

(easy-routes:defroute root ("/" :method :get) ()
  (djula:render-template*
   (djula:compile-string *template-root*)
   nil))

The nil argument means “return a string” (as in (format nil …)).

Templates are most useful when they do conditional logic, inherit from other templates and are given arguments. So, how could we render a list of products?

Render products

The Djula iteration tag is {% for … in … %} … {% endfor %}. Shall we try it to display a list of products?

As a reminder: our products can be retrieved with (products).

How well do you know HTML? Nothing’s fancy here. It’s actually boring technology.

(defparameter *template-root* "
<title> Lisp web app </title>
<body>
  <ul>
  {% for product in products %}
    <li> {{ product.1 }} - {{ product.2 }} </li>
  {% endfor %}
 </ul>
</body>
")

re-compile the variabe when done (C-c C-c).

Do you wonder what’s product.1? Recall that each product is a list of 3 three elements, so we access the element 1 of a product, which is its title. If our product had been a hash-table with a title key, we could have written {{ product.title }}. If the product had been an object with a title slot, we could have written the same. Djula makes it easy for us to access object properties.

How can we tell Djula to take a list of products as parameter? We simply use :key arguments, like this:

;; in the root route
(djula:render-template*
   (djula:compile-string *template-root*)
   nil
   :products (products))   ;; <----- added

you can compile the route again and refresh the page at localhost:8899/.

You should see a list of products! Like so (with proper HTML bullet points):

- Product nb 0 - 9.99
- Product nb 1 - 9.99
- Product nb 2 - 9.99
- Product nb 3 - 9.99
- Product nb 4 - 9.99

Cool!

A first refactor

We have a very cool route:

(easy-routes:defroute root ("/" :method :get) ()
  (djula:render-template*
   (djula:compile-string *template-root*)
   nil
   :products (products)))

To test it and see its output, we had to re-compile it (OK), and refresh our browser. ARGH! We can do better. It may not look obvious now, but we are already writing business logic inside a web route. We should extract as much logic as possible from the route. It will make everything so much easier to write and test in the long run.

My point here is that we have one application function: rendering products. We can have a function for this:

(defun render-products ()
  (djula:render-template*
   (djula:compile-string *template-root*)
   nil
   :products (products)))

(easy-routes:defroute root ("/" :method :get) ()
  (render-products))

The benefit is that you can run (render-products) by itself (and very quickly with C-c C-y M-x slime-call-defun) to test it in the REPL, and see the HTML output.

CL-USER> (in-package :myproject)
#<PACKAGE "MYPROJECT">
MYPROJECT> (render-products)
"
<title> Lisp web app </title>
<body>
  <ul>

    <li> Product nb 0 - 9.99 </li>

    <li> Product nb 1 - 9.99 </li>

    <li> Product nb 2 - 9.99 </li>

    <li> Product nb 3 - 9.99 </li>

    <li> Product nb 4 - 9.99 </li>

 </ul>
</body>
"

Now you do you, but you’ve been warned ;) Lisp makes small refactorings easy, so take advantage of it. And keep in mind to separate your application logic from the web shenanigans.

That being said, let’s move on. We’ll create a page to see each product and we’ll have more fun with a search form.

Whole file

This is our myproject.lisp file so far:

(in-package :myproject)

(defvar *server* nil
  "Server instance (Hunchentoot acceptor).")

(defparameter *port* 8899 "The application port.")

(defparameter *template-root* "
<title> Lisp web app </title>
<body>
  <ul>
  {% for product in products %}
    <li> {{ product.1 }} - {{ product.2 }} </li>
  {% endfor %}
 </ul>
</body>
")

(defun products (&optional (n 5))
  (loop for i from 0 below n
        collect (list i
                      (format nil "Product nb ~a" i)
                      9.99)))

(defun render-products ()
  (djula:render-template*
   (djula:compile-string *template-root*)
   nil
   :products (products)))

(easy-routes:defroute root ("/") ()
  (render-products))

(defun start-server (&key (port *port*))
  (format t "~&Starting the web server on port ~a~&" port)
  (force-output)
  (setf *server* (make-instance 'easy-routes:easy-routes-acceptor
                                :port (or port *port*)))
  (hunchentoot:start *server*))