Wiederverwendbare Vorlagen, dynamische Inhalte und strukturierte Dokumente
SerieTypst
Teil 3 von 6
Die Grundlagen sitzen, erste Dokumente sind erstellt. Jetzt wird es ernst: Wie baut man wiederverwendbare Templates? Wie richtet man Kopf- und Fußzeilen ein? Und wie integriert man externe Daten für automatisch generierte Berichte?
Dieser Artikel zeigt, wie Typst auch anspruchsvolle Anforderungen erfüllt – ohne die Komplexität, die man von LaTeX kennt.
Templates: Einmal bauen, überall nutzen
Ein Template in Typst ist eine Funktion, die Styling-Regeln setzt und den Inhalt umschließt. Das Konzept ist einfach, aber mächtig.
Grundstruktur eines Templates
// template.typ
#let report(
title: "Bericht",
author: "Unbekannt",
date: datetime.today(),
body
) = {
// Dokument-Metadaten
set document(title: title, author: author)
// Seitenformat
set page(
paper: "a4",
margin: (top: 3cm, bottom: 2.5cm, left: 2.5cm, right: 2cm),
)
// Grundschrift
set text(font: "Linux Libertine", size: 11pt, lang: "de")
// Überschriften-Styling
set heading(numbering: "1.1")
// Titelseite
align(center)[
#v(4cm)
#text(size: 24pt, weight: "bold")[#title]
#v(1cm)
#text(size: 14pt)[#author]
#v(0.5cm)
#text(size: 12pt, fill: gray)[#date.display("[day].[month].[year]")]
#v(2cm)
]
pagebreak()
// Inhalt
body
}
Die Nutzung ist dann denkbar einfach:
#import "template.typ": report
#show: report.with(
title: "Quartalsbericht Q4 2025",
author: "Max Mustermann",
)
= Zusammenfassung
Der Umsatz ist gestiegen...
= Details
== Produktbereich A
Die Zahlen im Detail...
Das #show: template.with(...) wendet das Template auf das gesamte Dokument an. Alle Styling-Regeln greifen automatisch.
Template für Rechnungen
Ein praxisnahes Beispiel – eine Rechnungsvorlage:
// rechnung.typ
#let rechnung(
rechnungsnummer: "",
datum: datetime.today(),
kunde: (:),
positionen: (),
steuersatz: 19,
body
) = {
set page(paper: "a4", margin: 2cm)
set text(font: "Inter", size: 10pt, lang: "de")
// Briefkopf
grid(
columns: (1fr, 1fr),
align(left)[
*Meine Firma GmbH* \
Musterstraße 123 \
12345 Berlin \
info\@meinefirma.de
],
align(right)[
#text(size: 20pt, weight: "bold")[RECHNUNG] \
#v(0.5cm)
Nr: #rechnungsnummer \
Datum: #datum.display("[day].[month].[year]")
]
)
v(2cm)
// Kundenadresse
[
*#kunde.name* \
#kunde.strasse \
#kunde.plz #kunde.ort
]
v(1.5cm)
// Positionstabelle
let netto-summe = positionen.map(p => p.menge * p.preis).sum()
let steuer = netto-summe * steuersatz / 100
let brutto = netto-summe + steuer
table(
columns: (auto, 1fr, auto, auto, auto),
align: (center, left, right, right, right),
stroke: none,
inset: 8pt,
table.header(
[*Pos*], [*Beschreibung*], [*Menge*], [*Einzelpreis*], [*Gesamt*],
),
table.hline(),
..positionen.enumerate().map(((i, p)) => (
[#(i + 1)],
[#p.beschreibung],
[#p.menge],
[#p.preis €],
[#(p.menge * p.preis) €],
)).flatten(),
table.hline(),
[], [], [], [Netto:], [#netto-summe €],
[], [], [], [MwSt #steuersatz%:], [#calc.round(steuer, digits: 2) €],
[], [], [], [*Brutto:*], [*#calc.round(brutto, digits: 2) €*],
)
v(1cm)
body
}
Anwendung:
#import "rechnung.typ": rechnung
#show: rechnung.with(
rechnungsnummer: "2026-001",
kunde: (
name: "Beispiel AG",
strasse: "Testweg 42",
plz: "10115",
ort: "Berlin",
),
positionen: (
(beschreibung: "Webentwicklung", menge: 40, preis: 95),
(beschreibung: "Consulting", menge: 8, preis: 120),
(beschreibung: "Hosting (Monat)", menge: 1, preis: 49),
),
)
Zahlbar innerhalb von 14 Tagen auf das unten genannte Konto.
Vielen Dank für Ihren Auftrag!
Die Positionen werden automatisch nummeriert, Summen berechnet, Mehrwertsteuer addiert. Das Template macht die Arbeit.
Layout-Regeln und Styling
Kopf- und Fußzeilen
Typst macht Header und Footer über die page-Funktion konfigurierbar:
#set page(
header: context {
if counter(page).get().first() > 1 [
_Quartalsbericht Q4 2025_
#h(1fr)
Seite #counter(page).display()
]
},
footer: [
#line(length: 100%, stroke: 0.5pt + gray)
#v(0.3cm)
#text(size: 8pt, fill: gray)[
Meine Firma GmbH | Vertraulich
#h(1fr)
Stand: #datetime.today().display("[day].[month].[year]")
]
],
)
Das context-Keyword ermöglicht den Zugriff auf den aktuellen Seitenzähler. So kann man die erste Seite (Titelseite) anders behandeln.
Automatische Nummerierung
Überschriften nummerieren sich automatisch:
#set heading(numbering: "1.1")
= Einleitung // wird zu "1 Einleitung"
== Motivation // wird zu "1.1 Motivation"
== Ziele // wird zu "1.2 Ziele"
= Hauptteil // wird zu "2 Hauptteil"
Andere Formate sind möglich:
#set heading(numbering: "I.A.1") // Römisch, Buchstaben, Arabisch
#set heading(numbering: "(1)") // Mit Klammern
Absatz- und Textformatierung
// Absatzabstand
#set par(
justify: true,
leading: 0.8em, // Zeilenabstand
first-line-indent: 1em, // Einzug erste Zeile
)
// Zeilenabstand für das ganze Dokument
#set text(
size: 11pt,
font: "Linux Libertine",
hyphenate: true, // Silbentrennung
)
// Überschriften ohne Einzug danach
#show heading: it => {
it
par(first-line-indent: 0pt)[]
}
Eigene Stile für Elemente
Mit #show-Regeln lassen sich beliebige Elemente umstylen:
// Alle Links blau und unterstrichen
#show link: it => underline(text(fill: blue, it))
// Code-Blöcke mit Hintergrund
#show raw.where(block: true): it => block(
fill: luma(245),
inset: 10pt,
radius: 4pt,
width: 100%,
it
)
// Zitate eingerückt und kursiv
#show quote: it => pad(left: 2em, right: 2em, emph(it))
Strukturelemente für lange Dokumente
Inhaltsverzeichnis
#outline(
title: "Inhaltsverzeichnis",
indent: auto,
depth: 3, // Bis zu welcher Ebene
)
Das Verzeichnis generiert sich automatisch aus allen Überschriften.
Abbildungsverzeichnis
// Abbildung mit Label
#figure(
image("diagramm.png", width: 80%),
caption: [Architektur des Systems],
) <fig:architektur>
// Verzeichnis aller Abbildungen
#outline(
title: "Abbildungsverzeichnis",
target: figure.where(kind: image),
)
Tabellenverzeichnis
#figure(
table(
columns: 3,
[A], [B], [C],
[1], [2], [3],
),
caption: [Beispieldaten],
) <tab:beispiel>
#outline(
title: "Tabellenverzeichnis",
target: figure.where(kind: table),
)
Querverweise
Wie in @fig:architektur dargestellt...
Die Daten aus @tab:beispiel zeigen...
Siehe Kapitel @sec:methodik für Details.
= Methodik <sec:methodik>
Typst löst die Referenzen automatisch auf und zeigt „Abbildung 1”, „Tabelle 2” oder „Kapitel 3.1”.
Fußnoten
Dies ist ein wichtiger Punkt#footnote[Quelle: Müller 2024, S. 42].
Weitere Details finden sich an anderer Stelle#footnote[
Für eine ausführliche Diskussion siehe den Anhang.
].
Fußnoten werden automatisch nummeriert und am Seitenende platziert.
Mathematik und Formeln
Typst hat einen leistungsfähigen Mathematik-Modus, der sich von LaTeX syntaktisch unterscheidet, aber ähnlich mächtig ist.
Grundlagen
// Inline
Die Formel $a^2 + b^2 = c^2$ ist bekannt.
// Display (zentriert)
$ integral_0^1 x^2 dif x = 1/3 $
Komplexere Ausdrücke
// Matrizen
$ mat(
1, 2, 3;
4, 5, 6;
7, 8, 9;
) $
// Gleichungssysteme
$ cases(
x + y = 10,
x - y = 2,
) $
// Grenzwerte
$ lim_(n -> infinity) (1 + 1/n)^n = e $
// Summen und Produkte
$ sum_(i=1)^n i = (n(n+1))/2 $
$ product_(k=1)^n k = n! $
Nummerierte Gleichungen
$ E = m c^2 $ <eq:einstein>
Wie in @eq:einstein gezeigt...
Eigene Symbole und Makros
// Kurzschreibweisen definieren
#let R = $bb(R)$ // Reelle Zahlen
#let N = $bb(N)$ // Natürliche Zahlen
#let diff = $partial$ // Partielle Ableitung
Für alle $x in #R$ gilt...
$ (diff f)/(diff x) = 2x $
Automatisierung: Dynamische Dokumente
Hier zeigt Typst seine Stärke als Programmiersprache. Inhalte können berechnet, Daten eingelesen und Dokumente dynamisch generiert werden.
Schleifen und Bedingungen
#let mitarbeiter = (
(name: "Anna", abteilung: "Entwicklung"),
(name: "Max", abteilung: "Marketing"),
(name: "Lisa", abteilung: "Vertrieb"),
)
== Teamübersicht
#for person in mitarbeiter [
- *#person.name* arbeitet in der Abteilung #person.abteilung
]
Daten aus JSON laden
#let daten = json("quartalszahlen.json")
= Quartalsbericht
#for quartal in daten.quartale [
== #quartal.name
Umsatz: #quartal.umsatz € \
Gewinn: #quartal.gewinn € \
#if quartal.gewinn > 0 [
#text(fill: green)[Positives Ergebnis]
] else [
#text(fill: red)[Negatives Ergebnis]
]
]
Die JSON-Datei quartalszahlen.json:
{
"quartale": [
{ "name": "Q1 2025", "umsatz": 150000, "gewinn": 12000 },
{ "name": "Q2 2025", "umsatz": 180000, "gewinn": 22000 },
{ "name": "Q3 2025", "umsatz": 165000, "gewinn": -5000 },
{ "name": "Q4 2025", "umsatz": 210000, "gewinn": 35000 }
]
}
CSV-Daten einlesen
#let csv-data = csv("teilnehmer.csv")
#table(
columns: csv-data.first().len(),
..csv-data.flatten()
)
Berechnungen im Dokument
#let preise = (120, 85, 200, 45, 180)
Die Summe beträgt *#preise.sum() €*.
Der Durchschnitt liegt bei *#calc.round(preise.sum() / preise.len(), digits: 2) €*.
#let sortiert = preise.sorted()
Vom günstigsten (#sortiert.first() €) zum teuersten (#sortiert.last() €).
Dynamische Tabellen aus Daten
#let produkte = (
(name: "Widget A", preis: 29.99, lager: 150),
(name: "Widget B", preis: 49.99, lager: 80),
(name: "Widget C", preis: 19.99, lager: 200),
)
#table(
columns: 4,
align: (left, right, right, center),
[*Produkt*], [*Preis*], [*Lager*], [*Status*],
..produkte.map(p => (
p.name,
[#p.preis €],
str(p.lager),
if p.lager < 100 [
#text(fill: orange)[Nachbestellen]
] else [
#text(fill: green)[OK]
],
)).flatten()
)
Projekt-Struktur für größere Dokumente
Bei umfangreichen Dokumenten empfiehlt sich eine Aufteilung in mehrere Dateien:
Die Hauptdatei bindet alles zusammen:
// main.typ
#import "template.typ": report
#show: report.with(
title: "Forschungsbericht 2025",
author: "Dr. Maria Schmidt",
)
#include "kapitel/einleitung.typ"
#include "kapitel/methodik.typ"
#include "kapitel/ergebnisse.typ"
#include "kapitel/fazit.typ"
Mit #include wird der Inhalt eingefügt, mit #import werden Funktionen und Variablen verfügbar gemacht.
Typst skaliert von einfachen Notizen bis zu komplexen, automatisch generierten Berichten. Die Kombination aus lesbarer Markup-Syntax und eingebauter Programmiersprache macht es besonders stark für:
- Wiederverwendbare Templates – einmal bauen, überall einsetzen
- Strukturierte Dokumente – Verzeichnisse, Referenzen, Nummerierung
- Datengetriebene Berichte – JSON/CSV einlesen, dynamisch generieren
- Konsistentes Corporate Design – zentrale Style-Definitionen
Im nächsten und letzten Teil der Serie geht es um Migration und Integration: Wie konvertiert man bestehende LaTeX-Dokumente? Wie bindet man Typst in CI/CD-Pipelines ein? Und welche Hybrid-Strategien funktionieren in der Praxis?
Ressourcen:
- Typst Template-Dokumentation
- Typst Universe – Community Templates
- Scripting-Referenz
- Math-Syntax-Referenz
Konkrete Templates-Use-Cases
- Rechnungen-Generation: Automatisiert aus Buchhaltungs-Daten.
- Wissenschaftliche Berichte: Templates für Hochschulen.
- Konzern-Reports: Konsistentes Branding.
- CV-Templates: Lebensläufe in vielen Varianten.
Realistischer Setup-Aufwand
- Einfaches Template: 2–4 Stunden.
- Komplexes Brand-Template: 1–2 Tage.
- Production-ready mit CI-Integration: 1 Woche.
Wann Typst-Automatisierung Sinn macht
- Bei wiederkehrenden Dokumenten: Rechnungen, Reports, Zertifikate.
- Bei Daten-getriebener Generation: Bericht aus CSV, JSON, Datenbank.
- Bei Versionierung: Git-friendly, Diff-fähig.
Wann nicht
- Bei einmaligen Dokumenten: Word/Pages reicht.
- Bei stark visuell-getriebenen Layouts: InDesign besser.
- Bei Co-Editing mit Nicht-Tech-Personen: Word einfacher.