1
0
Fork 0
cl-sites/lispcookbook.github.io/cl-cookbook/web.html
2023-10-25 11:23:21 +02:00

1264 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.2.0">
<title>Web development</title>
<meta charset="utf-8">
<meta name="description" content="A collection of examples of using Common Lisp">
<meta name="viewport" content=
"width=device-width, initial-scale=1">
<link rel="icon" href=
"assets/cl-logo-blue.png"/>
<link rel="stylesheet" href=
"assets/style.css">
<script type="text/javascript" src=
"assets/highlight-lisp.js">
</script>
<script type="text/javascript" src=
"assets/jquery-3.2.1.min.js">
</script>
<script type="text/javascript" src=
"assets/jquery.toc/jquery.toc.min.js">
</script>
<script type="text/javascript" src=
"assets/toggle-toc.js">
</script>
<link rel="stylesheet" href=
"assets/github.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; Web development</h1>
<div id="logo-container">
<a href="index.html">
<img id="logo" src="assets/cl-logo-blue.png"/>
</a>
<div id="searchform-container">
<form onsubmit="duckSearch()" action="javascript:void(0)">
<input id="searchField" type="text" value="" placeholder="Search...">
</form>
</div>
<div id="toc-container" class="toc-close">
<div id="toc-title">Table of Contents</div>
<ul id="toc" class="list-unstyled"></ul>
</div>
</div>
<div id="content-container">
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; Web development</h1>
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
<p class="announce">
📹 💻
<a style="font-size: 120%" href="https://www.udemy.com/course/common-lisp-programming/?couponCode=LISPMACROSPOWER" title="This course is under a paywall on the Udemy platform. Several videos are freely available so you can judge before diving in. vindarel is (I am) the main contributor to this Cookbook."> Discover vindarel's Lisp course in videos with this September coupon.</a>
<strong>
Recently added: 18 videos on MACROS.
</strong>
<a style="font-size: 90%" href="https://github.com/vindarel/common-lisp-course-in-videos/">Learn more</a>.
</p>
<p class="announce-neutral">
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
</p>
<div id="content"
<p>For web development as for any other task, one can leverage Common Lisps
advantages: the unmatched REPL that even helps to interact with a running web
app, the exception handling system, performance, the ability to build a
self-contained executable, stability, good threads story, strong typing, etc. We
can, say, define a new route and try it right away, there is no need to restart
any running server. We can change and compile <em>one function at a time</em> (the
usual <code>C-c C-c</code> in Slime) and try it. The feedback is immediate. We can choose
the degree of interactivity: the web server can catch exceptions and fire the
interactive debugger, or print lisp backtraces on the browser, or display a 404
error page and print logs on standard output. The ability to build
self-contained executables eases deployment tremendously (compared to, for
example, npm-based apps), in that we just copy the executable to a server and
run it.</p>
<p>And when we have deployed our app, we can still interact with it,
allowing for hot reload, that even works when new dependencies have to
be installed. If you are careful and dont want to use full live
reload, you might still enjoy this capability to reload, for example, a users
configuration file.</p>
<p>Well present here some established web frameworks and other common
libraries to help you getting started in developing a web
application. We do <em>not</em> aim to be exhaustive nor to replace the
upstream documentation. Your feedback and contributions are
appreciated.</p>
<!-- form creation, form validation -->
<!-- Javascript -->
<h2 id="overview">Overview</h2>
<p><a href="https://edicl.github.io/hunchentoot">Hunchentoot</a> and <a href="https://github.com/fukamachi/clack">Clack</a> are two projects that
youll often hear about.</p>
<p>Hunchentoot is</p>
<blockquote>
<p>a web server and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL. It provides facilities like automatic session handling (with and without cookies), logging, customizable error handling, and easy access to GET and POST parameters sent by the client.</p>
</blockquote>
<p>It is a software written by Edi Weitz (“Common Lisp Recipes”,
<code>cl-ppcre</code> and <a href="https://edicl.github.io/">much more</a>), its used and
proven solid. One can achieve a lot with it, but sometimes with more
friction than with a traditional web framework. For example,
dispatching a route by the HTTP method is a bit convoluted, one must
write a function for the <code>:uri</code> parameter that does the check, when it
is a built-in keyword in other frameworks like Caveman.</p>
<p>Clack is</p>
<blockquote>
<p>a web application environment for Common Lisp inspired by Pythons WSGI and Rubys Rack.</p>
</blockquote>
<p>Also written by a prolific lisper
(<a href="https://github.com/fukamachi/">E. Fukamachi</a>), it actually uses
Hunchentoot by default as the server, but thanks to its pluggable
architecture one can use another web server, like the asynchronous
<a href="https://github.com/fukamachi/woo">Woo</a>, built on the
<a href="http://software.schmorp.de/pkg/libev.html">libev</a> event loop, maybe
“the fastest web server written in any programming language”.</p>
<p>Well cite also <a href="https://github.com/orthecreedence/wookie">Wookie</a>, an asynchronous HTTP server, and its
companion library
<a href="https://github.com/orthecreedence/cl-async">cl-async</a>, for general
purpose, non-blocking programming in Common Lisp, built on libuv, the
backend library in Node.js.</p>
<p>Clack being more recent and less documented, and Hunchentoot a
de-facto standard, well concentrate on the latter for this
recipe. Your contributions are of course welcome.</p>
<p>Web frameworks build upon web servers and can provide facilities for
common activities in web development, like a templating system, access
to a database, session management, or facilities to build a REST api.</p>
<p>Some web frameworks include:</p>
<ul>
<li><a href="https://github.com/fukamachi/caveman">Caveman</a>, by E. Fukamachi. It provides, out of the box,
database management, a templating engine (Djula), a project skeleton
generator, a routing system à la Flask or Sinatra, deployment options
(mod_lisp or FastCGI), support for Roswell on the command line, etc.</li>
<li><a href="https://github.com/Shirakumo/radiance">Radiance</a>, by <a href="https://github.com/Shinmera">Shinmera</a>
(Qtools, Portacle, lquery, …), is a web application environment,
more general than usual web frameworks. It lets us write and tie
websites and applications together, easing their deployment as a
whole. It has thorough <a href="https://shirakumo.github.io/radiance/">documentation</a>, a <a href="https://github.com/Shirakumo/radiance-tutorial">tutorial</a>, <a href="https://github.com/Shirakumo/radiance-contribs">modules</a>, <a href="https://github.com/Shirakumo?utf8=%E2%9C%93&amp;q=radiance&amp;type=&amp;language=">pre-written applications</a> such as <a href="https://github.com/Shirakumo/purplish">an image board</a> or a <a href="https://github.com/Shirakumo/reader">blogging platform</a>, and more.
For example websites, see
<a href="https://shinmera.com/">https://shinmera.com/</a>,
<a href="https://reader.tymoon.eu/">reader.tymoon.eu</a> and <a href="https://events.tymoon.eu/">events.tymoon.eu</a>.</li>
<li><a href="https://github.com/joaotavora/snooze">Snooze</a>, by João Távora (Sly, Emacs Yasnippet, Eglot, …),
is “an URL router designed around REST web services”. It is
different because in Snooze, routes are just functions and HTTP
conditions are just Lisp conditions.</li>
<li><a href="https://github.com/mmontone/cl-rest-server">cl-rest-server</a> is a library for writing REST web
APIs. It features validation with schemas, annotations for logging,
caching, permissions or authentication, documentation via OpenAPI (Swagger),
etc.</li>
<li>last but not least, <a href="https://github.com/40ants/weblocks">Weblocks</a> is a venerable Common Lisp
web framework that permits to write ajax-based dynamic web
applications without writing any JavaScript, nor writing some lisp
that would transpile to JavaScript. It is seeing an extensive
rewrite and update since 2017. We present it in more details below.</li>
</ul>
<p>For a full list of libraries for the web, please see the <a href="https://github.com/CodyReichert/awesome-cl#network-and-internet">awesome-cl
list
#network-and-internet</a>
and <a href="https://www.cliki.net/Web">Cliki</a>. If you are looking for a
featureful static site generator, see
<a href="https://github.com/coleslaw-org/coleslaw">Coleslaw</a>.</p>
<h2 id="installation">Installation</h2>
<p>Lets install the libraries well use:</p>
<pre><code class="language-lisp">(ql:quickload '("hunchentoot" "caveman2" "spinneret" "djula" "easy-routes"))
</code></pre>
<p>To try Weblocks, please see its documentation. The Weblocks in
Quicklisp is not yet, as of writing, the one we are interested in.</p>
<p>Well start by serving local files and well run more than one local
server in the running image.</p>
<h2 id="simple-webserver">Simple webserver</h2>
<h3 id="serve-local-files">Serve local files</h3>
<h4 id="hunchentoot">Hunchentoot</h4>
<p>Create and start a webserver like this:</p>
<pre><code class="language-lisp">(defvar *acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4242))
(hunchentoot:start *acceptor*)
</code></pre>
<p>We create an instance of <code>easy-acceptor</code> on port 4242 and we start
it. We can now access <a href="http://127.0.0.1:4242/">http://127.0.0.1:4242/</a>. You should get a welcome
screen with a link to the documentation and logs to the console.</p>
<p>By default, Hunchentoot serves the files from the <code>www/</code> directory in
its source tree. Thus, if you go to the source of
<code>easy-acceptor</code> (<code>M-.</code> in Slime), which is probably
<code>~/quicklisp/dists/quicklisp/software/hunchentoot-v1.2.38/</code>, youll
find the <code>www/</code> directory. It contains:</p>
<ul>
<li>an <code>errors/</code> directory, with the error templates <code>404.html</code> and <code>500.html</code>,</li>
<li>an <code>img/</code> directory,</li>
<li>an <code>index.html</code> file.</li>
</ul>
<p>To serve another directory, we give the option <code>:document-root</code> to
<code>easy-acceptor</code>. We can also set the slot with its accessor:</p>
<pre><code class="language-lisp">(setf (hunchentoot:acceptor-document-root *acceptor*) #p"path/to/www")
</code></pre>
<p>Lets create our <code>index.html</code> first. Put this in a new
<code>www/index.html</code> at the current directory (of the lisp repl):</p>
<pre><code class="language-html">&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Hello!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Hello local server!&lt;/h1&gt;
&lt;p&gt;
We just served our own files.
&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Lets start a new acceptor on a new port:</p>
<pre><code class="language-lisp">(defvar *my-acceptor* (make-instance 'hunchentoot:easy-acceptor :port 4444
:document-root #p"www/"))
(hunchentoot:start *my-acceptor*)
</code></pre>
<p>go to <a href="http://127.0.0.1:4444/">http://127.0.0.1:4444/</a> and see the difference.</p>
<p>Note that we just created another <em>acceptor</em> on a different port on
the same lisp image. This is already pretty cool.</p>
<h2 id="access-your-server-from-the-internet">Access your server from the internet</h2>
<h3 id="hunchentoot-1">Hunchentoot</h3>
<p>With Hunchentoot we have nothing to do, we can see the server from the
internet right away.</p>
<p>If you evaluate this on your VPS:</p>
<pre><code>(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242))
</code></pre>
<p>You can see it right away on your servers IP.</p>
<p>Stop it with <code>(hunchentoot:stop *)</code>.</p>
<h2 id="routing">Routing</h2>
<h3 id="simple-routes">Simple routes</h3>
<h4 id="hunchentoot-2">Hunchentoot</h4>
<p>To bind an existing function to a route, we create a “prefix dispatch”
that we push onto the <code>*dispatch-table*</code> list:</p>
<pre><code class="language-lisp">(defun hello ()
(format nil "Hello, it works!"))
(push
(hunchentoot:create-prefix-dispatcher "/hello.html" #'hello)
hunchentoot:*dispatch-table*)
</code></pre>
<p>To create a route with a regexp, we use <code>create-regex-dispatcher</code>, where
the url-as-regexp can be a string, an s-expression or a cl-ppcre scanner.</p>
<p>If you didnt yet, create an acceptor and start the server:</p>
<pre><code class="language-lisp">(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 4242))
(hunchentoot:start *server*)
</code></pre>
<p>and access it on <a href="http://localhost:4242/hello.html">http://localhost:4242/hello.html</a>.</p>
<p>We can see logs on the REPL:</p>
<pre><code>127.0.0.1 - [2018-10-27 23:50:09] "get / http/1.1" 200 393 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:10] "get /img/made-with-lisp-logo.jpg http/1.1" 200 12583 "http://localhost:4242/" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:10] "get /favicon.ico http/1.1" 200 1406 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:19] "get /hello.html http/1.1" 200 20 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
</code></pre>
<hr />
<p><a href="https://edicl.github.io/hunchentoot/#define-easy-handler">define-easy-handler</a> allows to create a function and to bind it to an uri at once.</p>
<p>Its form follows</p>
<pre><code>define-easy-handler (function-name :uri &lt;uri&gt; …) (lambda list parameters)
</code></pre>
<p>where <code>&lt;uri&gt;</code> can be a string or a function.</p>
<p>Example:</p>
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey~@[ ~A~]!" name))
</code></pre>
<p>Visit it at <a href="http://localhost:4242/yo">p://localhost:4242/yo</a> and add parameters on the url:
<a href="http://localhost:4242/yo?name=Alice">http://localhost:4242/yo?name=Alice</a>.</p>
<p>Just a thought… we didnt explicitly ask Hunchentoot to add this
route to our first acceptor of the port 4242. Lets try another acceptor (see
previous section), on port 4444: <a href="http://localhost:4444/yo?name=Bob">http://localhost:4444/yo?name=Bob</a> It
works too ! In fact, <code>define-easy-handler</code> accepts an <code>acceptor-names</code>
parameter:</p>
<blockquote>
<p>acceptor-names (which is evaluated) can be a list of symbols which means that the handler will only be returned by DISPATCH-EASY-HANDLERS in acceptors which have one of these names (see ACCEPTOR-NAME). acceptor-names can also be the symbol T which means that the handler will be returned by DISPATCH-EASY-HANDLERS in every acceptor.</p>
</blockquote>
<p>So, <code>define-easy-handler</code> has the following signature:</p>
<pre><code>define-easy-handler (function-name &amp;key uri acceptor-names default-request-type) (lambda list parameters)
</code></pre>
<p>It also has a <code>default-parameter-type</code> which well use in a minute to get url parameters.</p>
<p>There are also keys to know for the lambda list. Please see the documentation.</p>
<h4 id="easy-routes-hunchentoot">Easy-routes (Hunchentoot)</h4>
<p><a href="https://github.com/mmontone/easy-routes">easy-routes</a> is a route
handling extension on top of Hunchentoot. It provides:</p>
<ul>
<li><strong>dispatch</strong> based on the HTTP method, such as GET or POST (which is otherwise cumbersome to do in Hunchentoot)</li>
<li><strong>arguments extraction</strong> from the url path</li>
<li><strong>decorators</strong> (functions to run before the route body, typically used to add a layer of authentication or changing the returned content type)</li>
<li><strong>URL generation</strong> from route names and given URL parameters</li>
<li>visualization of routes</li>
<li>and more</li>
</ul>
<p>To use it, dont create a server with <code>hunchentoot:easy-acceptor</code> but
with <code>easy-routes:easy-routes-acceptor</code>:</p>
<pre><code class="language-lisp">(setf *server* (make-instance 'easy-routes:easy-routes-acceptor))
</code></pre>
<p>Note: there is also <code>routes-acceptor</code>. The difference is that
<code>easy-routes-acceptor</code> iterates over Hunchentoots <code>*dispatch-table*</code>
if no route is found by <code>easy-routes</code>. That allows us, for example, to
serve static content the usual way with Hunchentoot.</p>
<p>Then define a route like this:</p>
<pre><code class="language-lisp">(easy-routes:defroute my-route-name ("/foo/:x" :method :get) (y &amp;get z)
(format nil "x: ~a y: ~a z: ~a" x y z))
</code></pre>
<p>Here, <code>:x</code> captures the path parameter and binds it to the <code>x</code>
variable into the route body. <code>y</code> and <code>&amp;get z</code> define url parameters,
and we can have <code>&amp;post</code> parameters to extract from the HTTP request
body.</p>
<p>These parameters can take an <code>:init-form</code> and <code>:parameter-type</code>
options as in <code>define-easy-handler</code>.</p>
<p>Now, imagine that we are deeper in our web application logic, and we
want to redirect our user to the route “/foo/3”. Instead of hardcoding
the URL, we can <strong>generate the URL from its name</strong>. Use
<code>easy-routes:genurl</code> like this:</p>
<pre><code class="language-lisp">(easy-routes:genurl my-route-name :id 3)
;; =&gt; /foo/3
(easy-routes:genurl my-route-name :id 3 :y "yay")
;; =&gt; /foo/3?y=yay
</code></pre>
<p><strong>Decorators</strong> are functions that are executed before the route body. They
should call the <code>next</code> parameter function to continue executing the
decoration chain and the route body finally. Examples:</p>
<pre><code class="language-lisp">(defun @auth (next)
(let ((*user* (hunchentoot:session-value 'user)))
(if (not *user*)
(hunchentoot:redirect "/login")
(funcall next))))
(defun @html (next)
(setf (hunchentoot:content-type*) "text/html")
(funcall next))
(defun @json (next)
(setf (hunchentoot:content-type*) "application/json")
(funcall next))
(defun @db (next)
(postmodern:with-connection *db-spec*
(funcall next)))
</code></pre>
<p>See <code>easy-routes</code> readme for more.</p>
<h4 id="caveman">Caveman</h4>
<p><a href="https://lispcookbook.github.io/cl-cookbook/caveman">Caveman</a> provides two ways to
define a route: the <code>defroute</code> macro and the <code>@route</code> pythonic
<em>annotation</em>:</p>
<pre><code class="language-lisp">(defroute "/welcome" (&amp;key (|name| "Guest"))
(format nil "Welcome, ~A" |name|))
@route GET "/welcome"
(lambda (&amp;key (|name| "Guest"))
(format nil "Welcome, ~A" |name|))
</code></pre>
<p>A route with an url parameter (note <code>:name</code> in the url):</p>
<pre><code class="language-lisp">(defroute "/hello/:name" (&amp;key name)
(format nil "Hello, ~A" name))
</code></pre>
<p>It is also possible to define “wildcards” parameters. It works with
the <code>splat</code> key:</p>
<pre><code class="language-lisp">(defroute "/say/*/to/*" (&amp;key splat)
; matches /say/hello/to/world
(format nil "~A" splat))
;=&gt; (hello world)
</code></pre>
<p>We must enable regexps with <code>:regexp t</code>:</p>
<pre><code class="language-lisp">(defroute ("/hello/([\\w]+)" :regexp t) (&amp;key captures)
(format nil "Hello, ~A!" (first captures)))
</code></pre>
<h3 id="accessing-get-and-post-parameters">Accessing GET and POST parameters</h3>
<h4 id="hunchentoot-3">Hunchentoot</h4>
<p>First of all, note that we can access query parameters anytime with</p>
<pre><code class="language-lisp">(hunchentoot:parameter "my-param")
</code></pre>
<p>It acts on the default <code>*request*</code> object which is passed to all handlers.</p>
<p>There is also <code>get-parameter</code> and <code>post-parameter</code>.</p>
<p>Earlier we saw some key parameters to <code>define-easy-handler</code>. We now
introduce <code>default-parameter-type</code>.</p>
<p>We defined the following handler:</p>
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey~@[ ~A~]!" name))
</code></pre>
<p>The variable <code>name</code> is a string by default. Lets check it out:</p>
<pre><code class="language-lisp">(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey~@[ ~A~] you are of type ~a" name (type-of name)))
</code></pre>
<p>Going to <a href="http://localhost:4242/yo?name=Alice">http://localhost:4242/yo?name=Alice</a> returns</p>
<pre><code>Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
</code></pre>
<p>To automatically bind it to another type, we use <code>default-parameter-type</code>. It can be
one of those simple types:</p>
<ul>
<li><code>'string</code> (default),</li>
<li><code>'integer</code>,</li>
<li><code>'character</code> (accepting strings of length 1 only, otherwise it is nil)</li>
<li>or <code>'boolean</code></li>
</ul>
<p>or a compound list:</p>
<ul>
<li><code>'(:list &lt;type&gt;)</code></li>
<li><code>'(:array &lt;type&gt;)</code></li>
<li><code>'(:hash-table &lt;type&gt;)</code></li>
</ul>
<p>where <code>&lt;type&gt;</code> is a simple type.</p>
<h3 id="accessing-a-json-request-body">Accessing a JSON request body</h3>
<h4 id="hunchentoot-4">Hunchentoot</h4>
<p>To read a request body, use <code>hunchentoot:raw-post-data</code>, to which you
can add <code>:force-text t</code> to always get a string (and not a vector of
octets).</p>
<p>Then you can parse this string to JSON with the library of your choice (<a href="https://github.com/Zulu-Inuoe/jzon/">jzon</a>, <a href="https://github.com/yitzchak/shasht">shasht</a>…).</p>
<pre><code class="language-lisp">(easy-routes route-api-demo ("/api/:id/update" :method :post) ()
(let ((json (ignore-errors
(jzon:parse (hunchentoot:raw-post-data :force-text t)))))
(when json
…)))
</code></pre>
<!-- ## Sessions -->
<!-- todo ? -->
<!-- ## Cookies -->
<h2 id="error-handling">Error handling</h2>
<p>In all frameworks, we can choose the level of interactivity. The web
framework can return a 404 page and print output on the repl, it can
catch errors and invoke the interactive lisp debugger, or it can show
the lisp backtrace on the html page.</p>
<h3 id="hunchentoot-5">Hunchentoot</h3>
<p>The global variables to set to choose the error handling behaviour are:</p>
<ul>
<li><code>*catch-errors-p*</code>: set to <code>nil</code> if you want errors to be caught in
the interactive debugger (for development only, of course):</li>
</ul>
<pre><code class="language-lisp">(setf hunchentoot:*catch-errors-p* nil)
</code></pre>
<p>See also the generic function <code>maybe-invoke-debugger</code> if you want to
fine-tune this behaviour. You might want to specialize it on specific
condition classes (see below) for debugging purposes. The default method <a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_invoke.htm">invokes
the debugger</a>
if <code>*catch-errors-p*</code> is <code>nil</code>.</p>
<ul>
<li><code>*show-lisp-errors-p*</code>: set to <code>t</code> if you want to see errors in HTML output in the browser.</li>
<li><code>*show-lisp-backtraces-p*</code>: set to <code>nil</code> if the errors shown in HTML
output (when <code>*show-lisp-errors-p*</code> is <code>t</code>) should <em>not</em> contain
backtrace information (defaults to <code>t</code>, shows the backtrace).</li>
</ul>
<p>Hunchentoot defines condition classes. The superclass of all
conditions is <code>hunchentoot-condition</code>. The superclass of errors is <code>hunchentoot-error</code> (itself a subclass of <code>hunchentoot-condition</code>).</p>
<p>See the documentation: <a href="https://edicl.github.io/hunchentoot/#conditions">https://edicl.github.io/hunchentoot/#conditions</a>.</p>
<h3 id="clack">Clack</h3>
<p>Clack users might make a good use of plugins, like the clack-errors middleware: <a href="https://github.com/CodyReichert/awesome-cl#clack-plugins">https://github.com/CodyReichert/awesome-cl#clack-plugins</a>.</p>
<p><img src="assets/clack-errors.png" width="800" /></p>
<h2 id="weblocks---solving-the-javascript-problem">Weblocks - solving the “JavaScript problem”©</h2>
<p><a href="https://github.com/40ants/weblocks">Weblocks</a> is a widgets-based and
server-based framework with a built-in ajax update mechanism. It
allows to write dynamic web applications <em>without the need to write
JavaScript or to write lisp code that would transpile to JavaScript</em>.</p>
<p><img src="assets/weblocks-quickstart-check-task.gif" alt="" /></p>
<p>Weblocks is an old framework developed by Slava Akhmechet, Stephen
Compall and Leslie Polzer. After nine calm years, it is seeing a very
active update, refactoring and rewrite effort by Alexander Artemenko.</p>
<p>It was initially based on continuations (they were removed to date)
and thus a lispy cousin of Smalltalks
<a href="https://en.wikipedia.org/wiki/Seaside_(software)">Seaside</a>. We can
also relate it to Haskells Haste, OCamls Eliom,
Elixirs Phoenix LiveView and others.</p>
<p>The <a href="http://ultralisp.org/">Ultralisp</a> website is an example Weblocks
website in production known in the CL community.</p>
<hr />
<p>Weblocks unit of work is the <em>widget</em>. They look like a class definition:</p>
<pre><code class="language-lisp">(defwidget task ()
((title
:initarg :title
:accessor title)
(done
:initarg :done
:initform nil
:accessor done)))
</code></pre>
<p>Then all we have to do is to define the <code>render</code> method for this widget:</p>
<pre><code class="language-lisp">(defmethod render ((task task))
"Render a task."
(with-html
(:span (if (done task)
(with-html
(:s (title task)))
(title task)))))
</code></pre>
<p>It uses the Spinneret template engine by default, but we can bind any
other one of our choice.</p>
<p>To trigger an ajax event, we write lambdas in full Common Lisp:</p>
<pre><code class="language-lisp">...
(with-html
(:p (:input :type "checkbox"
:checked (done task)
:onclick (make-js-action
(lambda (&amp;key &amp;allow-other-keys)
(toggle task))))
...
</code></pre>
<p>The function <code>make-js-action</code> creates a simple javascript function
that calls the lisp one on the server, and automatically refreshes the
HTML of the widgets that need it. In our example, it re-renders one
task only.</p>
<p>Is it appealing ? Carry on this quickstart guide here: <a href="http://40ants.com/weblocks/quickstart.html">http://40ants.com/weblocks/quickstart.html</a>.</p>
<h2 id="templates">Templates</h2>
<h3 id="djula---html-markup">Djula - HTML markup</h3>
<p><a href="https://github.com/mmontone/djula">Djula</a> is a port of Pythons
Django template engine to Common Lisp. It has <a href="https://mmontone.github.io/djula/djula/">excellent documentation</a>.</p>
<p>Caveman uses it by default, but otherwise it is not difficult to
setup. We must declare where our templates are with something like</p>
<pre><code class="language-lisp">(djula:add-template-directory (asdf:system-relative-pathname "webapp" "templates/"))
</code></pre>
<p>and then we can declare and compile the ones we use, for example::</p>
<pre><code class="language-lisp">(defparameter +base.html+ (djula:compile-template* "base.html"))
(defparameter +welcome.html+ (djula:compile-template* "welcome.html"))
</code></pre>
<p>A Djula template looks like this (forgive the antislash in <code>{\%</code>, this
is a Jekyll limitation):</p>
<pre><code>{\% extends "base.html" \%}
{\% block title %}Memberlist{\% endblock \%}
{\% block content \%}
&lt;ul&gt;
{\% for user in users \%}
&lt;li&gt;&lt;a href=""&gt;&lt;/a&gt;&lt;/li&gt;
{\% endfor \%}
&lt;/ul&gt;
{\% endblock \%}
</code></pre>
<p>At last, to render the template, call <code>djula:render-template*</code> inside a route.</p>
<pre><code class="language-lisp">(easy-routes:defroute root ("/" :method :get) ()
(djula:render-template* +welcome.html+ nil
:users (get-users)
</code></pre>
<p>Note that for efficiency Djula compiles the templates before rendering them.</p>
<p>It is, along with its companion
<a href="https://github.com/AccelerationNet/access/">access</a> library, one of
the most downloaded libraries of Quicklisp.</p>
<h4 id="djula-filters">Djula filters</h4>
<p>Filters allow to modify how a variable is displayed. Djula comes with
a good set of built-in filters and they are <a href="https://mmontone.github.io/djula/doc/build/html/filters.html">well documented</a>. They are not to be confused with <a href="https://mmontone.github.io/djula/doc/build/html/tags.html">tags</a>.</p>
<p>They look like this: ``, where <code>lower</code> is an
existing filter, which renders the text into lowercase.</p>
<p>Filters sometimes take arguments. For example: `` calls
the <code>add</code> filter with arguments <code>value</code> and 2.</p>
<p>Moreover, it is very easy to define custom filters. All we have to do
is to use the <code>def-filter</code> macro, which takes the variable as first
argument, and which can take more optional arguments.</p>
<p>Its general form is:</p>
<pre><code class="language-lisp">(def-filter :myfilter-name (value arg) ;; arg is optional
(body))
</code></pre>
<p>and it is used like this: ``.</p>
<p>Heres how the <code>add</code> filter is defined:</p>
<pre><code class="language-lisp">(def-filter :add (it n)
(+ it (parse-integer n)))
</code></pre>
<p>Once you have written a custom filter, you can use it right away
throughout the application.</p>
<p>Filters are very handy to move non-trivial formatting or logic from the
templates to the backend.</p>
<h3 id="spinneret---lispy-templates">Spinneret - lispy templates</h3>
<p><a href="https://github.com/ruricolist/spinneret">Spinneret</a> is a “lispy”
HTML5 generator. It looks like this:</p>
<pre><code class="language-lisp">(with-page (:title "Home page")
(:header
(:h1 "Home page"))
(:section
("~A, here is *your* shopping list: " *user-name*)
(:ol (dolist (item *shopping-list*)
(:li (1+ (random 10)) item))))
(:footer ("Last login: ~A" *last-login*)))
</code></pre>
<p>The author finds it is easier to compose the HTML in separate
functions and macros than with the more famous cl-who. But it
has more features under it sleeves:</p>
<ul>
<li>it warns on invalid tags and attributes</li>
<li>it can automatically number headers, given their depth</li>
<li>it pretty prints html per default, with control over line breaks</li>
<li>it understands embedded markdown</li>
<li>it can tell where in the document a generator function is (see <code>get-html-tag</code>)</li>
</ul>
<h2 id="serve-static-assets">Serve static assets</h2>
<h3 id="hunchentoot-6">Hunchentoot</h3>
<p>With Hunchentoot, use <code>create-folder-dispatcher-and-handler prefix directory</code>.</p>
<p>For example:</p>
<pre><code class="language-lisp">(push (hunchentoot:create-folder-dispatcher-and-handler
"/static/" (merge-pathnames "src/static" ;; starts without a /
(asdf:system-source-directory :myproject)))
hunchentoot:*dispatch-table*)
</code></pre>
<p>Now our projects static files located under
<code>/path/to/myproject/src/static/</code> are served with the <code>/static/</code> prefix:</p>
<pre><code class="language-html">&lt;img src="/static/img/banner.jpg" /&gt;
</code></pre>
<h2 id="connecting-to-a-database">Connecting to a database</h2>
<p>Please see the <a href="databases.html">databases section</a>. The Mito ORM
supports SQLite3, PostgreSQL, MySQL, it has migrations and db schema
versioning, etc.</p>
<p>In Caveman, a database connection is alive during the Lisp session and is
reused in each HTTP requests.</p>
<h3 id="checking-a-user-is-logged-in">Checking a user is logged-in</h3>
<p>A framework will provide a way to work with sessions. Well create a
little macro to wrap our routes to check if the user is logged in.</p>
<p>In Caveman, <code>*session*</code> is a hash table that represents the sessions
data. Here are our login and logout functions:</p>
<pre><code class="language-lisp">(defun login (user)
"Log the user into the session"
(setf (gethash :user *session*) user))
(defun logout ()
"Log the user out of the session."
(setf (gethash :user *session*) nil))
</code></pre>
<p>We define a simple predicate:</p>
<pre><code class="language-lisp">(defun logged-in-p ()
(gethash :user cm:*session*))
</code></pre>
<p>and we define our <code>with-logged-in</code> macro:</p>
<pre><code class="language-lisp">(defmacro with-logged-in (&amp;body body)
`(if (logged-in-p)
(progn ,@body)
(render #p"login.html"
'(:message "Please log-in to access this page."))))
</code></pre>
<p>If the user isnt logged in, there will nothing in the session store,
and we render the login page. When all is well, we execute the macros
body. We use it like this:</p>
<pre><code class="language-lisp">(defroute "/account/logout" ()
"Show the log-out page, only if the user is logged in."
(with-logged-in
(logout)
(render #p"logout.html")))
(defroute ("/account/review" :method :get) ()
(with-logged-in
(render #p"review.html"
(list :review (get-review (gethash :user *session*))))))
</code></pre>
<p>and so on.</p>
<h3 id="encrypting-passwords">Encrypting passwords</h3>
<h4 id="with-cl-pass">With cl-pass</h4>
<p><a href="https://github.com/eudoxia0/cl-pass">cl-pass</a> is a password hashing and verification library. It is as simple to use as this:</p>
<pre><code class="language-lisp">(cl-pass:hash "test")
;; "PBKDF2$sha256:20000$5cf6ee792cdf05e1ba2b6325c41a5f10$19c7f2ccb3880716bf7cdf999b3ed99e07c7a8140bab37af2afdc28d8806e854"
(cl-pass:check-password "test" *)
;; t
(cl-pass:check-password "nope" **)
;; nil
</code></pre>
<p>You might also want to look at
<a href="https://github.com/eudoxia0/hermetic">hermetic</a>, a simple
authentication system for Clack-based applications.</p>
<h4 id="manually-with-ironclad">Manually (with Ironclad)</h4>
<p>In this recipe we do the encryption and verification ourselves. We use the de-facto standard
<a href="https://github.com/froydnj/ironclad">Ironclad</a> cryptographic toolkit
and the <a href="https://github.com/cl-babel/babel">Babel</a> charset
encoding/decoding library.</p>
<p>The following snippet creates the password hash that should be stored in your
database. Note that Ironclad expects a byte-vector, not a string.</p>
<pre><code class="language-lisp">(defun password-hash (password)
(ironclad:pbkdf2-hash-password-to-combined-string
(babel:string-to-octets password)))
</code></pre>
<p><code>pbkdf2</code> is defined in <a href="https://tools.ietf.org/html/rfc2898">RFC2898</a>.
It uses a pseudorandom function to derive a secure encryption key
based on the password.</p>
<p>The following function checks if a user is active and verifies the
entered password. It returns the user-id if active and verified and
nil in all other cases even if an error occurs. Adapt it to your
application.</p>
<pre><code class="language-lisp">(defun check-user-password (user password)
(handler-case
(let* ((data (my-get-user-data user))
(hash (my-get-user-hash data))
(active (my-get-user-active data)))
(when (and active (ironclad:pbkdf2-check-password (babel:string-to-octets password)
hash))
(my-get-user-id data)))
(condition () nil)))
</code></pre>
<p>And the following is an example on how to set the password on the
database. Note that we use <code>(password-hash password)</code> to save the
password. The rest is specific to the web framework and to the DB
library.</p>
<pre><code class="language-lisp">(defun set-password (user password)
(with-connection (db)
(execute
(make-statement :update :web_user
(set= :hash (password-hash password))
(make-clause :where
(make-op := (if (integerp user)
:id_user
:email)
user))))))
</code></pre>
<p><em>Credit: <code>/u/arvid</code> on <a href="https://www.reddit.com/r/learnlisp/comments/begcf9/can_someone_give_me_an_eli5_on_hiw_to_encrypt_and/">/r/learnlisp</a></em>.</p>
<h2 id="runnning-and-building">Runnning and building</h2>
<h3 id="running-the-application-from-source">Running the application from source</h3>
<p>To run our Lisp code from source, as a script, we can use the <code>--load</code>
switch from our implementation.</p>
<p>We must ensure:</p>
<ul>
<li>to load the projects .asd system declaration (if any)</li>
<li>to install the required dependencies (this demands we have installed Quicklisp previously)</li>
<li>and to run our applications entry point.</li>
</ul>
<p>We could use such commands:</p>
<pre><code class="language-lisp">;; run.lisp
(load "myproject.asd")
(ql:quickload "myproject")
(in-package :myproject)
(handler-case
;; The START function starts the web server.
(myproject::start :port (ignore-errors (parse-integer (uiop:getenv "PROJECT_PORT"))))
(error (c)
(format *error-output* "~&amp;An error occured: ~a~&amp;" c)
(uiop:quit 1)))
</code></pre>
<p>In addition we have allowed the user to set the applications port
with an environment variable.</p>
<p>We can run the file like so:</p>
<pre><code>sbcl --load run.lisp
</code></pre>
<p>After loading the project, the web server is started in the
background. We are offered the usual Lisp REPL, from which we can
interact with the running application.</p>
<p>We can also connect to the running application from our preferred
editor, from home, and compile the changes in our editor to the
running instance. See the following section
<a href="web.html#connecting-to-a-remote-lisp-image">#connecting-to-a-remote-lisp-image</a>.</p>
<h3 id="building-a-self-contained-executable">Building a self-contained executable</h3>
<p>As for all Common Lisp applications, we can bundle our web app in one
single executable, including the assets. It makes deployment very
easy: copy it to your server and run it.</p>
<pre><code>$ ./my-web-app
Hunchentoot server is started.
Listening on localhost:9003.
</code></pre>
<p>See this recipe on <a href="scripting.html#for-web-apps">scripting#for-web-apps</a>.</p>
<h3 id="continuous-delivery-with-travis-ci-or-gitlab-ci">Continuous delivery with Travis CI or Gitlab CI</h3>
<p>Please see the section on <a href="testing.html#continuous-integration">testing#continuous-integration</a>.</p>
<h3 id="multi-platform-delivery-with-electron">Multi-platform delivery with Electron</h3>
<p><a href="https://ceramic.github.io/">Ceramic</a> makes all the work for us.</p>
<p>It is as simple as this:</p>
<pre><code class="language-lisp">;; Load Ceramic and our app
(ql:quickload '(:ceramic :our-app))
;; Ensure Ceramic is set up
(ceramic:setup)
(ceramic:interactive)
;; Start our app (here based on the Lucerne framework)
(lucerne:start our-app.views:app :port 8000)
;; Open a browser window to it
(defvar window (ceramic:make-window :url "http://localhost:8000/"))
;; start Ceramic
(ceramic:show-window window)
</code></pre>
<p>and we can ship this on Linux, Mac and Windows.</p>
<p>There is more:</p>
<blockquote>
<p>Ceramic applications are compiled down to native code, ensuring both performance and enabling you to deliver closed-source, commercial applications.</p>
</blockquote>
<p>Thus, no need to minify our JS.</p>
<h2 id="deployment">Deployment</h2>
<h3 id="deploying-manually">Deploying manually</h3>
<p>We can start our executable in a shell and send it to the background (<code>C-z bg</code>), or run it inside a <code>tmux</code> session. These are not the best but hey, it works©.</p>
<h3 id="systemd-daemonizing-restarting-in-case-of-crashes-handling-logs">Systemd: Daemonizing, restarting in case of crashes, handling logs</h3>
<p>This is actually a system-specific task. See how to do that on your system.</p>
<p>Most GNU/Linux distros now come with Systemd, so heres a little example.</p>
<p>Deploying an app with Systemd is as simple as writing a configuration file:</p>
<pre><code>$ sudo emacs -nw /etc/systemd/system/my-app.service
[Unit]
Description=your lisp app on systemd example
[Service]
WorkingDirectory=/path/to/your/project/directory/
ExecStart=/usr/bin/make run # or anything
Type=simple
Restart=on-failure
[Install]
WantedBy=network.target
</code></pre>
<p>Then we have a command to <code>start</code> it, only now:</p>
<pre><code>sudo systemctl start my-app.service
</code></pre>
<p>and a command to install the service, to <strong>start the app after a boot
or reboot</strong> (thats the “[Install]” part):</p>
<pre><code>sudo systemctl enable my-app.service
</code></pre>
<p>Then we can check its <code>status</code>:</p>
<pre><code>systemctl status my-app.service
</code></pre>
<p>and see our applications <strong>logs</strong> (we can write to stdout or stderr,
and Systemd handles the logging):</p>
<pre><code>journalctl -u my-app.service
</code></pre>
<p>(you can also use the <code>-f</code> option to see log updates in real time, and in that case augment the number of lines with <code>-n 50</code> or <code>--lines</code>).</p>
<p>Systemd handles crashes and <strong>restarts the application</strong>. Thats the <code>Restart=on-failure</code> line.</p>
<p>Now keep in mind a couple things:</p>
<ul>
<li>we want our app to crash so that it can be re-started automatically:
youll want the <code>--disable-debugger</code> flag with SBCL.</li>
<li>Systemd will, by default, run your app as root. If you rely on your
Lisp to read your startup file (<code>~/.sbclrc</code>), especially to setup
Quicklisp, you will need to use the <code>--userinit</code> flag, or to set the
Systemd user with <code>User=xyz</code> in the <code>[service]</code> section. And if you
use a startup file, be aware that the line <code>(user-homedir-pathname)</code>
will not return the same result depending on the user, so the snippet
might not find Quicklisps setup.lisp file.</li>
</ul>
<p>See more: <a href="https://www.freedesktop.org/software/systemd/man/systemd.service.html">https://www.freedesktop.org/software/systemd/man/systemd.service.html</a>.</p>
<h3 id="with-docker">With Docker</h3>
<p>There are several Docker images for Common
Lisp. For example:</p>
<ul>
<li><a href="https://hub.docker.com/r/clfoundation/sbcl/">clfoundation/sbcl</a>
includes the latest version of SBCL, many OS packages useful for CI
purposes, and a script to install Quicklisp.</li>
<li><a href="https://github.com/40ants/base-lisp-image">40ants/base-lisp-image</a>
is based on Ubuntu LTS and includes SBCL, CCL, Quicklisp, Qlot and
Roswell.</li>
<li><a href="https://github.com/container-lisp/s2i-lisp">container-lisp/s2i-lisp</a>
is CentOs based and contains the source for building a Quicklisp based
Common Lisp application as a reproducible docker image using OpenShifts
source-to-image.</li>
</ul>
<h3 id="with-guix">With Guix</h3>
<p><a href="https://www.gnu.org/software/guix/">GNU Guix</a> is a transactional
package manager, that can be installed on top of an existing OS, and a
whole distro that supports declarative system configuration. It allows
to ship self-contained tarballs, which also contain system
dependencies. For an example, see the <a href="https://github.com/atlas-engineer/nyxt/">Nyxt browser</a>.</p>
<h3 id="running-behind-nginx">Running behind Nginx</h3>
<p>There is nothing CL-specific to run your Lisp web app behind Nginx. Heres an example to get you started.</p>
<p>We suppose you are running your Lisp app on a web server, with the IP
address 1.2.3.4, on the port 8001. Nothing special here. We want to
access our app with a real domain name (and eventuall benefit of other
Nginxs advantages, such as rate limiting etc). We bought our domain
name and we created a DNS record of type A that links the domain name
to the servers IP address.</p>
<p>We must configure our server with Nginx to tell it that all
connections coming from “your-domain-name.org”, on port 80, are to be
sent to the Lisp app running locally.</p>
<p>Create a new file: <code>/etc/nginx/sites-enabled/my-lisp-app.conf</code> and add this proxy directive:</p>
<pre><code class="language-lisp">server {
listen www.your-domain-name.org:80;
server_name your-domain-name.org www.your-domain-name.org; # with and without www
location / {
proxy_pass http://1.2.3.4:8001/;
}
# Optional: serve static files with nginx, not the Lisp app.
location /files/ {
proxy_pass http://1.2.3.4:8001/files/;
}
}
</code></pre>
<p>Note that on the proxy_pass directive: <code>proxy_pass
http://1.2.3.4:8001/;</code> we are using our servers public IP
address. Oten, your Lisp webserver such as Hunchentoot directly
listens on it. You might want, for security reasons, to run the Lisp
app on localhost.</p>
<p>Reload nginx (send the “reload” signal):</p>
<pre><code>$ nginx -s reload
</code></pre>
<p>and thats it: you can access your Lisp app from the outside through <code>http://www.your-domain-name.org</code>.</p>
<h3 id="deploying-on-heroku-and-other-services">Deploying on Heroku and other services</h3>
<p>See <a href="https://gitlab.com/duncan-bayne/heroku-buildpack-common-lisp">heroku-buildpack-common-lisp</a> and the <a href="https://github.com/CodyReichert/awesome-cl#deployment">Awesome CL#deploy</a> section for interface libraries for Kubernetes, OpenShift, AWS, etc.</p>
<h2 id="monitoring">Monitoring</h2>
<p>See <a href="https://github.com/deadtrickster/prometheus.cl">Prometheus.cl</a>
for a Grafana dashboard for SBCL and Hunchentoot metrics (memory,
threads, requests per second,…).</p>
<h2 id="connecting-to-a-remote-lisp-image">Connecting to a remote Lisp image</h2>
<p>This this section: <a href="debugging.html#remote-debugging">debugging#remote-debugging</a>.</p>
<h2 id="hot-reload">Hot reload</h2>
<p>This is an example from <a href="https://github.com/stylewarning/quickutil/blob/master/quickutil-server/">Quickutil</a>. It is actually an automated version of the precedent section.</p>
<p>It has a Makefile target:</p>
<pre><code class="language-lisp">hot_deploy:
$(call $(LISP), \
(ql:quickload :quickutil-server) (ql:quickload :swank-client), \
(swank-client:with-slime-connection (conn "localhost" $(SWANK_PORT)) \
(swank-client:slime-eval (quote (handler-bind ((error (function continue))) \
(ql:quickload :quickutil-utilities) (ql:quickload :quickutil-server) \
(funcall (symbol-function (intern "STOP" :quickutil-server))) \
(funcall (symbol-function (intern "START" :quickutil-server)) $(start_args)))) conn)) \
$($(LISP)-quit))
</code></pre>
<p>It has to be run on the server (a simple fabfile command can call this
through ssh). Beforehand, a <code>fab update</code> has run <code>git pull</code> on the
server, so new code is present but not running. It connects to the
local swank server, loads the new code, stops and starts the app in a
row.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="https://hg.sr.ht/~wnortje/feather">Feather</a>, a template for web
application development, shows a functioning Hello World app
with an HTML page, a JSON API, a passing test suite, a Postgres DB
and DB migrations. Uses Qlot, Buildapp, SystemD for deployment.</li>
<li><a href="https://github.com/vindarel/lisp-web-template-productlist">lisp-web-template-productlist</a>,
a simple project template with Hunchentoot, Easy-Routes, Djula and Bulma CSS.</li>
<li><a href="https://github.com/vindarel/lisp-web-live-reload-example/">lisp-web-live-reload-example</a> -
a toy project to show how to interact with a running web app.</li>
</ul>
<h2 id="credits">Credits</h2>
<ul>
<li><a href="https://lisp-journey.gitlab.io/web-dev/">https://lisp-journey.gitlab.io/web-dev/</a></li>
</ul>
<p class="page-source">
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/web.md">web.md</a>
</p>
</div>
<script type="text/javascript">
// Don't write the TOC on the index.
if (window.location.pathname != "/cl-cookbook/") {
$("#toc").toc({
content: "#content", // will ignore the first h1 with the site+page title.
headings: "h1,h2,h3,h4"});
}
$("#two-cols + ul").css({
"column-count": "2",
});
$("#contributors + ul").css({
"column-count": "4",
});
</script>
<div>
<footer class="footer">
<hr/>
&copy; 2002&ndash;2023 the Common Lisp Cookbook Project
<div>
📹 Discover <a style="color: darkgrey; text-decoration: underline", href="https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358">vindarel's Lisp course on Udemy</a>
</div>
</footer>
</div>
<div id="toc-btn">T<br>O<br>C</div>
</div>
<script text="javascript">
HighlightLisp.highlight_auto({className: null});
</script>
<script type="text/javascript">
function duckSearch() {
var searchField = document.getElementById("searchField");
if (searchField && searchField.value) {
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
// https://duckduckgo.com/params
// kj=b2: blue header in results page
// kf=-1: no favicons
}
}
</script>
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
</body>
</html>