I recently got interested in photography again after a many year hiatus. This was instigated by more frequent travel and by a large photo editing project I embarked on last year.
In 2011 and 2013, I did a 365 project where I made and posted a new photo every day. I averaged something like 30 photos a day—and kept everything—so there were a lot of mediocre photos to delete. It was a daunting editing task. To make it manageable, each day in 2025 I edited the photos from the corresponding day in 2011 and 2013. It was a great way to familiarize myself with my old photos, find some gems I forgot about, and free up a lot of space on my drives. It also made me miss the life-documenting aspect of carrying a camera all the time.
That process also piqued a desire for a new personal website where I could display some of my favorite photos and some artwork that I’ve produced over the years. I also wanted to set up a blog because I’ve had an impulse to write more too.
Implementation Details
The source code for this site is here.
I have another website, nybble, which is focused on in-depth technical content. For that project, I used the Haunt static site generator, which is written in Guile Scheme.
For this site, I wanted to create image galleries—something Haunt isn’t set up to do out of the box. Since I’ve been interested in Common Lisp for a long time, but have never written anything significant with it, I decided to use it to roll my own static site generator.
I used spinneret to generate HTML, and 3bmd to convert Markdown to HTML. I also wrote a tiny library, image-dimensions, to quickly return the width and height of an image. I wanted to include that data in the gallery HTML, so things didn’t jump around as the images loaded.
Extracting the dimensions of a PNG is easy because the data is always located in the same place at the beginning of the image. JPEGs require a little more work—you have to step through a sequence of marked segments until you find the frame segment that contains the dimensions.
One thing I like in Common Lisp is that mapping functions like mapcar and mapc can take a
function with an arbitrary number of parameters, and then a matching number of lists to provide the arguments to the
function. As soon as one of the lists is exhausted, the expression terminates.
I wanted the site generator to take a list of images (either photos or drawings) and create a unique page for each
one, with links to the previous and next images. Using mapcar with successive subsets of the image list
provides an easy way to get a series of (previous current next) triplets. Here’s a simplified example:
(let ((nums '(0 1 2 3 4 5)))
(mapcar #'list nums (cdr nums) (cddr nums)))
=> ((0 1 2) (1 2 3) (2 3 4) (3 4 5))
To make this loop, you just need to attach the last image at the front and the first image at the back, like so:
(let* ((nums '(0 1 2 3 4 5))
(nums (append (last nums) nums (list (car nums)))))
(mapcar #'list nums (cdr nums) (cddr nums)))
=> ((5 0 1) (0 1 2) (1 2 3) (2 3 4) (3 4 5) (4 5 0))
Here’s the actual function in the site generator. It uses a locally defined function, write-image-page,
to receive the (previous current next) triplets and write the corresponding page. It uses
mapc instead of mapcar because there’s no need to accumulate a result—I’m only interested in
the side effects.
(defun build-image-pages (path images)
"Build a web page in PATH for each image in IMAGES."
(flet ((write-image-page (prev curr next)
(write-html (str:concat path (name-with-extension curr ".html"))
(lambda () (image-page prev curr next)))))
;; attach the last image to the front, and the first image to the end
(let ((images (append (last images) images (list (car images)))))
;; zip the list with itself to get prev-curr-next triplets
(mapc #'write-image-page
images
(cdr images)
(cddr images)))))
And here’s the function that generates the HTML for the image page:
(defun image-page (prev image next)
"Build a web page for IMAGE with links to the PREV and NEXT images."
(with-page (:title "Steve Sprang" :script "/js/gallery-keys.js")
(:nav.image-nav
(:a#prev :href (name-with-extension prev ".html") "prev")
(:span "/")
(:a#next :href (name-with-extension next ".html") "next"))
(:div.presenter
(multiple-value-bind (width height)
(image-dimensions image)
(:img.presented :src (file-namestring image) :alt ""
:width width :height height)))))
Most of this project is straightforward because this site isn’t complicated, but it’s always amazing how much time I can spend tweaking things! In the future, I want to add support for an Atom feed and implement a mechanism to attach alt text to the gallery photos for better accessibility.