This tutorial will show how a blog can easily be implemented in Common Lisp, using a few frameworks. Installing these frameworks is not covered, and neither are details on getting Common Lisp implementation up and running.
Part 1 shows how data can be stored persistently, and how HTML pages can be generated and served to the end-user. For your convenience, the source code of the files created in this tutorial can be downloaded.
The frameworks I'll be using are:
Update: Part 2 of this tutorial is now available.
First, let's define the package that will hold our code and load the packages that we require.
(in-package :cl-user) (defpackage :my-blog (:use :common-lisp)) (in-package :my-blog) (require :hunchentoot) (require :html-template) (require :elephant) (use-package :elephant) |
After in-package
comes our package content. First, we
make sure we have the necessary libraries loaded, by
using require
. Next, we use the
function use-package
to load the symbols exported by
Elephant into our local namespace. Without this, we would have to
prefix use of those symbols with elephant:
, as we
will see be done with the other packages.
Next, we define our model. To keep this simple, we will just
define a class for our blog posts, and then put them all in a
list. Alternativly, we could have defined a blog
class that would be a container for our blog posts.
(defpclass blog-post () ((title :initarg :title :accessor title) (body :initarg :body :accessor body) (timestamp :initarg :timestamp :accessor timestamp :initform (get-universal-time)))) |
Here we define a new class, blog-post
, which has
three attributes: title
, body
and timestamp
. Notice that we're using
Elephants defpclass
macro, instead
of defclass
which is the normal way of declaring
classes.
defpclass
makes sure that the class uses
the persistent-metaclass
metaclass, which adds
persistence to our class. Alternativly, we could have written
using defclass
and setting the metaclass ourselves.
(defclass blog-post () ((title :initarg :title :accessor title) (body :initarg :body :accessor body) (timestamp :initarg :timestamp :accessor timestamp :initform (get-universal-time))) (:metaclass persistent-metaclass)) |
We're almost ready to start creating blog posts, but we will first need to tell Elephant where to store our data. In this example I will use CLSQL and SQLite, but there are several other alternatives.
; Open the store where our data is stored (defvar *elephant-store* (open-store '(:clsql (:sqlite3 "/tmp/blog.db")))) ; Container for all our blog posts (defvar *blog-posts* (or (get-from-root "blog-posts") (let ((blog-posts (make-pset))) (add-to-root "blog-posts" blog-posts) blog-posts))) |
In the first line here, the function open-store
is
called, which tells Elephant to open a new persistent store where
our data is to be stored. The parameters tell it to use the CLSQL
package, and SQLite3. The store object is put into a variable in
case we need it later.
The next line defines a variable that will contain all our blog
posts. It is either retrieved from the Elephant store
using get-from-root
, but if it's not found then a
new pset
is created and added to the store.
The reason for using pset
instead of a normal Common
Lisp collection such as a list, is that operations which change
these do not have persistent side effects. In other words; if we
want collections that are persistent, we have to use classes
provided by Elephant. Two classes are provided: pset
which provides a persistent and unordered collection of objects,
and btree
which provides persistent collection of
key-value pairs.
Let's create a blog post and see if things work.
CL-USER> (in-package :my-blog) #<PACKAGE "MY-BLOG"> MY-BLOG> (setq first-post (make-instance 'blog-post :title "Hello blog world" :body "First post!")) #<BLOG-POST oid:5> MY-BLOG> (insert-item first-post *blog-posts*) #<BLOG-POST oid:5> MY-BLOG>
Ok, so far so good. Let's stop the Lisp instance and see if the blog post was stored.
CL-USER> (in-package :my-blog) #<PACKAGE "MY-BLOG"> MY-BLOG> *blog-posts* #<DEFAULT-PSET oid:4> MY-BLOG> (pset-list *blog-posts*) (#<BLOG-POST oid:5>) MY-BLOG> (title (first (pset-list *blog-posts*))) "Hello blog world" MY-BLOG>
When we load our package again, we can see that
the *blog-posts*
is defined, as we would expect, and
when we generate a list of the contents of the pset
using pset-list
, we can see that it contains the blog
posts that we created.
Let's create another blog post for good measure.
MY-BLOG> (insert-item (make-instance 'blog-post :title "This is fun" :body "Common Lisp is easy!") *blog-posts*) #<BLOG-POST oid:6> MY-BLOG>
Now that we have the possibility of storing and fetching blog posts, we need to somehow generate a web page containing the blog posts. We will do this using the html-template framework. We will create a single template, which simply displays all blog posts on a single page.
<!DOCTYPE html> <html> <head> <title>My Blog</title> </head> <body> <h1>My Blog</h1> <!-- tmpl_loop blog-posts --> <div> <h2><!-- tmpl_var title --></h2> <div> <!-- tmpl_var body --> </div> </div> <!-- /tmpl_loop --> </body> </html>
The format of html-template is based on the Perl module
HTML::Template. tmpl_loop
will loop
over a collection, and tmpl_var
will print the value
of the supplied parameters.
Next, we create a function that will use this template and generate the final page.
(defun generate-index-page () "Generate the index page showing all the blog posts." (with-output-to-string (stream) (html-template:fill-and-print-template #P"index.tmpl" (list :blog-posts (loop for blog-post in (pset-list *blog-posts*) collect (list :title (title blog-post) :body (body blog-post)))) :stream stream))) |
Let's go through this step by step. First,
since html-template::fill-and-print-template
needs to
print the resulting HTML to a stream, we use the
macro with-output-to-string
to create a stream that
can be written to and that will return the result as a string.
Next, we call the function fill-and-print-template
,
which does exactly that. Since it's in the
package html-template
, we prefix it with the correct
namespace. The required parameters to this function are the
template that is to be rendered and parameters that are to be used
in the template. We also tell the function to print to our
character stream. The loop
goes through the list of
blog posts and creates property lists for each post, containing
the title and body. To show how what the result of this loop , we
can run it separatly.
MY-BLOG> (loop for blog-post in (pset-list *blog-posts*) collect (list :title (title blog-post) :body (body blog-post))) ((:TITLE "This is fun" :BODY "Common Lisp is easy!") (:TITLE "Hello blog world" :BODY "First post!")) MY-BLOG>
To make sure fill-and-print
finds the correct file, we can set
variable *default-template-pathname*
to point to
where our templates are.
(setq html-template:*default-template-pathname* #P"/Users/vetler/Documents/devel/blog/source/") |
Let's try it out.
MY-BLOG> (generate-index-page) "<!DOCTYPE html> <html> <head> <title>My Blog</title> </head> <body> <h1>My Blog</h1> <div> <h2>Hello blog world</h2> <div> First post! </div> </div> </body> </html>" MY-BLOG>
Finally, we will need to set up a web server to serve our web page. We will do this using Hunchentoot, a web server implemented in Common Lisp.
Since we already have everything we need for generating the web page, it's just a matter of telling the web server to call the function for generating the web page and starting it.
(setq hunchentoot:*dispatch-table* (list (hunchentoot:create-prefix-dispatcher "/" 'generate-index-page))) (defvar *ht-server* (hunchentoot:start-server :port 8080)) |
The web server uses the *dispatch-table*
to map URLs
to functions. In this example we simply create a mapping from the
root to our function, but it's also possible to create dispatchers
with regular expressions,
using create-regex-dispatcher
.
Lastly the web server is started, at port 8080. That's it! Now
it's just a matter of pointing a browser
at http://localhost:8080/
and looking at our beautiful blog.
The basics of how to store data persistently and how to generate and serve web pages should now be clear. Of course, there is some essential functionality missing from our current implementation. In the next part, it will be shown how show blog posts on separate pages, edit content, and how to query the Elephant store.
Until then, good luck with Common Lisp!