Addie's place on the web...

Just some stuff I want to share with you

Fun with Jekyll plugins

Sunday 28 June 2020
Jekyll

Earlier this year, I started to rebuild my website and decided to rebuild it with a static site generator: Jekyll. While building the site, I had some design principles in mind:

  • The HTML pages should contain as little clutter as possible.
  • The site should look good on smartphones, tablets and computers.
  • Try to limit the amount of processing that needs to be done by the browser.

This requires processing of the photos and GPS tracks while generating the static HTML files. To do that, there are lots of plugins out there that can help. I found quite some plugins that can-do parts of the trick, but in the end, I still ended up with issues. But hey, I am an IT-guy; I can build a plugin myself, right?

Display the photos

There are more than 5.000 photos on the website, and I need a nice way of showing them in a kind of a photo album. But next to that, I also need an easy way to include them into my posts. My search resulted in 2 interesting components: Justified Gallery and lightGallery.

Justified Gallery takes a set of photos and shows them nicely as small pictures next to each other. A great way to show which photos are available in a folder.

When you click on a single picture, it will show up full screen. This is where the magic of lightGallery starts; this will display a single photo by using the file that best matches the screen resolution of the device.

To make all this work, we need to take care of 2 things: have a list of photos and make sure that we have several files with different resolutions.

Now there are quite some Jekyll plugins out there that work nicely, but all of them do only parts of the story. Time to get creative and build my own plugin.

Step 1: get the photos organized

My plugin groups the photos into albums and galleries. A gallery can only contain photos. An album can have albums and galleries.

I considered to store the information in yaml files as data and then processing them. But eventually, I ended up storing them as markdown files and store the data as front matter into them.

Here is an example of an album:

---
layout: album
album_name: 2009_australia_adelaide
album_title: 2009 Australia Adelaide
album_description: Exploring Adelaide
album_path: _gallery/2009_australia/Adelaide
parent_album: 2009_australia
date: 2009-05-08
---

And here is one for a gallery:

---
layout: gallery
gallery_name: 2009_australia_adelaide_20090508
gallery_title: 2009 Australia Adelaide - Kangaroo Island Day 1
gallery_description: Exploring Kangaroo Island day 1
gallery_image_tags:
    owner: Addie Janssen
gallery_path: 20090508
parent_album: 2009_australia_adelaide
date: 2009-05-08
---
Step 2: process the pages

Now it is time to generate the pages that represent the albums and galleries. This is done using layouts; one for the album and another for the gallery.

My plugin introduces the following tags that can be used in the layouts:

  • momentum_album
  • momentum_gallery
  • momentum_slider
  • momentum_image

As you may have guessed: I have named my plugin “momentum”.

The layouts use momentum_album and momentum_gallery to retrieve information about these objects. The next step is to use Liquid to format the final output. I may share the plugin in the future and document how things work in detail, but for now, everything is way too experimental.

The layouts are setup in such a way that they result in the correct HTML output for both Justified Gallery and lightGallery.

When using these tags, they trigger another process: resizing.

Step 3: resizing the images

For lightGallery to choose the correct photo-file to use, it needs to have options to choose from.

Let us have a look at my _config.yml file:

momentum:
  image:
    path: "/images"
    resize:
      thumb: "x150"
      ls300: "300"
      ls500: "500"
      ls750: "750"
      ls1000: "1000"
      ls1500: "1500"
      ls2500: "2500"

When one of the momentum tags is used, they trigger a resize process. For each original photo, the plugin generates 7 new files and the plugin keeps an administration of these.

The actual resizing is done using ImageMagick. During the same step, I also strip all Exif tags and apply only a limited set of tags on the resized images. The Exif processing is done using MiniExiftool and MultiExiftool; both are wrappers for ExifTool which does the actual work.

I will need to revisit this step in the future; the current process takes a lot of time (over 30 hours for the current 5.000+ photos). There are faster image processing libraries out there and the way Exiftool works is extremely slow. It is on the (long) to-do list.

Step 4: generate the HTML files

Once all this is done, we can use some Liquid loops in the layouts:

<div id="{{ etag }}">
    {%- for image in images %}
    <a href="{{image.highres.path}}" data-responsive="{{image.responsive}}">
        <img src="{{image.thumb.path}}" height="{{image.thumb.height}}" width="{{image.thumb.width}}">
    </a>
    {%- endfor %}
</div>
<script>
    momentumDisplaySlider('{{ etag }}');
</script>

Which then results in:

<div id="09c3c913d6ae40b165222f2287f3f974">

<a href="/images/2009_australia_adelaide_20090508_ls2500/P1040655.JPG"
	data-responsive="/images/2009_australia_adelaide_20090508_ls300/P1040655.JPG 300,
				/images/2009_australia_adelaide_20090508_ls500/P1040655.JPG 500w,
				/images/2009_australia_adelaide_20090508_ls750/P1040655.JPG 750w,
				/images/2009_australia_adelaide_20090508_ls1000/P1040655.JPG 1000w,
				/images/2009_australia_adelaide_20090508_ls1500/P1040655.JPG 1500w ">
    <img src="/images/2009_australia_adelaide_20090508_thumb/P1040655.JPG">
</a>

...

</div>
<script>
    momentumDisplaySlider('09c3c913d6ae40b165222f2287f3f974');
</script>

Every album and gallery have a unique etag. As shown above, that is used to as an id for the div element. When a momentum gallery is included in a HTML file, the same file also includes a reference to momentum.js. That script contains the definition of momentumDisplaySlider; this is where we integrate with Justified Gallery and lightGallery:

function momentumDisplaySlider(etag) {

    $('#' + etag).justifiedGallery({
        border: 0,
        margins: 4,
        lastRow: 'nojustify'
    }).on('jg.complete', function () {
        $('#' + etag).lightGallery({
            mode: 'lg-slide',
            preload: 3,
            enableDrag: false,
            download: false,
            hash: false,
            thumbnail: false,
            share: false,
            autoplay: false,
            autoplayControls: false,
            fullScreen: true,
            zoom: true
        })
    });
}

And that’s it regarding photo albums and galleries.

Display the GPS tracks

Once I return from a holiday, I like to see where I have been. In a previous post, I mentioned the GPS trackers I used so far. Each of them had a slightly different way to produce a GPS track. Over time, this has resulted in a stack of NMEA, GPX, KML and KMZ files; all slightly different.

While searching for ways to display the tracks, I came across Leaflet. Leaflet can be extended using plugins and most of my file could be displayed on a map using them. This would require knowledge on the browser side about the files I have and that’s not what I was planning to do.

Some of these files not only contain GPS information, but also have markup (line coloring, thickness, etc.) in them. If I would display them on a map using a Leaflet plugin, I would have to take that into account as well.

This was not the way I wanted to go; eventually, I ended up with a slightly different approach. Time to add some more functionality to my momentum plugin.

Step 1: get the GPS tracks organized

I used the same approach as with the photos. The information is stored in trips and tracks. A track is an actual GPS track. A trip can have trips and tracks.

Here is an example of a trip:

---
layout: trip
trip_name: 2009_australia_adelaide
trip_title: 2009 Australia Adelaide
trip_description: Exploring Adelaide
trip_path: _tracks/2009_australia/Adelaide
parent_trip: 2009_australia
date: 2009-05-08
---

And here is one for a track:

---
layout: track
track_name: 2009_australia_adelaide_20090508
track_title: 2009 Australia Adelaide - Kangaroo Island Day 1
track_description: Exploring Kangaroo Island
track_path: 20090508-KangarooIsland.kml
track_type: kml
parent_trip: 2009_australia_adelaide
date: 2009-05-08
---
Step 2: process the trip and track pages

My plugin introduces the following tags that can be used in the layouts and posts:

  • momentum_trip
  • momentum_track
  • momentum_singletrack

The layouts use momentum_trip and momentum_track to retrieve information about these objects.

When using these tags, they trigger a conversion process.

Step 3: convert the GPS files

When one of the momentum tags are used, they trigger a conversion process. Instead of linking directly to the NMEA, GPS, KMZ or KML files, they are converted into something that Leaflet natively supports: a polyline.

As a result, there is no need for Leaflet plugins or any conversion by the browser.

A polyline is an array of latitude and longitude coordinates. There is no markup in them and during the conversion, I remove any duplicates.

Depending on the original GPS track, these arrays can get big. To prevent that the client has to download a big file, the actual array is compressed using the deflate algorithm.

Step 4: generate the HTML files

In the layout pages, we can now use Liquid loops to generate the output we need:

<div id="{{ etag }}" style="height: 600px;"></div>
<script>
	momentumDisplayMap('{{ etag }}', '{{ polylines[0].path }}')
</script>

Which then results in:

<div id="c2eb2529289e29c1167a0109bc902cd2" style="height: 600px;"></div>
<script>
	momentumDisplayMap('c2eb2529289e29c1167a0109bc902cd2', '/tracks/2009_australia_adelaide_20090508/c2eb2529289e29c1167a0109bc902cd2.polyline')
</script>

Every trip and track have a unique etag to identify them. Like with the photos, a link to momentum.js is included in the HTML file to provide the implementation of momentumDisplayMap:

function momentumDisplayMap(etag, path) {

    var xhr = new XMLHttpRequest();
    xhr.open("GET", path, true);
    xhr.responseType = "arraybuffer";

    xhr.onload = function () {

        var compressed = new Uint8Array(xhr.response);
        var uncompressed = JSON.parse(pako.inflate(compressed, { to: 'string' }));
        var polyline = L.polyline(uncompressed, { weight: '5' });

        var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        });

        var bing = new L.BingLayer('Avvr3d0Psw4D2Lwi9iSTr6DU8EryZoMAdxiH19x7GMwqaZXW7p3DWyyui9Qpy8a5', {
            imagerySet: 'AerialWithLabelsOnDemand'
        });

        var baseMaps = {
            "OpenStreetMap": osm,
            "Bing - Aerial": bing
        };

        var map = L.map(etag, {
            fullscreenControl: true,
            layers: [osm, polyline]
        });
        map.fitBounds(polyline.getBounds());
        L.control.layers(baseMaps).addTo(map);
    }

    xhr.send();
}

The momentumDisplayMap function will download the polyline file; decompress it and then plot it on a map. The current setup uses both OpenStreetMap and Bing Maps. As you can see, there is a key to authenticate with Bing Maps; you can try to copy it and use in in your own application or website, but that will not work. Bing Maps has some nice features to prevent cross site scripting. If you want to use Bing Maps; hop over to the dev center and get your free key.

And that is how the maps get generated and displayed.

Conclusion

I am happy with the result for now; it does the trick. There is little work to add another photo gallery or album or a GPS trip or track. And by using the momentum tags in my layouts, pages and posts, I can quickly add single pictures or tracks or full albums, galleries or trips to my posts.

There are probably many more ways to accomplish what I did here. But it was not only to get my site up and running; I wanted to figure out how this can be done. I added a few items on my to-do list along the way; so, the setup will probably change over the next months.

Next step is to start looking into some styling.


Want to respond to this post?
Look me up on Twitter, Facebook or LinkedIn.