Static photo albums with llgal


I used to create photo albums on flickr for our vacations. I even had a paid pro account for a few years. While I have backups of all my photos, it still felt off to show these albums using somebody else's computer. They might disappear any day. Since I have the storage capacity and some time to waste I set out to self-host photo albums.

Nextcloud Photos

My first layer of photo backups is a self-hosted Nextcloud. I am using the Nextcloud android app to automatically upload pictures when on WiFi. So I might as well use the built-in Nextcloud Photos 2.01.

Unfortunately it's not quite there yet. In increasing order of annoyance:

  1. Video buffering is not a thing.
  2. Videos do not get a preview image.
  3. It is very confused about ordering. The order in the photo app is correct but when added to the gallery sometimes two photos switch places. There is some mumbling about mtime vs. ctime vs. exiff data in some forum, but that's not the problem here. First of all, no matter how Nextcloud sorts the picture it would always arrive at the same ordering. I did not edit the photos, so mtime and ctime are the same. But also note that the order in the photo app is correct. It only gets confused when photos are added to an album. The photo album tells a story. It is really bad if the "arrival at home" photo is shown before the "last night of the vacation" photo.
  4. The absolute deal-breaker is that it does not work though. The whole idea is to share a link to the album for anonymous users. You can do that, and it shows you the album with thumbnails. But as soon as you click on a photo or start the slide show it wants you to login. I suspect you need to enable sharing via link to every photo in the album to make it work. There does not seem to be a way to do this in bulk so I did not try it. I have also found a discussion online where people argue for years along the lines of "it does not work" - "yes it does" - "no it does not". That was tiresome and they are still going on. There was some mumbling that you need to enable federation in Nextcloud to make anonymous albums work, but I have that on and it still does not work.


I then remembered that I had used a static photo album generator before: llgal. But I never tried to make it "pretty" or convert all my flickr albums to it.


llgal is in OpenBSD ports, so it can be installed with pkg_add. I store all my albums as sub-folders in /var/www/ and then run llgal. I was experimenting a bit with the command line flags to figure out what I need.

I then setup a ~/.llgal/llgalrc file. With that in place I can run llgal in the top level photos directory and it will (re-)create all albums as needed.

I downloaded a sample file from GitHub and edited for my needs. You can see the resulting file here.

These are the things I configured:

thumbnails_per_row = 3
thumbnail_height_max = 240
thumbnail_width_max = 240
MVI_link_to_target = 1
DIR_link_to_target = 1
slide_width_max = 700
slide_height_max = 700
show_all_exif_tags = 1
credits_text = "Copyright © 2014 - 2023 Florian Obser. All rights reserved."
exclude = "^js$"
sort_criteria = "revtime"
recursive = 1
link_subgalleries = 1

Video thumbnails

llgal does not create thumbnails for video files (yet) so I had to hack around that a bit. First we use ffmpeg to create the thumbnails:

for i in *.mp4; do
    ffmpeg -ss 1 -i ${i} -frames:v 1 -vf "scale=240:-1" \

We grab one frame (-frames:v 1), one second into the video (-ss 1), and scale it to 240 pixels wide while keeping the aspect ratio (-vf "scale=240:-1"). We store the thumbnail in the .llgal sub-directory, where llgal stores its own thumbnails and scaled images. The config file has this:

# Additional prefix of user-provided scaled images
# user_scaled_image_filenameprefix = "my"

# Additional prefix of user-provided thumbnails
# user_thumbnail_image_filenameprefix = "my"

I have not worked out what that does, but I am using the "my" prefix because of this.

llgal does not pick up these thumbnails but we can use the captions file to show them. First we are creating /captions files in all albums: llgal --gc. This creates .llgal/captions in all albums.

It creates lines for all photos and videos that we can edit, for videos it looks like this:

MVI: VID_20230120_124000.mp4 ---- Open movie VID_20230120_124000.mp4 ----

And we can change it to this to show the thumbnail2:

MVI: VID_20230120_124000.mp4 ---- <img src=".llgal/my_thump_VID_20230120_124000.mp4.jpg" /><br /> Open movie ----

I am using the same trick to have thumbnails for the top-level album overview. The only difference is that it is a DIR line instead of MVI.

Keyboard and swipe navigation.

llgal links to as an example. Those albums use hammer.js for swipe navigation and mousetrap for keyboard navigation. I have copied slidetemplate.html from /usr/local/share/llgal to ~/.llgal and edited it: slidetemplate.html. We need to include the JavaScript files in the header:

<script type="text/javascript" src="/js/mousetrap.min.js"></script>
<script type="text/javascript" src="/js/hammer.min.js"></script>

Set the id for the navigation links:

<p class="center">
  <a id="prevslide" href="<!--PREV-SLIDE-->"><!--PREV-SLIDE-LINK-TEXT--></a>
  &nbsp; &nbsp; &nbsp;
  <a id="indexlink" href="<!--INDEX-FILE-->"><!--INDEX-LINK-TEXT--></a>
  &nbsp; &nbsp; &nbsp;
  <a id="nextslide" href="<!--NEXT-SLIDE-->"><!--NEXT-SLIDE-LINK-TEXT--></a>

And then hook up JavaScript to the navigation links:

<script type="text/javascript">
    // Script for keyboard navigation
    var prev = document.getElementById("prevslide").href;
    var next = document.getElementById("nextslide").href;
    Mousetrap.bind('left',  function () { location.href = prev; });
    Mousetrap.bind('right', function () { location.href = next; });
    Mousetrap.bind('h',     function () { location.href = indexlink; });
    Mousetrap.bind('j',     function () { location.href = prev; });
    Mousetrap.bind('k',     function () { location.href = next; });

<script type="text/javascript">
    // Script for touch gestures (Hammerjs)
    var slide = document.getElementById('slide-container');
    var mc = new Hammer(slide);
    mc.on("swiperight", function(ev) { location.href = next; });
    mc.on("swipeleft",  function(ev) { location.href = prev; });

This seems to disable pinch-zooming on the image itself on android. I have not figured out why that is. The hammerjs documentation talks about pinch, but my understanding is that it should just work. It does not though…


This was not a lot of work, please enjoy the first album from our trip to Malta in 2023. Time permitting I will convert more albums from flickr and google photos in the future.



Why is there not way to link to a description of this thing? There is a GitHub page and it's described on but you can't direct-link to it‽


I am using an emacs macro for that. YMMV.

Published: 2023-01-29