Warum das Ganze?#

Mein Theme kann aus dem klassischen Markdown ![Image Alt Text](/path/to/image/) keine Bildformatierung und -Positionierung ableiten. Das benötige ich aber, um Bilder links, mittig, und rechts zu platzieren - mit und ohne Textfluss. Also muss ich selbst ran und mir etwas programmieren.

Einstieg in shortcodes, layouts, css#

Einen Überblick verschaffe ich mir auf cshire und stelle schnell fest, dass mein erster Versuch per /layouts/render-image.html nicht funktioniert.

Debugging#

Auf diese Weise stelle ich fest, ob render-image.html überhaupt gelesen und angewendet wird - und zwar unabhängig von CSS:

<div style="border: 3px dashed red; padding: 10px; margin: 10px; font-weight: bold;">
  RENDER-IMAGE.HTML IS BEING READ!
</div>

Das funktioniert. Also muss Hugo irgendwas in der Markdown-Verarbeitung entgehen.

Ursachenbehebung#

Ich sehe nun, dass Hugo zwar das Layout rendert, meine Bilder aber nicht richtig auswertet. Warum? Weil zur Auswertung der Bilder ein Auslöser, genannt shortcode, fehlt. Shortcodes in Hugo werden mit {{ <shortcode-name> parameter1="String" parametern="string" }} im Markdown angelegt, mittels der entsprechenden Vorschrift /layouts/shortcodes/<shortcode-name>.html ausgewertet und anschließend durch CSS interpretiert dargestellt.

Shortcode: image.html#

Ich habe mir also ein Shortcode-Template gebastelt, sodass ich dem Bild ein paar weitere Parameter zusätzlich zu Quelldatei src und Alternativtext alt mitgeben kann: position, caption, attrid, attrlink. Position und Titel des Bildes sind selbsterklärend. Eine Attributionsmöglichkeit wollte ich für nicht selbsterstellte Bilder haben. Bei diesen greife ich entweder auf offene Inhalte unter Rechtsverhältnissen wie den creative commons zu oder besorge mir vorab das schriftliche Einverständnis der Urheberrechtsinhaberschaft.

{{- /* layouts/_shortcodes/image.html */ -}}

{{- $src := .Get "src" -}}
{{- $alt := .Get "alt" | default "" -}}
{{- $position := .Get "position" | default "center" -}}
{{- $caption := .Get "caption" -}}
{{- $attrid := .Get "attrid" -}}
{{- $attrlink := .Get "attrlink" -}}

<figure class="media-frame media-frame--{{ $position }}">
  <img src="{{ $src }}" alt="{{ $alt }}">

  {{- if $caption -}}
    <figcaption class="media-caption">
      <span class="caption-text">{{ $caption }}</span>
      {{- if and $attrid $attrlink -}}
        <a
          href="{{ $attrlink }}"
          class="attr-link"
          aria-label="Attribution {{ $attrid }}"
        >
          <sup class="attr-id">[{{ $attrid }}]</sup>
        </a>
      {{- end -}}
    </figcaption>
  {{- end -}}
</figure>

Diese Vorlage liest die mitgegebenen Parameter aus dem Shortcode aus und packt das Bild <img> in einen Rahmen <figure class="media-frame">. Dieser trägt die Positionsinformation. Sofern ein Bildtitel und/oder eine Attribution vorliegt, wird dies in der figcaption class="media-caption" abgebildet.

Verwendung des Shortcodes#

In Markdown würde ein Bild mit “Vollausstattung” nun so aussehen:

{{< image src="/path/to/image.jpg" 
alt="Example placeholder image for shortcode demonstration purposes" 
caption="Shortcode demo image" 
attrid="1" attrlink="https://example.com/" 
position="right" >}}

Migration#

Nur wie komme ich jetzt von den bisher von mir in Jekyll verwendeten ![alt](/path/to/image) {: .align-center} in das oben angegebene Format?

Mit einem Skript! Dafür erstelle ich mir ein python-Programm, welches durch jeden meiner Artikel kämmt und dort nach Bildern in der oben angegebenen Formatierung sucht. Bei einem Treffer extrahiert es src, alt, position und ersetzt den kompletten Ausdruck zu einem Hugo-Shortcode-Äquivalent.

import os
import re
from pathlib import Path

# spaces/tabs only, no newlines
SPACES = r'[ \t]*'

IMG_WITH_OPTIONAL_ALIGN = re.compile(
    r'!\[([^\]]*?)\]\(([^)]*?)\)'      # alt, src
    + SPACES +                         # optional spaces/tabs before decorator
    r'(?:\{:' + SPACES +               # literal "{:"
    r'[^}\n]*?\.align-(center|left|right)\b[^}\n]*?'  # decorator body (no newlines)
    r'\})?'                           # optional "}"
)

def convert_image_alignments(directory):
    def repl(m):
        alt = m.group(1).replace('"', "'")
        src = m.group(2)
        position = m.group(3) or ''
        # return built Hugo shortcode. Escape Python f-string curly brace { with {{.
        return f'{{{{< image src="{src}" alt="{alt}" position="{position}" >}}}}'

    for root, _, files in os.walk(directory):
        for file in files:
            if not file.endswith('.md'):
                continue

            filepath = Path(root) / file
            content = filepath.read_text(encoding='utf-8')

            new_content = IMG_WITH_OPTIONAL_ALIGN.sub(repl, content)
            if new_content != content:
                filepath.write_text(new_content, encoding='utf-8')
                print(f"Updated: {filepath}")

if __name__ == "__main__":
    project_root = input("Enter the path to your project root: ").strip()
    if os.path.isdir(project_root):
        convert_image_alignments(project_root)
        print("Conversion complete!")
    else:
        print("Invalid directory path.")

Bei den Regexes habe ich mir von Mistral helfen lassen, ansonsten ist das Skript aber einfach gehalten.

Bilder mit Titel und/oder Attribution waren in Jekyll als figure deklariert. Auch für diese schreibe ich ein ganz ähnliches Skript und lasse es bei der Migration durchlaufen.

CSS#

Kommen wir zur Darstellung der Bilder. Ich erstelle eine seitenfüllende CSS-Datei /assets/css/media.css, welche außerdem Größe und Formatierung anderer eingebetteter Medien wie Audio- und Videodateien einheitlich sowohl für die mobile- als auch die Desktopansicht regeln soll. Dafür greift sie auf die in der Vorlage definierten class=-Attribute zu.

Ich nehme zur Erläuterung mal einen Ausschnitt aus der Datei heraus: Hier lege ich das Verhalten des media-frame-Objektes fest. Jedes Asset (Bild, Video, Audio) wird in einem Rahmen eingebettet, welcher der Darstellung die nötige Luft verschafft. Mit dem Attribut position= wird definiert, wo und wie dargestellt werden soll.

Dabei sind die Inhalte standardmäßig zentriert und werden solange sie nicht größer sind als die maximale Textbreite in ihrer nativen Auflösung dargestellt.

.media-frame img,
.media-frame audio,
.media-frame video {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;

  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
}

.media-frame--center {
  margin-left: auto;
  margin-right: auto;
}

/* Left/Right align only for Non-mobile use */
@media (min-width: 768px) {
  .media-frame--left {
    float: left;
    max-width: 60%;
    margin-top: 0.35em;
    margin-right: 1rem;
    margin-bottom: 0.75rem;
  }

  .media-frame--right {
    float: right;
    max-width: 60%;
    margin-top: 0.35em;
    margin-left: 1rem;
    margin-bottom: 0.75rem;
  }
}

Auf einer Seite ausgerichtete Bilder werden in der Breite begrenzt, denn es soll noch Textfluss drum herum stattfinden. Außerdem werden minimale Abstände zu anderen Objekten definiert, damit sich bei aufeinanderfolgenden Medien nicht Rahmen an Rahmen reibt. In der mobilen Ansicht ist der Platz so beengt, dass Bilder stets zentriert dargestellt werden.

Galerie#

Für den Fall, dass ich viele Bilder an einer zentralen Stelle anzeigen will, erstelle ich mir zusätzlich einen gallery-Shortcode und zugehöriges CSS. Die Galerie zeigt Bilder in einem Raster an. Bewegt man die Maus über eines der Bilder, so erscheint es heller als die anderen. Klickt man auf ein Bild, so erscheint es in groß am unteren Rand der Galerie, wobei der oben bereits beschriebene media-Shortcode wiederverwendet wird.

Die darzustellenden Bilder werden in der front matter des Markdown-Dokumentes als Liste per src und alt angelegt. Hier ein Ausschnitt aus der Vorlage zur Auswertung des Shortcodes.

<section class="hugo-gallery">
  <div class="hugo-gallery__frame">
    <div class="hugo-gallery__grid" role="list">
      {{- range $i, $img := $items -}}
        {{- $src := $img.src -}}
        {{- $alt := $img.alt | default (printf "Gallery image %d" (add $i 1)) -}}

        {{- $thumbURL := $src -}}
        {{- $p := strings.TrimPrefix $src "/assets/" -}}

        {{- with resources.Get $p -}}
          {{- $thumb := .Fit "400x220" -}}
          {{- $thumbURL = $thumb.RelPermalink -}}
        {{- end -}}

        <a
          class="hugo-gallery__thumb"
          href="#gallery-full-{{ $i }}"
          aria-label="{{ $alt }}">
          <img
            src="{{ $thumbURL }}"
            alt="{{ $alt }}"
            class="hugo-gallery__thumb-img"
            loading="lazy">
        </a>
      {{- end -}}
    </div>

Ich iteriere durch alle in der Frontmatter definierten Bilder, begrenze ihre Größe auf handliche 400x220px und ordne sie in einem Raster class=hugo-gallery__grid an.

Tests#

Das Erstellen der Vorlagen ging verhältnismäßig schnell. Mit CSS stehe ich aber auf Kriegsfuß. Ständig funkt mir irgendein Override aus einer anderen Datei dazwischen oder ich komme mir selbst mit den Styles im Basis-CSS in die Quere. Daher benötige ich einen Ort zum Testen von Formatierung und Anordnung.

Daher habe ich eine Bild-testseite erstellt. Neugierig? Prüfen der Darstellung von Medien in Hugo