In a recent article I wanted one of the images to change dynamically with the theme.
I didn’t find any support for this in the Blowfish Hugo theme I’m using, so I had to get creative. I also wanted to reuse an earlier shortcode I experimented with for automatically resizing images.
The end result is a markup render-image hook and a custom dynamic-image shortcode that both use the same image partial.
Motivation#
We’ve generated two similar images using ImageMagick.
A dark version
magick -background none -fill "#333" -size 1000x250 -gravity center \
-stroke white -strokewidth 2 label:HELLO 01-hello-dark.gif
… and a light version
magick -background none -fill "#CCC" -size 1000x250 -gravity center \
-stroke black -strokewidth 2 label:HELLO 01-hello-light.gif
The markdown for rendering these images are
![Hello](images/01-hello-dark.gif "Dark grey 'HELLO'")
![Hello](images/01-hello-light.gif "Light grey 'HELLO'")
We want the dark image to show when we’re using a dark theme, and the light image to show when we’re using a light theme. We also want to be able to toggle between both versions independent of which theme we’re using.
By using the shortcode defined below this boils down to
{{< dynamic-image title="title" alt="alt" dark="images/01-hello-dark.gif" light="images/01-hello-light.gif" >}}
which should render “HELLO” in either dark grey for the dark theme, or light grey if you like to burn your retinas.
We can also statically render both images using regular markdown image syntax.
Implementation#
So how is this all tied together?
By creating a partial which we invoke in both our markup render image hook and our dynamic image shortcode we can avoid duplicating code. This should make it easier to maintain and refine by only having to change code one place.
Partial#
We’ve created the following partial under layouts/partials
as image.html
{{- $page := .page }}
{{- $url := .url }}
{{- $title := .title }}
{{- $alt := .alt }}
{{- $id := .id }}
{{- $resource := "" }}
{{/* Look for the resource in the given page bundle */}}
{{- if $page.Resources.GetMatch ($url.String) }}
{{- $resource = $page.Resources.GetMatch ($url.String) }}
{{/* Look for the resource global and remote scope */}}
{{- else if resources.GetMatch ($url.String) }}
{{- $resource = resources.Get ($url.String) }}
{{- end }}
{{/* Flag for rendering change button */}}
{{- $dynamic := .dynamic | default false }}
{{/* Possible sizes for resizing */}}
{{- $sizes := slice "340" "600" "960" "1280" "1600" "1920" "2560" "3840" -}}
{{- with $resource }}
<figure style="margin: 0.0em; padding-top: 0.25em" id="{{ $id }}">
<div style="display: flex; justify-content: center">
<img class="my-0 rounded-md" style="display: grid; place-items: center"
{{- if eq .MediaType.SubType "svg" }}
{{/* Don't resize vector images */}}
src="{{ .RelPermalink }}"
{{- else }}
srcset="
{{- with $sizes -}}
{{- range $i, $e := . -}}
{{- if ge $resource.Width . -}}
{{- if $i }}, {{ end -}}{{- ($resource.Resize (printf " %sx%s" . " webp") ).RelPermalink }} {{ . }}w
{{- end -}}
{{- end -}}
{{- end -}}"
src="{{ (.Resize "660x webp").RelPermalink }}"
{{- end }}
{{ with $alt }} alt="{{ . }}" {{ end }}
loading="lazy"
style="padding-bottom: 0.5em" />
</div>
<figcaption style="padding: 0.8em 0.5em 0.5em; margin-block-start: 0.0em; line-height: 1.0em; font-size: 0.75em; text-align: center;">
{{ with $title }} {{ . }} {{ end }}
{{- /* Link to full size image */}}
(<a href="{{ .RelPermalink | safeURL }}" target="_blank">full size</a>)
{{- /* Button for changing image */}}
{{ if $dynamic }}<button id="{{ $id }}-button">[<a>☾/☼</a>]</button>{{ end }}
</figcaption>
</figure>
{{- else }}
--- RESOURCE {{ $url }} NOT FOUND ---
{{- end }}
Since we don’t have access to the .Page
attribute in a partial we have to supply it manually together with the url
to the image, a title
, an alt
-attribute, and an (optional) id
.
We then use the .Page
element to do to a lookup for the image in both page- and global space.
The optional dynamic
flag is used to render a button-element which is used to change images individually.
Bitmap images are resized to conserve bandwidth for smaller screens while simultaneously offering higher quality images
for bigger screens using
the srcset
-attribute.
Markup render image hook#
Next we override the markup image render hook by creating render-image.html
under layouts/_default/_markup
and
invoke our image partial.
{{- $url := urls.Parse (.Destination) -}}
{{ partial "image.html" (dict "page" .Page "url" $url "title" .Title "alt" .Text) }}
We supply the partial with the .Page
attribute and parse the url
from the .Destination
attribute supplied from
the markup code.
The title
and alt
variables are taken directly from the .Title
and .Text
markup properties respectively.
Dynamic image shortcode#
The first part of the dynamic image shortcode is fairly similar to the markup render image hook.
We provide title
and alt
-attributes to be shared by both the dark and light version and supply separate URLs for
the dark
and light
images.
We’re basically calling the image
partial twice with slightly different input.
{{- $dark := urls.Parse (.Get "dark") }}
{{- $light := urls.Parse (.Get "light") }}
{{- $title := .Get "title" }}
{{- $alt := .Get "alt" }}
{{- $id := .Get "id" | default (print $dark.String $light.String) }}
{{- $darkId := print $id "-dark" }}
{{- $lightId := print $id "-light" }}
{{ partial "image.html" (dict "page" .Page "url" $dark "id" $darkId "title" $title "alt" $alt "dynamic" true) }}
{{ partial "image.html" (dict "page" .Page "url" $light "id" $lightId "title" $title "alt" $alt "dynamic" true) }}
<script>
//{{/* Use `md5` to render the id to a legal function name and chain it with `safeJS` to remove quotation marks */}}
function change{{ $id | md5 | safeJS }}(){
document.getElementById(`{{ $id }}-light`).hidden = getTargetAppearance() !== "light"
document.getElementById(`{{ $id }}-dark`).hidden = getTargetAppearance() === "light"
}
window.addEventListener("DOMContentLoaded", () => {
// Change when theme is changed
const switcher{{ $darkId | md5 | safeJS }}Theme = document.getElementById("appearance-switcher");
if (switcher{{ $darkId | md5 | safeJS }}Theme) {
switcher{{ $darkId | md5 | safeJS }}Theme.addEventListener("click", change{{ $id | md5 | safeJS }})
}
const switcherMobile{{ $darkId | md5 | safeJS }}Theme = document.getElementById("appearance-switcher-mobile");
if (switcherMobile{{ $darkId | md5 | safeJS }}Theme) {
switcherMobile{{ $darkId | md5 | safeJS }}Theme.addEventListener("click", change{{ $id | md5 | safeJS }})
}
// Change individual images
const switcher{{ $darkId | md5 | safeJS }}DarkButton = document.getElementById("{{ $id }}-dark-button");
if (switcher{{ $darkId | md5 | safeJS }}DarkButton) {
switcher{{ $darkId | md5 | safeJS }}DarkButton.addEventListener("click", () => {
document.getElementById(`{{ $id }}-light`).hidden = false
document.getElementById(`{{ $id }}-dark`).hidden = true
})
}
const switcher{{ $darkId | md5 | safeJS }}LightButton = document.getElementById("{{ $id }}-light-button");
if (switcher{{ $darkId | md5 | safeJS }}LightButton) {
switcher{{ $darkId | md5 | safeJS }}LightButton.addEventListener("click", () => {
document.getElementById(`{{ $id }}-dark`).hidden = false
document.getElementById(`{{ $id }}-light`).hidden = true
})
}
});
// Invoke the change-function to hide the unwanted image
change{{ $id | md5 | safeJS }}()
</script>
The responsiveness happens in the <script>
part where we hide certain elements based on their ID and the current theme
or selection.
To stop two different dynamic images interfering with each other we define unique function names for each of them.
This is done by taking the md5
-hash of the ID to get a legal function name and pass it to safeJS
to remove the
quotation marks around the result.
In the script we’re using
the Blowfish Theme function getTargetAppearance()
to get the current theme.
The appearance-switcher
and appearance-switcher-mobile
elements are also Blowfish specific.
Other themes might have similar functions and elements that can be used if you want to port this functionality.
The {{ $id }}-dark-button
and {{ $id }}-light-button
elements are the optional “dynamic” button from the image
partial.
By creating an event listeners on these elements we can change the hidden
attribute for the corresponding image.
At the end of the script we’re invoking the change{{ $id | md5 | safeJS }}()
-function once in order to hide the
unwanted image.
This means that if the reader has disabled Javascript both the dark and light image will show.
Other uses#
The images could also be completely different and could give a different experience based on which theme you’ve chosen, though in the current iteration they would share the same title and alt-text.
Having a versatile partial should make further image-centric shortcodes easier to write.