Zum Inhalt springen
CASOON

Typst für professionelle Dokumente: Templates, Layouts und Automatisierung

Wiederverwendbare Vorlagen, dynamische Inhalte und strukturierte Dokumente

14 Minuten
Typst für professionelle Dokumente: Templates, Layouts und Automatisierung
#Typst #Templates #PDF #Automatisierung
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:

projekt/ ├── main.typ # Hauptdatei ├── template.typ # Template-Definition ├── kapitel/ │ ├── einleitung.typ │ ├── methodik.typ │ ├── ergebnisse.typ │ └── fazit.typ ├── bilder/ │ └── ... └── daten/ └── statistik.json

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:

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.