Blogging with Haunt and Scheme

2020-03-31, by Natalie Pendragon

scheme lisp technology colophon

I'm beginning to think I may enjoy recreating my blog with different technologies more than I actually enjoy blogging! So, here's an account of my latest blog re-creation with Guile Scheme and Haunt.

The background context for this adventure is that I've been spending some time in 2020 learning Lisp and Scheme. So when I came across Haunt, and was almost immediately impressed by the minimalist aesthetic and nice code quality, I thought it would be a nice exercise to recreate my blog using it.

I had also recently learned of Edward Tufte , his unique handout style, and the CSS created to mimic it. It's an interesting and compelling set of design principles, and serendipitously gets to the root of some of the "design excesses" so prevalent in modern web design. Sidenotes in place of footnotes, when the aspect ratio is wider than a comfortable column of text, I find especially nice!

Haunt also supports SkribeSee also Skribilo, which is another layer of power and document processing coolness, built on top of Skribe., which was my first exposureBut not my last exposure! I recently came across Hoplon, which is a ClojureScript web framework that appears to also completely pave over HTML with LISP. to a Lisp-y representation of HTML. The way I understand it is that it works well because HTML is, functionally, a subset of what you can represent with Lisp-type syntax, so it works. There's some documentation in the Haunt manual, but here is an example of what it looks like in practice:

 :title "Hello, Skribe!"
 :date (make-date* 2016 08 20 12 00)
 :tags '("skribe" "foo" "baz")

 (h2 [This is a Skribe post])

 (p [Skribe is a ,(em [really]) cool document authoring format
     that provides all the power of Scheme whilst giving the user
     a means to write literal text without stuffing it into a
     string literal. If this sort of thing suits you, be sure to
     check out ,(anchor "Skribilo"
                        ""), too.]))

Okay, that's a lot of context. Now I'll show you some of the code I implemented to get my own working setup with all of this. To start with, here's the file and directory structure:

├── drafts/
├── haunt.scm
├── posts
   ├── post1.skr
   └── ...etc
└── static
    ├── css
       ├── et-book/
       ├── tufte-blog.css
       └── tufte.css
    ├── images/
    └── root
        ├── android-chrome-192x192.png
        ├── android-chrome-512x512.png
        ├── apple-touch-icon.png
        ├── favicon-16x16.png
        ├── favicon-32x32.png
        ├── favicon.ico
        ├── gpg.txt
        └── site.webmanifest

It's pretty simple, and most of the complexity lives in haunt.scm file at the root of the project. That file contains, more or less, the following (eliding some of the imports and other basic bits):

(define (tufte-header)
  '(header  (@ (class "nav")) (ul (li (a (@ (href "/")) "Natalie Pendragon"))
                                  (li "·")
                                  (li (a (@ (href "/about.html")) "About"))
                                  (li "·")
                                  (li (a (@ (href "/index.html")) "Archive"))
                                  (li "·")
                                  (li (a (@ (href "/feed.xml")) "Atom")))))

(define (tufte-footer)
  '(footer (div "Made with " (a (@ (href "")) "Emacs") " and " (a (@ (href "")) "Haunt"))))

(define (tufte-layout site title body)
  `((doctype "html")
     (meta (@ (http-equiv "Content-Type") (content "text/html; charset=UTF-8")))
     (meta (@ (http-equiv "Content-Language") (content "en")))
     (meta (@ (name "author") (content "Natalie Pendragon")))
     (meta (@ (name "viewport") (content "width=device-width")))
     (title ,(or title (if site (site-title site) "")))
     ,(stylesheet "tufte")
     ,(stylesheet "tufte-blog")
     (link (@ (rel "shortcut icon")
              (href "favicon.ico"))))
    (body ,(tufte-header)
          (article ,body)

(define (tufte-post-template post)
  `((h1 ,(post-ref post 'title))
    (p (@ (class "subtitle")) ,(date->string* (post-date post)) " — by " ,(post-ref post 'author))
    (section ,(post-sxml post))
    (p ,(post-ref post 'tags))))

(define (tufte-collection-template site title posts prefix)
  (define (post-uri post)
    (string-append (or prefix "") "/"
                   (site-post-slug site post) ".html"))
  `((h3 ,title)
     ,@(map (lambda (post)
                (a (@ (href ,(post-uri post)))
                   ,(post-ref post 'title)
                   ,(date->string* (post-date post)))))

(define tufte-theme
  (theme #:name "tufte"
         #:layout tufte-layout
         #:post-template tufte-post-template
         #:collection-template tufte-collection-template))

You can see here how a general page layout is defined, including the header and the footer, then used within the definition of a theme. The theme is Tufte-flavored, but is defined in the format that Haunt expects for any theme. You could also skip this entirely, to use the default theme, and get up and running quickly. Note that the header, footer, and layout all use the Skribe-style syntax I mentioned earlier.

Next, I define a few static pages with Skribe, and finally finish up with a site definition that ties everything together for Haunt.

(define (about-page site posts)
  (define body
    `((h1 "Hi.")))
  (make-page "about.html"
             (with-layout tufte-theme site "About" body)

(site #:title ""
      #:domain ""
      '((author . "Natalie Pendragon")
        (email  . ""))
      #:readers (list commonmark-reader skribe-reader sxml-reader)
      #:builders (list (blog #:theme tufte-theme)
                       (static-directory "static/root" "/")
                       (static-directory "static/images" "images")
                       (static-directory "static/css" "css")))

And that's about it!