Skip to main content
  1. Articles/

Theme Dynamic Images

·1422 words·7 mins·
Vegard S. Hagen
Author
Vegard S. Hagen
Pondering post-physicists
Table of Contents

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

Hello
Dark grey 'HELLO' (full size)

 magick -background none -fill "#333" -size 1000x250 -gravity center \
        -stroke  white -strokewidth 2 label:HELLO 01-hello-dark.gif

… and a light version

Hello
Light grey 'HELLO' (full size)

 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.

HELLO
Theme dynamic 'HELLO'. Click the ☾/☼ symbol to change. (full size)
HELLO
Theme dynamic 'HELLO'. Click the ☾/☼ symbol to change. (full size)

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.

Danbo
Danbo either drawing a heart or enjoying the outdoors (full size)
Danbo
Danbo either drawing a heart or enjoying the outdoors (full size)