Search this site

Automatically Generating Photo Box Galleries in Hugo

Because who doesn't like ordering a box of photos and spilling them all over a table?

Automatically Generating Photo Box Galleries in Hugo

I enjoy photography. I’ve intermittently used Flickr to upload pictures I’ve captured for a long time, I’m a big proponent of sharing my work too so any photos not featuring an obviously identifiable human subject I release under a Creative Commons license, typically CC BY-NC-ND. Having shifted my blog to Hugo, I was bored and thought I could do something creative.

The concept of photo boxes sprang to mind – the sort you order that arrives brimming with pictures which you then splay out at various angles (let’s be honest, even with OCD, it’s probably impossible to get them straight). Such a tangible experience is deeply satisfying, and I thought, why not replicate this on the internet? Integrating a few services I use, particularly given Flickr’s user-friendly API, seemed straightforward.

Screenshot of the generated page This page was automatically generated from the example below.

Hugo Modifications

Post Frontmatter

As a lazy coder, I was struck by an idea: what if I could transform entire Hugo posts into mere frontmatter? I could leave the rest to be handled by code after simply linking to one of my Flickr albums.

freezing-layover-in-zurich-switzerland.md.yaml Source
1---
2title: Freezing Layover in Zurich, Switzerland
3description: "Grounded in Zürich after some bad weather but my heart's up in the clouds. Unexpected layovers lead to unexpected friendships—particularly when you'd rather be airborne-shoutout to my new seagull (snowgull?) mates!"
4date: 2023-12-03 01:00:00 +0000
5flickr_embed: 'https://www.flickr.com/photos/83515912@N03/sets/72177720313855068'
6image: '/images/freezing-layover-in-zurich-switzerland.jpg'
7tags: [Album]
8---

Layouts and Partials

Hugo’s templating engine is impressively powerful. To avoid cluttering the main layout file and to facilitate more complex requests, I employed layouts and partials. Initially, I embedded the following in layouts/_default/single.html.

single.html Source
1<!-- this should be embedded within single.html at the right place -->
2{{ if .Params.flickr_embed }}
3  {{ partial "flickr-embed.html" .Params.flickr_embed }}
4{{ end }}

This section calls a partial, akin to a function, that queries the Flickr API and returns links and image embeds for all images in the Flickr album.

flickr-embed.html Source
 1{{ $url := . }}
 2{{ $photoset_id := "" }}
 3{{ $user_id := "" }}
 4{{ $regex := `https:\/\/w+?\.?flickr\.com\/photos\/([a-zA-Z0-9_@]+)\/(albums|sets|photosets)\/([0-9]+)\/?` }}
 5{{ $matches := findRE $regex $url }}
 6
 7{{ if $matches }}
 8  {{ $user_id = replaceRE $regex "$1" $url }}
 9  {{ $photoset_id = replaceRE $regex "$3" $url }}
10{{ end }}
11
12{{ with site.Params.flickr.flickrApiKey }}
13  {{ $api_key := . }}
14  {{ $api_url := printf "%s%s%s%s%s" "https://api.flickr.com/services/rest/?format=json&method=flickr.photosets.getPhotos&photoset_id=" $photoset_id "&page=1&api_key=" $api_key "&nojsoncallback=1" }}
15
16  {{ $flickr := getJSON $api_url }}
17  {{ if $flickr }}
18    <div class="photo-box">
19        {{ with $flickr.photoset }}
20          {{ range .photo }}
21            <a href="https://www.flickr.com/photos/{{ $user_id }}/{{ .id }}" target="_blank">
22              <img src="https://c1.staticflickr.com/{{ .farm }}/{{ .server }}/{{ .id }}_{{ .secret }}_b.jpg" alt="{{ .title }}" loading="lazy">
23            </a>
24          {{ end }}
25        {{ end }}
26    </div>
27  {{ end }}
28{{ end }}

Now let’s review what that’s doing:

  1. The $url variable captures the current URL passed as an argument from the partial call in single.html.
  2. A regex pattern extracts the $photoset_id and $user_id from the Flickr URL.
  3. If the Flickr API key (set in config.toml as Params.flickr.flickrApiKey), it constructs a request to list the contents of the album (photoset in Flickr API speak).
  4. The response from the Flickr API is then used to generate HTML, displaying and linking each image (with lazy loading) from the Flickr album in a photo-box div.

As an example output:

Show example HTML output
example-output.html Source
 1<div class="photo-box">
 2<a href="https://www.flickr.com/photos/83515912@N03/53444202842" target="_blank">
 3<img src="https://c1.staticflickr.com/66/65535/53444202842_2286302d06_b.jpg" alt="R0000560" loading="lazy">
 4</a>
 5<a href="https://www.flickr.com/photos/83515912@N03/53444202807" target="_blank">
 6<img src="https://c1.staticflickr.com/66/65535/53444202807_4f2452d4f7_b.jpg" alt="R0000561" loading="lazy">
 7</a>
 8<a href="https://www.flickr.com/photos/83515912@N03/53444202782" target="_blank">
 9<img src="https://c1.staticflickr.com/66/65535/53444202782_41f0b5171f_b.jpg" alt="R0000568" loading="lazy">
10</a>
11<a href="https://www.flickr.com/photos/83515912@N03/53445263598" target="_blank">
12<img src="https://c1.staticflickr.com/66/65535/53445263598_89f8870916_b.jpg" alt="R0000575" loading="lazy">
13</a>
14<a href="https://www.flickr.com/photos/83515912@N03/53445538565" target="_blank">
15<img src="https://c1.staticflickr.com/66/65535/53445538565_85d6bc6566_b.jpg" alt="20231203_153608" loading="lazy">
16</a>
17<a href="https://www.flickr.com/photos/83515912@N03/53444202542" target="_blank">
18<img src="https://c1.staticflickr.com/66/65535/53444202542_97dcc85d1c_b.jpg" alt="R0000579" loading="lazy">
19</a>
20<a href="https://www.flickr.com/photos/83515912@N03/53444202507" target="_blank">
21<img src="https://c1.staticflickr.com/66/65535/53444202507_50e6bf7740_b.jpg" alt="R0000585" loading="lazy">
22</a>
23<a href="https://www.flickr.com/photos/83515912@N03/53445447724" target="_blank">
24<img src="https://c1.staticflickr.com/66/65535/53445447724_60f74acbd6_b.jpg" alt="R0000590" loading="lazy">
25</a>
26<a href="https://www.flickr.com/photos/83515912@N03/53445263418" target="_blank">
27<img src="https://c1.staticflickr.com/66/65535/53445263418_aa59539309_b.jpg" alt="R0000593" loading="lazy">
28</a>
29<a href="https://www.flickr.com/photos/83515912@N03/53445263428" target="_blank">
30<img src="https://c1.staticflickr.com/66/65535/53445263428_2d7b2b230e_b.jpg" alt="R0000596" loading="lazy">
31</a>
32<a href="https://www.flickr.com/photos/83515912@N03/53444202257" target="_blank">
33<img src="https://c1.staticflickr.com/66/65535/53444202257_2ae198bf86_b.jpg" alt="R0000597" loading="lazy">
34</a>
35<a href="https://www.flickr.com/photos/83515912@N03/53445538525" target="_blank">
36<img src="https://c1.staticflickr.com/66/65535/53445538525_1e1b43e44e_b.jpg" alt="20231203_164707" loading="lazy">
37</a>
38<a href="https://www.flickr.com/photos/83515912@N03/53445538235" target="_blank">
39<img src="https://c1.staticflickr.com/66/65535/53445538235_bcd7f6d1a6_b.jpg" alt="R0000604" loading="lazy">
40</a>
41</div>

Theming

While functionality is crucial, I really did want this photo box vibe. To achieve this, I turned to SCSS for its added flexibility (but mostly because I was already using it).

Show photo-box SCSS
_photo-box.scss Source
 1$photoRotation: 4deg; // Allows setting the alternating photo rotation
 2$breakpoints: (
 3  1200: 4,
 4  // 4 columns for screens wider than 1200px
 5  800: 3,
 6  // 3 columns for screens wider than 800px
 7  600: 2,
 8  // 2 columns for screens wider than 600px
 9  400: 1 // 1 column for screens wider than 400px,
10);
11
12.photo-box {
13  display: flex;
14  flex-wrap: wrap;
15  justify-content: center;
16}
17
18.photo-box a {
19  display: inline-block;
20  position: relative;
21  margin: 5px;
22  flex-basis: calc(25% - 10px); // 25% for 4 images in a row, minus margin
23  max-width: 25%; // Ensures no more than 4 images per row
24  text-align: center;
25  z-index: 1; // Low z-index for non-hovered items
26  transform: rotate(-$photoRotation); // Default rotation
27
28  &:nth-child(even) {
29    transform: rotate(-$photoRotation); // Subtle left rotation for even items
30  }
31  &:nth-child(odd) {
32    transform: rotate($photoRotation); // Subtle right rotation for odd items
33  }
34  img {
35    width: 100%;
36    height: auto;
37    transition: transform 0.4s ease-out;
38    box-shadow: 1.5px 2px 5px #0008;
39  }
40
41  &:hover {
42    z-index: 999; // High z-index and zoom for hovered items to mimic them being picked up
43
44    img {
45      transform: scale(2);
46      box-shadow: 3px 4px 10px #0006;
47    }
48  }
49}
50
51// Use the breakpoints map to calculate the correct flex-basis and max-width for each breakpoint
52@each $breakpoint, $columns in $breakpoints {
53  @media (max-width: #{$breakpoint}px) {
54    .photo-box a {
55      flex-basis: calc(100% /#{$columns} - 10px);
56      max-width: calc(100% /#{$columns});
57    }
58  }
59}

The .photo-box class is the main container. It utilizes flexbox to arrange the images neatly and centers them within the container.

Within each image container the anchors (.photo-box a) are styled. Even and odd elements are subtly rotated in opposite directions, this gives that spilled photo box look I was aiming for, and there is a zooming effect when you hover over each image. Hovering also changes the z-index to bring the selected image to the forefront as otherwise they’d still be clipped behind successive elements (…I spent 30 minutes trying to figure out why I didn’t like it before that).

The individual images (.photo-box a img) are styled to include a smooth transition effect when you hover over them. When I was first playing around with it I felt as though it lacked depth so added a subtle box shadow.

To ensure the layout remains responsive, the code incorporates breakpoints using media queries. These breakpoints adjust the number of images displayed in a row based on the screen width (up to a maximum of 4 but this can be configured).

This SCSS approach can be readily adapted to create similar photo grids, or you could embrace the entire concept to automatically generate Hugo albums from Flickr. But for me, I have this nice little photo box asthetic and can now use the best tool for the job (Flickr for album organisation) while still benefiting from it automatically. You can see all of these automatically generated albums here: https://blog.mitcdh.au/tags/album/

Rowing Across the Sunset
Older post

Rowing Across the Sunset

Newer post

November Festivities, Vienna

November Festivities, Vienna