惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
博客园_首页
宝玉的分享
宝玉的分享
S
SegmentFault 最新的问题
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
美团技术团队
IT之家
IT之家
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
博客园 - 叶小钗
M
MIT News - Artificial intelligence
B
Blog RSS Feed
有赞技术团队
有赞技术团队
Y
Y Combinator Blog

Vegard S. Hagen on Stonegarden

Direct Server Return with Cilium Direct Server Return with Cilium BGP with Cilium and UniFi BGP with Cilium and UniFi Custom OIDC claims with Argo CD and Audiobookshelf Custom OIDC claims with Argo CD and Audiobookshelf OpenID Connect with Authelia on Kubernetes OpenID Connect with Authelia on Kubernetes LLDAP — Declarative Selfhosted Lightweight Authentication LLDAP — Declarative Selfhosted Lightweight Authentication Demystifying Kubernetes RBAC and OIDC Auth Demystifying Kubernetes RBAC and OIDC Auth Postgres databases in Kubernetes Postgres databases in Kubernetes TalosCon & SREDay London 2024 TalosCon & SREDay London 2024 Talos Kubernetes on Proxmox using OpenTofu Talos Kubernetes on Proxmox using OpenTofu Kubernetes Proxmox Container Storage Interface Kubernetes Proxmox Container Storage Interface Intel Quick Sync Video with Kubernetes Intel Quick Sync Video with Kubernetes External services with Gateway API External services with Gateway API Kubernetes on Proxmox Kubernetes on Proxmox Bootstrapping K3s with Cilium Bootstrapping K3s with Cilium CUDA on Kubernetes CUDA on Kubernetes
Theme Dynamic Images
Vegard S. Hagen · 2023-11-19 · via Vegard S. Hagen on Stonegarden

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)