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.
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.
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.
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.
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:
- The
$urlvariable captures the current URL passed as an argument from the partial call insingle.html. - A regex pattern extracts the
$photoset_idand$user_idfrom the Flickr URL. - If the Flickr API key (set in
config.tomlasParams.flickr.flickrApiKey), it constructs a request to list the contents of the album (photoset in Flickr API speak). - 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-boxdiv.
As an example output:
Show example HTML output
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
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/







