8 Min. Lesezeit

Hugo Shortcodes – Interaktive Komponenten für Markdown

Hugo Shortcodes selbst erstellen: Schritt-für-Schritt von einfachen Wrappern bis zu interaktiven Code-Demos mit Live-Preview. Inkl. Code-Beispiele.

Markdown ist simpel – und genau das ist seine Stärke. Aber manchmal braucht man mehr: eine Info-Box, einen eingebetteten CodePen, Tabs oder eine Bildergalerie. Genau dafür gibt es Hugo Shortcodes. Sie erweitern Markdown um wiederverwendbare Komponenten, ohne dass man HTML direkt in Artikel schreiben muss.

Was sind Shortcodes?

Shortcodes sind kleine Template-Snippets, die man in Markdown-Dateien aufruft:

Normaler Markdown-Text...

{{< info >}}
Das hier ist eine Info-Box!
{{< /info >}}

Und weiter gehts mit Text.

Hugo ersetzt den Shortcode-Aufruf durch das gerenderte HTML. Die Shortcode-Dateien liegen in layouts/shortcodes/.

layouts/
└── shortcodes/
    ├── info.html
    ├── codepen.html
    └── tabs.html

Eingebaute Shortcodes

Hugo bringt einige Shortcodes mit:

<!-- YouTube einbetten -->
{{< youtube dQw4w9WgXcQ >}}

<!-- Vimeo einbetten -->
{{< vimeo 146022717 >}}

<!-- GitHub Gist -->
{{< gist user 123456789 >}}

<!-- Tweet einbetten -->
{{< tweet user="1453110110599868418" >}}

<!-- Syntax-gehighlighteter Code mit Dateinamen -->
{{< highlight javascript "linenos=true,hl_lines=3" >}}
const greeting = 'Hallo';
const name = 'Welt';
console.log(`${greeting}, ${name}!`);
{{< /highlight >}}

<!-- Relativer Link zu anderer Seite -->
{{< ref "blog/anderer-artikel.md" >}}

Eigene Shortcodes erstellen

1. Einfache Info-Box

Datei: layouts/shortcodes/info.html

<div class="info-box">
  <span class="info-box__icon">ℹ️</span>
  <div class="info-box__content">
    {{ .Inner | markdownify }}
  </div>
</div>

Verwendung:

{{< info >}}
**Hinweis:** Hugo Shortcodes werden zur Build-Zeit gerendert,
nicht im Browser. Das macht sie schnell und SEO-freundlich.
{{< /info >}}

CSS:

.info-box {
  display: flex;
  gap: 1rem;
  padding: 1rem;
  background: #e8f4fd;
  border-left: 4px solid #2196f3;
  border-radius: 0.25rem;
  margin: 1.5rem 0;
}

.info-box__icon {
  font-size: 1.5rem;
  flex-shrink: 0;
}

.info-box__content p:last-child {
  margin-bottom: 0;
}

2. Verschiedene Alert-Typen

Datei: layouts/shortcodes/alert.html

{{ $type := .Get 0 | default "info" }}
{{ $icons := dict
  "info" "ℹ️"
  "warning" "⚠️"
  "danger" "🚨"
  "success" "✅"
  "tip" "💡"
}}
{{ $icon := index $icons $type | default "ℹ️" }}

<div class="alert alert--{{ $type }}">
  <span class="alert__icon">{{ $icon }}</span>
  <div class="alert__content">
    {{ .Inner | markdownify }}
  </div>
</div>

Verwendung:

{{< alert "warning" >}}
Diese API ist deprecated und wird in v3.0 entfernt.
{{< /alert >}}

{{< alert "tip" >}}
Mit `Cmd + Shift + P` öffnest du die Command Palette in VS Code.
{{< /alert >}}

{{< alert "danger" >}}
**Achtung:** Diese Aktion kann nicht rückgängig gemacht werden!
{{< /alert >}}

3. CodePen einbetten

Datei: layouts/shortcodes/codepen.html

{{ $user := .Get "user" }}
{{ $slug := .Get "slug" }}
{{ $height := .Get "height" | default "400" }}
{{ $theme := .Get "theme" | default "dark" }}
{{ $tabs := .Get "tabs" | default "result" }}
{{ $title := .Get "title" | default "CodePen" }}

<div class="codepen-wrapper">
  <p
    class="codepen"
    data-height="{{ $height }}"
    data-theme-id="{{ $theme }}"
    data-default-tab="{{ $tabs }}"
    data-slug-hash="{{ $slug }}"
    data-user="{{ $user }}"
  >
    <span>
      <a href="https://codepen.io/{{ $user }}/pen/{{ $slug }}">
        {{ $title }}
      </a> auf <a href="https://codepen.io">CodePen</a> ansehen.
    </span>
  </p>
</div>

Am Ende der Seite (oder in baseof.html):

{{ if .HasShortcode "codepen" }}
  <script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
{{ end }}

Verwendung:

{{< codepen user="css-tricks" slug="abcdEFG" title="Flexbox Demo" tabs="css,result" >}}

4. Tabs

Datei: layouts/shortcodes/tabs.html

{{ $id := .Get "id" | default (printf "tabs-%d" .Ordinal) }}

<div class="tabs" id="{{ $id }}">
  {{ .Inner }}
</div>

Datei: layouts/shortcodes/tab.html

{{ $name := .Get "name" }}
{{ $active := .Get "active" | default false }}

<input
  type="radio"
  name="tabs-{{ .Parent.Ordinal }}"
  id="tab-{{ .Parent.Ordinal }}-{{ .Ordinal }}"
  class="tabs__input"
  {{ if $active }}checked{{ end }}
>
<label for="tab-{{ .Parent.Ordinal }}-{{ .Ordinal }}" class="tabs__label">
  {{ $name }}
</label>
<div class="tabs__content">
  {{ .Inner | markdownify }}
</div>

CSS (Pure CSS Tabs):

.tabs {
  display: flex;
  flex-wrap: wrap;
  margin: 1.5rem 0;
}

.tabs__input {
  position: absolute;
  opacity: 0;
}

.tabs__label {
  padding: 0.75rem 1.25rem;
  background: #f5f5f5;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: all 0.2s;
}

.tabs__label:hover {
  background: #eee;
}

.tabs__input:checked + .tabs__label {
  background: #fff;
  border-bottom-color: #2196f3;
  font-weight: 600;
}

.tabs__content {
  display: none;
  width: 100%;
  padding: 1rem;
  border: 1px solid #ddd;
  border-top: none;
  order: 1;
}

.tabs__input:checked + .tabs__label + .tabs__content {
  display: block;
}

Verwendung:

{{< tabs >}}

{{< tab name="JavaScript" active="true" >}}
```javascript
const greeting = 'Hallo Welt';
console.log(greeting);

{{< /tab >}}

{{< tab name=“Python” >}}

greeting = "Hallo Welt"
print(greeting)

{{< /tab >}}

{{< tab name=“Go” >}}

package main

import "fmt"

func main() {
    fmt.Println("Hallo Welt")
}

{{< /tab >}}

{{< /tabs >}}


### 5. Bild mit Caption

**Datei:** `layouts/shortcodes/figure.html`

```html
{{ $src := .Get "src" }}
{{ $alt := .Get "alt" | default "" }}
{{ $caption := .Get "caption" }}
{{ $width := .Get "width" }}
{{ $class := .Get "class" | default "" }}

<figure class="figure {{ $class }}">
  <img
    src="{{ $src }}"
    alt="{{ $alt }}"
    {{ with $width }}width="{{ . }}"{{ end }}
    loading="lazy"
  >
  {{ with $caption }}
    <figcaption class="figure__caption">
      {{ . | markdownify }}
    </figcaption>
  {{ end }}
</figure>

Verwendung:

{{< figure
  src="/images/screenshot.png"
  alt="VS Code mit geöffnetem Terminal"
  caption="**Abbildung 1:** VS Code Workspace Setup"
  width="800"
>}}

6. Interaktive Code-Demo

Datei: layouts/shortcodes/demo.html

{{ $html := "" }}
{{ $css := "" }}
{{ $js := "" }}

{{ range .Inner | split "---SPLIT---" }}
  {{ $parts := . | split "---" }}
  {{ range $parts }}
    {{ if hasPrefix . "HTML" }}
      {{ $html = . | replaceRE "^HTML\\s*" "" }}
    {{ else if hasPrefix . "CSS" }}
      {{ $css = . | replaceRE "^CSS\\s*" "" }}
    {{ else if hasPrefix . "JS" }}
      {{ $js = . | replaceRE "^JS\\s*" "" }}
    {{ end }}
  {{ end }}
{{ end }}

<div class="demo" id="demo-{{ .Ordinal }}">
  <div class="demo__code">
    {{ with $html }}
    <details open>
      <summary>HTML</summary>
      <pre><code class="language-html">{{ . | htmlEscape }}</code></pre>
    </details>
    {{ end }}

    {{ with $css }}
    <details>
      <summary>CSS</summary>
      <pre><code class="language-css">{{ . | htmlEscape }}</code></pre>
    </details>
    {{ end }}

    {{ with $js }}
    <details>
      <summary>JavaScript</summary>
      <pre><code class="language-javascript">{{ . | htmlEscape }}</code></pre>
    </details>
    {{ end }}
  </div>

  <div class="demo__preview">
    <strong>Ergebnis:</strong>
    <iframe
      srcdoc="<style>{{ $css }}</style>{{ $html }}<script>{{ $js }}</script>"
      sandbox="allow-scripts"
      loading="lazy"
    ></iframe>
  </div>
</div>

Einfachere Alternative – Nur HTML/CSS:

Datei: layouts/shortcodes/css-demo.html

{{ $css := .Get "css" }}
{{ $html := .Inner }}

<div class="css-demo">
  <div class="css-demo__code">
    <pre><code class="language-css">{{ $css | htmlEscape }}</code></pre>
  </div>
  <div class="css-demo__preview">
    <style scoped>{{ $css | safeCSS }}</style>
    {{ $html | safeHTML }}
  </div>
</div>

Verwendung:

{{< css-demo css=".box { width: 100px; height: 100px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }" >}}
<div class="box"></div>
{{< /css-demo >}}

7. Details/Akkordeon

Datei: layouts/shortcodes/details.html

{{ $summary := .Get "summary" | default "Details anzeigen" }}
{{ $open := .Get "open" | default false }}

<details class="details" {{ if $open }}open{{ end }}>
  <summary class="details__summary">{{ $summary }}</summary>
  <div class="details__content">
    {{ .Inner | markdownify }}
  </div>
</details>

Verwendung:

{{< details summary="Warum ist der Himmel blau?" >}}
Kurzwelliges blaues Licht wird in der Atmosphäre stärker gestreut
als langwelliges rotes Licht (Rayleigh-Streuung).
{{< /details >}}

Shortcode-Parameter

Hugo unterstützt zwei Arten von Parametern:

Positional Parameters

<!-- layouts/shortcodes/youtube.html -->
{{ $id := .Get 0 }}
<iframe src="https://youtube.com/embed/{{ $id }}"></iframe>
{{< youtube dQw4w9WgXcQ >}}

Named Parameters

<!-- layouts/shortcodes/button.html -->
{{ $text := .Get "text" }}
{{ $url := .Get "url" }}
{{ $style := .Get "style" | default "primary" }}

<a href="{{ $url }}" class="btn btn--{{ $style }}">{{ $text }}</a>
{{< button text="Jetzt kaufen" url="/shop" style="success" >}}

Alle Parameter auslesen

{{ range $key, $value := .Params }}
  {{ $key }}: {{ $value }}<br>
{{ end }}

Nützliche Variablen

<!-- Inner Content (zwischen öffnendem und schließendem Tag) -->
{{ .Inner }}

<!-- Inner als Markdown gerendert -->
{{ .Inner | markdownify }}

<!-- Seiten-Kontext -->
{{ .Page.Title }}
{{ .Page.Permalink }}

<!-- Shortcode-Position (für eindeutige IDs) -->
{{ .Ordinal }}

<!-- Parent Shortcode (bei verschachtelten Shortcodes) -->
{{ .Parent }}

<!-- Prüfen ob Parameter existiert -->
{{ if .Get "title" }}...{{ end }}

<!-- Prüfen ob Seite diesen Shortcode nutzt (für bedingte Assets) -->
{{ if .Page.HasShortcode "codepen" }}
  <script src="..."></script>
{{ end }}

Best Practices

1. Shortcode-spezifische Assets laden

<!-- In baseof.html oder am Ende des Shortcodes -->
{{ if .HasShortcode "mermaid" }}
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
  <script>mermaid.initialize({startOnLoad: true});</script>
{{ end }}

2. Saubere Defaults

{{ $size := .Get "size" | default "medium" }}
{{ $theme := .Get "theme" | default site.Params.defaultTheme | default "light" }}

3. Fehlerbehandlung

{{ $src := .Get "src" }}
{{ if not $src }}
  {{ errorf "figure shortcode requires 'src' parameter: %s" .Position }}
{{ end }}

4. Semantisches HTML

<!-- GUT -->
<figure>
  <img src="{{ .Get "src" }}" alt="{{ .Get "alt" }}">
  <figcaption>{{ .Get "caption" }}</figcaption>
</figure>

<!-- SCHLECHT -->
<div class="image-container">
  <img src="{{ .Get "src" }}">
  <div class="caption">{{ .Get "caption" }}</div>
</div>

5. Barrierefreiheit

<div class="alert" role="alert" aria-live="polite">
  {{ .Inner | markdownify }}
</div>

Debugging

<!-- Alle verfügbaren Daten anzeigen -->
<pre>{{ . | jsonify (dict "indent" "  ") }}</pre>

<!-- Position im Quellcode -->
{{ printf "Shortcode at %s" .Position }}

<!-- Debug-Ausgabe in Konsole -->
{{ warnf "Debug: %v" .Params }}

Shortcode-Bibliothek

Eine Sammlung nützlicher Shortcodes für den Alltag:

layouts/shortcodes/
├── alert.html          # Warnungen, Infos, Tipps
├── button.html         # CTA-Buttons
├── codepen.html        # CodePen Embeds
├── details.html        # Aufklappbare Details
├── figure.html         # Bilder mit Caption
├── gallery.html        # Bildergalerie
├── highlight.html      # Code mit Zeilennummern
├── mermaid.html        # Diagramme
├── quote.html          # Blockquote mit Quelle
├── tabs.html           # Tab-Navigation
├── toc.html            # Inhaltsverzeichnis
├── video.html          # Video-Player
└── youtube.html        # YouTube Embed

Fazit

Hugo Shortcodes sind mächtig und flexibel:

  • Wiederverwendbar: Einmal schreiben, überall nutzen
  • Markdown-freundlich: Kein HTML im Content nötig
  • Performant: Zur Build-Zeit gerendert
  • SEO-freundlich: Statisches HTML, kein JavaScript nötig
  • Typsicher: Mit Fehlerbehandlung und Defaults

Für diesen Blog nutze ich Shortcodes für Info-Boxen, Code-Demos und eingebettete Medien. Sie halten die Markdown-Dateien sauber und die Komponenten konsistent.