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.