I’ve been thinking about adding some kind of photo implementation on this blog since I first set it up. I didn’t really know what I wanted, or how I wanted to use it - so I’ve been putting it off.

The last couple of weeks I’ve been more interested in photography, even borrowed a macro lens to see if that is something I enjoyed (it was 👍)

It’s time to tackle the photo implementation!

TLDR; jump to implementation.
Table of contents

I gave a lot of thought to whether the photos should have their own post, like BrianLi is doing, or be part of a gallery.

Single photo post

Initially I decided that one photo per post was the most flexible, as I could freely organize them into one or more taxonomies. A photo could be part of one gallery, many galleries, or no galleries.

I thought about the URL structure, and eventually decided that each photo would get a unique ID instead of using slugs. That would make my photo URLs something lie /photos/fjh43g/, and galleries /galleries/plants/.

But once I actually started using it — I found that it was difficult to mange, even with the helper script I had made.

If I added 20 photos, 20 posts were created. If I wanted to add or alter the taxonomies for those photos, I had to edit 20 index.md files. In my RSS feed; 20 new “posts”.

I abandoned the “one-photo-one-post” approach and tried making gallery posts instead.

Photo gallery post

The more I thought about the concept of “photo gallery posts” — the more sense it started to make. I rarely take just one photo, so each gallery post would be a “photo shoot”.

And it is more manageable 😃 Instead of 20 posts, I will have one post, with 20 photos. For taxonomies I can use tags, or series, or something else — I haven’t decided yet. It’s easier to make those decisions later, when I’ve used it for a while.

Naming things is hard

I did think way too much about what to call such a post — photo post, gallery post, album post? Bike-shedding for sure. In the end I decided to not call it anything. It’s a post. The underlying type is not really important. I did add a camera icon, indicating that the post is photo related.


To make the galleries, I’m only using CSS Flexbox. For showing individual photos, and its title and metadata, I am using lightgallery.js.

This isn’t a step-by-step guide, but if you know the Hugo template system — you should understand what is going on 😃

Here is the part of the layout that creates the gallery:

<div class="photo-container" id="lightgallery">
{{- $context := . -}}
{{- range $src := .Page.Resources.Match "gallery/*.jpg" -}}
  {{- $title := (trim .Title "gallery/") -}}

  {{- $exifJson := $context.Page.Resources.GetMatch (printf "%s.json" .Name) -}}

  {{- $exif := slice -}}
  {{- with (index ($exifJson.Content | unmarshal) 0) -}}
    {{- with .Title -}}{{- $title = . -}}{{- end -}}
    {{- with .Make2 -}}{{- $exif = $exif | append (printf "Make: %s" .) -}}{{- end -}}
    {{- with .Model -}}{{- $exif = $exif | append (printf "Camera: %s" .) -}}{{- end -}}
    {{- with .LensSpec -}}{{- $exif = $exif | append (printf "Lens: %s" .) -}}{{- end -}}
    {{- with .FocalLength -}}{{- $exif = $exif | append (printf "Focal length: %s" .) -}}{{- end -}}
    {{- with .FNumber -}}{{- $exif = $exif | append (printf "Aperture: ƒ/%.1f" .) -}}{{- end -}}
    {{- with .ExposureTime -}}
      {{- if eq (printf "%T" .) "float64" -}}
        {{- $exif = $exif | append (printf "Exposure time: %.1f s" .) -}}
      {{- else -}}
        {{- $exif = $exif | append (printf "Exposure time: %s s" .) -}}
      {{- end -}}
    {{- end -}}
    {{- with .ISO -}}{{- $exif = $exif | append (printf "ISO: %.0f" .) -}}{{- end -}}
  {{- end -}}

  <div class="photo-item" data-src="{{ .Permalink }}" data-sub-html="<h4>{{ $title }}</h4><p>{{ delimit $exif " | " }}</p>">
    {{- $crop := default "smart" -}}
    {{- $tinyw := printf "500x375 %s Lanczos q85" $crop -}}
    {{- $smallw := printf "800x600 %s Lanczos q80" $crop -}}
    {{- $mediumw := printf "1200x900 %s Lanczos q70" $crop -}}
    {{- $largew := printf "1600x1200 %s Lanczos q60" $crop -}}

    {{- $srcset := slice -}}

    {{- $tiny := ($src.Fill $tinyw) -}}
    {{- $srcset = $srcset | append (printf "%s 500w" $tiny.Permalink) -}}
    {{- $img := dict "src" $tiny.RelPermalink "w" $tiny.Width "h" $src.Height -}}

    {{- if and (ge $src.Width "800") (ne $src.MediaType.SubType "png") -}}
        {{- $small := ($src.Fill $smallw) -}}
        {{- $srcset = $srcset | append (printf "%s 800w" $small.Permalink) -}}
        {{- $img = dict "src" $small.RelPermalink "w" $small.Width "h" $small.Height -}}
    {{- end -}}
    {{- if and (ge $src.Width "1200") (ne $src.MediaType.SubType "png") -}}
        {{- $medium := ($src.Fill $mediumw) -}}
        {{- $srcset = $srcset | append (printf "%s 1200w" $medium.Permalink) -}}
    {{- end -}}
    {{- if and (ge $src.Width "1600") (ne $src.MediaType.SubType "png") -}}
        {{- $large := ($src.Fill $largew) -}}
        {{- $srcset = $srcset | append (printf "%s 1600w" $large.Permalink) -}}
    {{- end -}}

    <a href="{{ .Permalink }}">
    <img loading="lazy" class="center"
      sizes="(min-width: 900px) 420px, (min-width: 684px) 310px, calc(100vw - 40px)"
      srcset='{{ delimit $srcset ", " }}'
      src="{{ $img.src }}"
      width="{{ $img.w }}" height="{{ $img.h }}" alt="{{ $title }}">
{{ end }}

It looks for page resources matching gallery/*.jpg, each photo is expected to have a json file with metadata at <filename>.jpg.json. Multiple sizes of the photo is created, and added to the image source set.

EXIF data is added to the $exif array, and shown below the photo when opened in the lightbox. If the EXIF data contains a title tag, this is used, otherwise the page resource title.

A bit of (S)CSS is also needed:

.photo-container {
    display: flex;
    flex-wrap: wrap;
    margin: 0 -40px;

    @media(max-width:899px) {
        margin: 0;

    img {
        border-radius: 8px;

    .photo-item {
        padding: 2px;
        width: 50%;

        @media(max-width:683px) {
            width: 100%;

To conditionally load the lightgallery.js library, I have the following code in the head partial:

{{- if and (eq .Type "photos") (eq .Kind "page") -}}
  <link rel="stylesheet" href="{{ (resources.Get "assets/lightgallery.min.css" | fingerprint).Permalink }}" />
{{- end -}}

And in the footer:

{{- if and (eq .Type "photos") (eq .Kind "page") -}}
  <script src="{{ (resources.Get "assets/lightgallery.min.js" | fingerprint).Permalink }}"></script>
  <script>lightGallery(document.getElementById('lightgallery'), {selector: '.photo-item'});</script>
{{- end -}}

This is the command I use to save EXIF metadata to json files:

exiftool ${@:1} -w %f.%e.json -json -struct \
    -EXIF:All \
    -XMP:Title \

I think I struck a nice balance between simplicity and functionality. See for yourself — here is my first photo gallery post 😃