📜 ⬆️ ⬇️

Sample Common Lisp Site

Introduction





This article is written to illustrate how Common Lisp applies to typical web development tasks.
')
I will try to show how Lisp implements the main things used in web programming - templating, routing and caching. I also left some space for macros.

The article is largely educational, nevertheless it is quite a working website - rigidus.ru



Templates and their compilation



{namespace tpl} {template root} <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">{\n} <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">{\n} <head>{\n} <title>{$headtitle}</title>{\n} <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />{\n} <link rel="stylesheet" type="text/css" media="screen" href="/style.css" />{\n} <link rel="Shortcut Icon" type="image/x-icon" href="/favicon.ico" />{\n} </head>{\n} <body id="top">{\n} {$content | noAutoescape}{\n} </body>{\n} </html>{\n} {/template} 


This simple template is compiled on the fly with the CL-CLOSURE-TEMPLATE library into the machine code for the root function in the tpl package. Thus, applying data to a template is a call to the compiled function:

 (tpl:root (list :headtitle " " :content "Hello world")) 


Instead of “Hello world”, you can substitute a function call into which another template is compiled - for example, the “base” template, which provides the minimum grid for the site:

 {template base} <div id="center"> <div class="col1 left"> <a id="logo" href="index.html"> <img src="http://www.gravatar.com/avatar/d8a986606b9d5e4769ba062779e95d9f?s=45" style="border: 1px solid #7F7F7F"/> </a> <ul id="nav"> {foreach $elt in $navpoints} {call navelt data="$elt" /} {/foreach} </ul> </div> {$content |noAutoescape} <div class="clear">.</div> </div> <div id="footer"> <p> <a href="/about">About</a> | <a href="/contacts">Contacts</a> </p> </div> {/template} 


Now, using the example of the introductory article habrahabr.ru/blogs/webdev/111365 and our newly created template, we could write a request-dispatcher for the site from one page like this:

 (defun request-dispatcher (request) (tpl:root (list :headtitle "My home page" :content (tpl:base (list :navpoints ..-.. :content ..-..))))) 


RESTAS routes



The RESTAS library frees us from the fascinating writing of dispatchers.
Now the dispatcher will be created on the basis of the routes (routes) that we define, which we define like this:

 (restas:define-route main ("") (tpl:main (list :headtitle "My main page" :content "Hello! <a href=\"/articles\">Articles</a>"))) (restas:define-route css ("/css/:cssfile") (hunchentoot:handle-static-file (format nil "~a/css/~a" *base-dir* cssfile))) 


- What is this nonsense? - A sophisticated web developer will ask. - This is what I should ask my route for each css-file?

- Not at all! - I will answer. You can specify a lambda: a requirement that decides whether the route is suitable or not. Here is the updated code that gives the file if it finds it on disk in the site directory:

 (restas:define-route static ("/:staticfile" :requirement (lambda () (let ((request-file (pathname (format nil "~a/~a" *base-dir* (hunchentoot:request-uri hunchentoot:*request*)))) (files (directory (format nil "~a/*.*" *base-dir*)))) (not (null (find request-file files :test #'equal)))))) (hunchentoot:handle-static-file (format nil "~a/~a" *base-dir* staticfile))) 


Here we simply determined the routes for the main page and for the delivery of css-files - as you can see you can use: wildcards

Using Macros



I prepare articles for the site using org-mode — Emaks’s convenient mode, combining simplicity of markup (like wikis) and various convenient tools, such as folding sections. I wrote the org-to-html function,
which I transfer the text of the article in the org-mode format, and it automatically builds me html with the headers extracted from the metadata specified directly in the article, and also returns information about sections and subsections.

After this function has processed my file, I may need to change some of the headers and to preserve the simplicity of the call, I use the default-page macro:

 (defmacro default-page (menu file-path &optional (body nil)) `(let ((menu-memo ,menu)) (multiple-value-bind (content sections directives) (org-to-html (alexandria:read-file-into-string ,file-path)) (let ((title (getf directives :title))) ,body (page title menu-memo (tpl:default (list :title title :navpoints menu-memo :sections (loop :for i :from 1 :for section :in sections :collect (list :anchor (format nil "anchor-~a" i) :level (format nil "level-~a" (car section)) :title (cadr section))) :content content))))))) 


Now I can not only get rid of a complex call in the client code, but also make an “injection” of any code inside default-page, for example:

 (restas:define-route about ("/about") (default-page (menu) (base-path "about.org") ;;     -  (let ((cnt (length sections))) ;;      e (setf title (format nil "~a — ~a " title cnt))))) 


In the next section, this approach is used more meaningfully.

Caching



My articles are in files containing meta information: headings and categories. To build the "/ articles" page, I go through the files, which can take a while and load the system. However, this data can
remember in closure, which is exactly what the code does:

 (let ((memo)) (restas:define-route articles ("/articles") (when (null memo) (setf memo (default-page (menu) (base-path "articles.org") (setf content ;;  ,    ;;   (    ) )))) memo)) 


It is clear that if it is necessary for the cache to become obsolete over time, this is also fairly easy to implement. While it is easier for me to enter slime, do Ctrl + X, Ctrl + E on the last line of this code and it will be executed anew, which will lead to resetting the cache. Uploading a new article (what happens is not
too often) I do it - this is a good reason to immediately add some more functionality.

For those interested in details:
I posted the source code on github.com/rigidus/rigidus.ru
And the site itself is on rigidus.ru
Let's see how it will cope with habraeffektom.

Source: https://habr.com/ru/post/112051/


All Articles