This is a short guide to setting up a lisp-powered website with Hunchentoot and Talcl/Buildnode. Hunchentoot is a web server, Talcl is a templating system, and Buildnode is a CXML helper library Talcl uses. These are from notes I made while writing an app to help my wife record attendance and student progress for dance classes.
I’m calling this project “Alice”, so time to make the foundation:
(quickproject:make-project "~/lisp/alice/" :depends-on '(:iterate :alexandria :talcl :hunchentoot :buildnode))
I generally always include iterate and alexandria, and we’ll want a few things from buildnode directly so we’re depending on that separately from talcl. Quickproject makes all my files, and I’m good to start.
Here’s the basic goal:
I want to have my templates stored in .tal files, and hunchentoot will need a place to look for static files, so we start with a few new directories: “www” for hunchentoot and “templates” for tal. To easily get paths to these, I added a helper function:
(defun resource-path (path) "looks up path relative to whereever this asdf system is installed. Returns a truename" (truename (asdf:system-relative-pathname :alice path)))
Now the fun begins. Next is a function to start the hunchentoot acceptor (which will handle listening on a port and dispatching requests) AND configure the static file handling I wanted.
(defvar *acceptor* nil "the hunchentoot acceptor") (defun start-server (&optional (port 8888)) (setf *acceptor* (make-instance 'hunchentoot:acceptor :port port)) ;; make a folder dispatcher the last item of the dispatch table ;; if a request doesn't match anything else, try it from the filesystem (setf (alexandria:last-elt hunchentoot:*dispatch-table*) (hunchentoot:create-folder-dispatcher-and-handler "/" (resource-path "www"))) (hunchentoot:start *acceptor*))
By having the folder-dispatcher-and-handler as the last item in hunchentoot’s *dispatch-table*, it will only bail to the filesystem if no other handlers match. Hunchentoot has a *default-handler* mechanism, but it is limited; default-handlers do not have access to any request information.
Now I toss a stub style.css into my www/ directory, call start-server, then can load http://localhost:8080/style.css in my browser. Great, the right half of my desired flowchart is done. Now the Talcl part.
Talcl reads template files, compiles them into lisp functions that accept a tal enviroment. The tal environment is a set of key/value pairs that will fill in the templates. Talcl has a bunch of features to handle writing to streams, but for now I’ll just generate strings and pass them to hunchentoot.
(defvar *tal-generator* (make-instance 'talcl:caching-file-system-generator :root-directories (list (resource-path "templates"))))
The tal generator maps template names to template files, compiling the templates if needed. There are a few different classes that can be used here, but this one checks file dates and recompiles only if the file is newer.
(defun render-page (template &optional tal-env) "renders the given template" (talcl:document-to-string (buildnode:with-html-document (talcl:tal-processing-instruction *tal-generator* template tal-env))))
This helper function takes the template name and the optional tal environment, and returns a string of the final output. Talcl deals in XML, but HTML is not XML so I use the buildnode:with-html-document macro to resolve the mismatches (eg: <script src=…></script> instead of <script/>). According to Talcl examples, there are several ways to get your tal content into an XML document, and tal-processing-instruction is the fastest.
(hunchentoot:define-easy-handler (home :uri "/") () (render-page "home.tal" (talcl:tal-env 'course (current-course))))
This adds a handler to hunchentoot’s table, and should get us going down the left branch of my flowchart. The tal-env call is creating the tal environment where the compile template function will look for substitutions. I think of these like keyword arguments for the template. In this case, I’m pulling some course information and passing it to home.tal.
The last complicated bit is the tal templates themselves. There are some good examples in the talcl repository. I want one tal file to be the main site template, a frame around whatever content I’m trying to show with all the html,head,body tags. Then I’ll have one tal file for each major UI element.
The overall site template will be in templates/template.tal:
<html lang="en" xmlns:tal="http://common-lisp.net/project/bese/tal/core" tal:in-package=":alice"> <head> <meta charset="utf8"/> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"/> <script src="/script/alice.js"/> <link rel="stylesheet" href="/css/style.css" type="text/css" /> </head> <body> <span id="body">$body</span> </body> </html>
Since this is XML, we need some xmlns noise at the top, but we can use XMLisms like “<script/>”. The key things to note here:
tal:in-package=":alice"– need to tell Tal where it should be evaluating
$body– this is one way to substitute values into the template. Talcl will look for a symbol
'alice::bodyin it’s tal environment
So that’s our main template file, now for the home.tal file:
<tal:tal xmlns:tal="http://common-lisp.net/project/bese/tal/core" xmlns:param="http://common-lisp.net/project/bese/tal/params" tal:in-package=":alice"> <tal:include tal:name="template.tal"> <param:body> <button>Start Jam Class</button><br/> <tal:loop tal:var="c" tal:list="(classes course)"> <a href="class?name=$(name c)">Start $(name c) Class</a> </tal:loop> </param:body> </tal:include> </tal:tal>
I have the same xmlns noise, but have a new one namespace, param. This is the xml namespace tal uses to pass information from one template to another. The top level XML node is a “tal:tal” node, which does not render any output. I include template.tal to get our main template, passing it the UI for this page in a param:body. This adds
'alice::body to the tal environment, with the XML contents as the value, then template.tal is called. I use some fancier tal statements to loop over all the dance classes in the given course and make a link to each one.