csv-table: Hugo-Shortcode für interaktive CSV-Tabellen
Leg eine CSV-Datei ins Projekt, füg eine Zeile in deinen Post ein und bekomm eine sortierbare, responsive und barrierefreie HTML-Tabelle ohne eine einzige Zeile Markup.

Wenn du jemals versucht hast, tabellarische Daten auf einer Hugo-Seite darzustellen, weißt du, wie schnell alles schiefgeht. Sagen wir, du hast eine ganz normale CSV-Datei mit App-Rankings, Preisvergleichen oder Benchmark-Ergebnissen. Um daraus eine hübsche Tabelle auf der Seite zu machen, musst du entweder mit Markdown-Markup kämpfen oder HTML von Hand schreiben. Beide Varianten sind mäßig, und beide skalieren schlecht.
Ich bin oft genug darüber gestolpert, um mir schließlich ein vernünftiges Werkzeug zu bauen, einen Shortcode namens csv-table. Du gibst ihm eine CSV-Datei, und er erzeugt eine gestylte, sortierbare, responsive HTML-Tabelle. Eine Zeile im Content, null manuelles Markup.
Warum Markdown-Tabellen nicht funktionieren
Jeder, der mal versucht hat, eine Markdown-Tabelle größer als 3x3 zusammenzubauen, kennt den Schmerz. Brian Wisti hat es in seinem Post über CSV und Datentabellen in Hugo treffend formuliert: Lesen ist einfach, aber Pflegen ohne Editor-Plugins ist eine Qual. Und er hat recht. Die Probleme fangen sofort an:
Die Formatierung von Markdown-Tabellen ist fragil. Ein fehlendes Pipe-Zeichen oder eine versehentlich hinzugefügte Spalte in einer Zeile, und statt einer Tabelle bekommst du Zeichensalat. Keine Fehlermeldung, einfach kaputtes Layout.
Sie skalieren nicht. Eine 5x5-Tabelle funktioniert problemlos. Dreißig Zeilen aus Excel schon nicht mehr. Jedes Mal, wenn sich die Daten ändern, musst du all diese senkrechten Striche wieder von Hand ausrichten.
Null Interaktivität. Dreißig Zeilen Daten, und der Leser kann nicht nach Preis, Bewertung oder Name sortieren. Markdown kann das einfach nicht.
Und schließlich sind die Daten fest im Artikeltext verdrahtet. Sie existieren nicht separat. Du kannst dieselbe Tabelle nicht in einem anderen Post wiederverwenden, die Daten nicht unabhängig vom Text aktualisieren und sie nicht per Skript generieren und einfach einbinden.
Was es schon gibt und warum es nicht reicht
Hugo hat eingebaute Werkzeuge für die Arbeit mit Daten. Die Funktion transform.Unmarshal kann CSV in Arrays parsen, und mehrere Leute in der Community haben gezeigt, wie man das nutzen kann.
Der Shortcode csv-to-table von Joe Mooring ist eine solide Lösung. Er arbeitet mit Seitenressourcen, Sektionsressourcen und globalen Ressourcen, behandelt Fehler korrekt, unterstützt Überschriften, benutzerdefinierte Trennzeichen und optionale Kopfzeilen. Für eine einfache CSV-zu-HTML-Konvertierung ist das ein hervorragender Ausgangspunkt.
Brian Wisti (Link oben) ging einen anderen Weg: Er bettete CSV-Daten direkt in die Shortcode-Tags ein, statt auf eine externe Datei zu verweisen. Außerdem experimentierte er mit JSON-basierten Datentabellen und einem zeilenorientierten list-table-Format. Der Post ist interessant und zeigt gut, wie weit man mit Hugo-Shortcodes kommen kann, wenn man etwas Einfallsreichtum mitbringt.
Beide Ansätze haben mir Ideen gegeben, aber keiner löste mein Problem vollständig. Ich brauchte Sortierung, Responsive-Verhalten auf Mobilgeräten, Ausrichtung pro Spalte, die Möglichkeit, Spalten auszublenden und Zeilen zu begrenzen, plus korrektes Accessibility-Markup. Und das nicht als einzelne Teile, sondern als ein einziges Werkzeug, das ich überall einsetzen kann.
Wie csv-table funktioniert
Deine Daten liegen in einer CSV-Datei im assets-Verzeichnis von Hugo. Im Artikeltext sieht der Aufruf so aus:
{{< csv-table file="app-rankings.csv" >}}
Das war’s. Hugo parst die CSV-Datei beim Build über transform.Unmarshal (ohne eigenen Parser) und generiert eine semantische HTML-Tabelle mit korrekten <thead>, <tbody>, scope-Attributen, ARIA-Rollen und aria-sort-Indikatoren.
Auf der Client-Seite kümmert sich tablesort.js um die Interaktivität: Ein Klick auf den Spaltenheader sortiert aufsteigend, ein weiterer Klick absteigend. Die Sortierung funktioniert korrekt mit formatierten Zahlen, Prozenten und Währungswerten.
Einfach im Standard, flexibel bei Bedarf
Das Prinzip ist simpel: Convention over Configuration. Die Standardeinstellungen decken 80 % der Fälle ab. Die restlichen 20 % werden über Parameter gelöst, nicht über einen Fork der Vorlage.
So sieht ein ausführlicherer Aufruf aus:
{{< csv-table
file="sales.csv"
caption="Q1 Sales by Region"
sortBy="Revenue"
order="desc"
limit=10
col_right="Revenue,Units"
col_hide="Internal_ID"
compact=true
>}}
Hier wird die Datei sales.csv genommen, nur 10 Zeilen mit dem höchsten Revenue angezeigt, numerische Spalten rechtsbündig ausgerichtet, die Spalte Internal ID ausgeblendet und der Kompaktmodus mit engerem Layout aktiviert. Die Tabellenüberschrift erscheint oben.
Der vollständige Parametersatz deckt alles ab, was ich in der Praxis gebraucht habe:
caption- TabellenüberschriftsortByundorder- anfängliche Sortierung (aufsteigend als Standard)limit- maximale Anzahl angezeigter Zeilencol_hide- Spalten ausblenden, ohne die Quelldatei zu bearbeitencol_nowrap- Zeilenumbruch in einer Spalte verhinderncol_left,col_center,col_right- Ausrichtung pro Spaltecol_width- explizite CSS-Breiten über<colgroup>header(Wert “hide”) - Kopfzeile ausblendenfont_monoundfont_size- Schriftsteuerungcompact- kompaktes Layoutresponsive- Responsive-Modus: horizontales Scrollen oder Karten auf Mobilgerätenclass- zusätzliche CSS-Klassen
Ein Detail, das ich besonders hervorheben möchte: Alle Spaltenparameter arbeiten mit Namen, nicht mit Indizes. Du schreibst col_hide="Email,Phone", nicht col_hide="3,5". Intern baut der Shortcode eine Header-zu-Index-Map, sodass die Suche schnell ist, während der Aufruf lesbar bleibt. Und wenn du Spalten in deiner CSV hinzufügst oder umstellst, geht der Shortcode nicht kaputt.
Tabellen auf kleinen Bildschirmen
Tabellen und Mobilgeräte sind eine schmerzhafte Kombination. Der Shortcode bietet zwei Modi.
Standardmäßig ist scroll aktiviert: Die Tabelle wird in einen horizontal scrollbaren Container gepackt. Die Tabellenstruktur bleibt erhalten, was wichtig ist, wenn Spalten nebeneinander verglichen werden müssen (Vergleichstabellen, Spezifikationen).
Die Alternative ist stack: Jede Zeile wird auf schmalen Bildschirmen zu einer Karte. Vor jeder Zelle erscheint der Spaltenname über data-label-Attribute und CSS-Pseudoelemente ::before. Das ist reines CSS, kein JavaScript. Funktioniert gut für Daten, bei denen jede Zeile ein eigenständiger Datensatz ist (eine App-Liste, ein Produktkatalog), und der Leser davon profitiert, alle Felder eines Elements auf einmal zu sehen.
Was unter der Haube passiert
Ein paar technische Details für Neugierige.
CSS und JavaScript des Shortcodes werden einmal pro Seite geladen, selbst wenn fünf Tabellen darauf sind. Hugos Scratch-Mechanismus verfolgt, ob die Ressourcen bereits eingebunden wurden, und verhindert doppelte <link>- und <script>-Tags.
Sowohl CSS als auch JS durchlaufen Hugos Asset-Pipeline: In Production sind sie minifiziert, und die Dateinamen enthalten einen Hash für Cache-Busting. Die tablesort-Bibliothek kann über ein CDN geladen (in den Theme-Einstellungen konfigurierbar) oder lokal eingebunden werden, wenn die Seite auf externe Requests verzichtet.
Das CSS basiert auf Custom Properties und unterstützt den Dark Mode über den Selektor [data-theme="dark"]. Wenn dein Theme dieses Attribut bereits umschaltet, übernehmen die Tabellen das Theme automatisch.
Was Barrierefreiheit betrifft: Das Markup enthält role-Attribute, scope auf Kopfzellen und aria-sort-Indikatoren, die sich beim Sortieren dynamisch aktualisieren. Screenreader können durch die Tabellenstruktur navigieren und verstehen die aktuelle Sortierreihenfolge.
Wenn etwas schiefgeht (Datei nicht gefunden, falscher Pfad, kaputte CSV-Datei), zeigt der Shortcode eine klare Fehlermeldung genau an der Stelle der Tabelle. Keine stillen Fehler, kein kaputtes Layout.
Wo es wirklich Zeit spart
Der Shortcode läuft schon eine Weile in meinem Blog. Hier sind die Szenarien, in denen er am meisten bringt.
Wenn sich Daten häufig ändern (monatliche Rankings, Quartalszahlen), aktualisiere ich einfach die CSV-Datei und baue die Seite neu. Den Post selbst fasse ich nicht an.
Zum Beispiel nutzt mein Post Top Raycast Extensions csv-table, um ein vollständiges Ranking von Erweiterungen mit Sortierung, Zeilenlimit und ausgeblendeten Spalten anzuzeigen. Alle Daten sind eine einzige CSV-Datei, die ich regelmäßig neu generiere. Post aktualisieren = Datei ersetzen + neu bauen.
Wenn Daten aus einem Skript oder einem Tabellenexport kommen, geht die CSV-Datei direkt ins Projekt, ohne jegliche Umformatierung.
Wenn ich ein “Top 10” aus einem größeren Datensatz zeigen will, erledigt der Parameter limit das, ohne die Quelldatei zu beschneiden.
Wenn dieselben Daten in mehreren Posts mit unterschiedlicher Darstellung gebraucht werden (verschiedene sichtbare Spalten, verschiedene Sortierung), referenziere ich eine CSV mit verschiedenen Parametern. Eine Datenquelle, mehrere Ansichten.
Das will ich auch!
Der Code ist noch nicht öffentlich verfügbar. Ich habe ihn für mein eigenes Hugo-Theme (“domino”) gebaut und bin noch nicht dazu gekommen, ihn in ein separates, ordentlich dokumentiertes Modul zu verpacken. Bei genügend Interesse veröffentliche ich ihn als wiederverwendbares Hugo-Modul. Schreib mir, wenn dir das nützlich wäre.
Alle Bausteine sind gut dokumentiert: transform.Unmarshal zum Parsen von CSV, ein map-basiertes Parametersystem für Spaltenoperationen, tablesort.js für die clientseitige Sortierung und CSS Custom Properties für das Theming.
Die Komplexität liegt nicht in einem einzelnen dieser Teile, sondern darin, sie zusammenspielen zu lassen: ARIA-Attribute, Responsive-Modi, Asset-Deduplizierung, Fehlerbehandlung und sinnvolle Standardeinstellungen, damit die einfachen Fälle einfach bleiben.
Eine Mail, sobald ein neuer Beitrag erscheint.



