Making a Static Site

In this tutorial, we will create our first static website using Static. The site will have pages "Home", "About the site", "Services" and a blog with posts. The blog posts will be collected in an RSS feed, and all pages will be presented in a file sitemap.xml . At the end, we will connect the Disqus comment system to the posts for feedback from visitors. Thanks to these additional features, the site will be more user-friendly and attractive to users.

Initializing static for the site

For a quick start, StatiCL allows you to create the structure of a future site using a simple command. You can call it via Lisp REPL:

CL-USER> (staticl:new-site "/tmp/my-site"
                           "My Lisp Site"
                           "https://about-lisp.org")
#P"/tmp/my-site/"

Or you can call the same command from the command line if you installed StatiCL using Roswell - just open a terminal and enter this command to create the initial site config:

$ staticl --verbose new-site -o /tmp/my-site 'My Lisp Site' https://about-lisp.org
Site's content was written to: /tmp/my-site/

Let's look at what happened as a result:

$ tree -a /tmp/blah
/tmp/blah
├── .staticlrc
├── about.page
├── blog
│   ├── first.post
│   └── second.post
└── index.page

Here we see the site config .staticlrc, two regular pages: index.page and about.page, as well as two blog posts. And here are the settings generated for us:

(asdf:load-system "staticl/format/spinneret")
    
(site "My Lisp Site"
      :description "A site description."
      :url "https://about-lisp.org"
      :navigation (menu (item "Blog" "/blog/")
                        (item "About" "/about/"))
      :pipeline (list (load-content)
                      (prev-next-links)
                      (paginated-index :target-path "blog/")
                      (rss :target-path "blog/rss.xml")
                      (atom :target-path "blog/atom.xml")
                      (tags-index :target-path "tags/")
                      (sitemap))
      :theme "readable")

To create a site, the staticl/user-package:site function is used, which passes the site title, its description, the URL for publication, links for navigation and a description of the content processing pipeline. The pipeline plays a key role in the formation of the site, since the final result depends on it. To load the staticl/format/spinneret dependency, you need to call the asdf:load-system function. This module adds support for the Spinneret format, an example of which can be found in the about.post file. Now let's look at the contents of the pipeline.

The first call in the pipeline is staticl/user-package:load-content function. It is responsible for downloading content from files with the post and page extensions. Next comes the staticl/user-package:prev-next-links (1 2) block, it links the "post" type content together, due to which the Previous and Next links appear on the blog pages. The staticl/user-package:paginated-index (1 2) block is responsible for creating pages on which blog posts are grouped into N pieces. Here, in the example, the number of posts per page is not specified, but the staticl/user-package:paginated-index function can accept the :PAGE-SIZE argument, as well as some other arguments. The great news is that when you edit such a config in an editor that supports working with Common Lisp, the IDE will tell you what the signature of each function is and what parameters it can have. Try to do this with a static site generator that uses the YAML format for configuration!

Next we have two calls of staticl/user-package:rss function and staticl/user-package:atom function. They are similar in that they give a feed output from the latest blog posts. Well, the staticl/user-package:sitemap function generates a file sitemap.xml and includes all the content that was created in the previous stages.

Generating a static website

Now that we have some content, let's make an HTML website out of it. To do this, run the following command in REPL:

CL-USER> (staticl:generate :root-dir "/tmp/my-site"
                           :stage-dir "/tmp/result")
#P"/tmp/result/"

Or on the command line:

$ staticl -v generate -s /tmp/my-site -o /tmp/result
Site was written to: /tmp/result/

This command created several HTML files in the /tmp/result/ directory, and also put the necessary CSS and JS files there:

$ tree /tmp/result
/tmp/result
├── about
│   └── index.html
├── blog
│   ├── atom.xml
│   ├── first
│   │   └── index.html
│   ├── index.html
│   ├── rss.xml
│   └── second
│       └── index.html
├── css
│   ├── bootstrap.min.css
│   └── custom.css
├── img
│   ├── cc-by-sa.png
│   ├── glyphicons-halflings-white.png
│   ├── glyphicons-halflings.png
│   └── staticl-logo-small.webp
├── index.html
├── js
│   └── bootstrap.min.js
├── sitemap.xml
└── tags
    ├── bar
    │   └── index.html
    ├── example
    │   └── index.html
    └── foo
        └── index.html

Now we need to somehow open the site in the browser. If you just open the file in the browser index.html, then he will load it without styles, and other things may not work either, since the page opened in this way will not have a domain. To fully open the site, we need to launch a web server. Previously, I would have done this using python. This is how you can distribute static from a local directory using python and its http.server module:

$ cd /tmp/result
$ python3 -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

However, we can do better – a web server is already built into StatiCL. Moreover, it can track changes in your site's files and update the site pages opened in the browser. To start this local web server, use the following command in REPL:

CL-USER> (staticl:serve :root-dir #P"/tmp/my-site"
                        :stage-dir #P"/tmp/result")
 [21:35:55] staticl/utils utils.lisp (top level form find-class-by-name) -
  Searching class by name STATICL/UTILS::NAME: "closure-template"
  [21:35:55] staticl/server server.lisp (top level form serve) -
  Starting Clack server to serve site from /tmp/result/
Hunchentoot server is started.
Listening on localhost:8001.

Or from the command line:

$ staticl serve -s /tmp/my-site -o /tmp/result
  [21:39:48] staticl/server server.lisp (top level form serve) -
  Starting Clack server to serve site from /tmp/result/
Hunchentoot server is started.
Listening on localhost:8001.

As you can see, the serve command is very similar to generate, and accepts similar parameters, because it needs not only to distribute static, but also to generate it. In addition, the serve command will try to open the site in the browser. Look at it:

If you click on the Blog link, a page with a list of posts will open. This part of the pipeline is responsible for its generation: `(paginated-index :target-path #P"blog/")'. The page looks like this:

On the index page, StatiCL displays only the introduction if the source file of the post separates it from the main part with the line <!--more-->.

If you click on one of the posts, it will open in its entirety:

Pay attention to the "Next" link in the lower right corner. All blog posts are linked to each other and this is also done thanks to a separate pipeline block: `(prev-next-links)'. This pipeline block adds metadata to each post, which is then available in the template. If you remove it, the links will disappear from the pages.

To stop your server, use the following command in REPL:

CL-USER> (staticl:stop)
; No values

How to add comments using Disqus

Like any content creator, you will definitely want to communicate with your readers. The easiest way to do this is to connect dynamic comments to our static blog. There are many services that provide such comments – for example, Disqus, Commento, Remark42, etc..

At the moment, StatiCL only supports Disqus, but it's easy to write a plugin for any other comment system.

Let's add comments to our website! All you need to do is register with Disqus, get a short site name, and add another pipeline block to the config. Let's say I registered the site name example in Disqus, and then I need to add the `(disqus "example") block to our pipeline:

:pipeline (list (load-content)
                (prev-next-links)
                (paginated-index :target-path #P"blog/")
                (rss :target-path "blog/rss.xml")
                (atom :target-path "blog/atom.xml")
                (tags-index :target-path "tags/")
                (sitemap)
                (disqus "example"))

This is how the post page with comments will look like:

Content filtering

Now let's assume that we don't want to connect comments to all posts, but only to those where there is no no-comments tag. To do this, we can filter the content using the staticl/user-package:filter (1 2) block. This block accepts a number of parameters, as well as other pipeline blocks, which will receive only those content elements that have passed the filter. This is how the filter will look like, which will apply staticl/user-package:disqus function only to content that does not have the no-comments tag:

(filter (:tags "no-comments"
         :invert t)
        (disqus "example"))

Now add the no-comments tag to the blog/first.post file and make sure that comments are not displayed on the page of this post. They are on the page of the second post.

In the following tutorials, we will figure out how to create themes for your static website and learn how to add the necessary functionality using plugins.