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

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
Project Zero
Project Zero
D
DataBreaches.Net
I
InfoQ
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Vercel News
Vercel News
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
I
Intezer
The Hacker News
The Hacker News
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
Help Net Security
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Scott Helme
Scott Helme
T
Threatpost
爱范儿
爱范儿
N
Netflix TechBlog - Medium
D
Docker
云风的 BLOG
云风的 BLOG
C
Cisco Blogs
K
Kaspersky official blog
H
Help Net Security
S
Secure Thoughts
T
Threat Research - Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security @ Cisco Blogs
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
G
Google Developers Blog
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
B
Blog
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
Simon Willison's Weblog
Simon Willison's Weblog
S
Securelist
P
Privacy International News Feed
Spread Privacy
Spread Privacy
The Last Watchdog
The Last Watchdog

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 Archived: Gateway API with Cilium and Cert-manager Gateway API v1.3.0 with Cilium and cert-manager Archived: Gateway API with Cilium and Cert-manager Gateway API v1.3.0 with Cilium and cert-manager Wildcard Certificates with Traefik Wildcard Certificates with Traefik Migrating from MetaLB to Cilium Migrating from MetaLB to Cilium Theme Dynamic Images mo.unit wiring mo.unit wiring Bio Bio Browser rendered terminal Browser rendered terminal Argo CD Kustomize with Helm Argo CD Kustomize with Helm Cloudflared SSH tunneling Cloudflared SSH tunneling Stonegarden Stonegarden
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)