Skip to content

Instantly share code, notes, and snippets.

@kineticz
Last active June 17, 2026 09:46
Show Gist options
  • Select an option

  • Save kineticz/cc33d13db5eb98bac38eb597ea6afa42 to your computer and use it in GitHub Desktop.

Select an option

Save kineticz/cc33d13db5eb98bac38eb597ea6afa42 to your computer and use it in GitHub Desktop.
Puterize Typst Style — elegant en onderscheidend Typst template (blauw-cyaan-oranje, DM Sans + Red Hat Mono)

Puterize Typst Style

Puterize huisstijl-documenttemplate voor Typst — een moderne tech-stijl met blauw→cyaan gradient, oranje accenten, DM Sans en Red Hat Mono.

Vereisten

  • Typst ≥ 0.15.0
  • Fonts (staan al op je systeem, of installeer ze):
    • DM Sans — body en display
    • Red Hat Mono — code, labels, meta

Fonts installeren (indien niet aanwezig)

# DM Sans (Google Fonts, OFL)
sudo apt install fonts-dm-sans 2>/dev/null || \
  sudo cp fonts/*.ttf /usr/local/share/fonts/ && sudo fc-cache -fv

# Red Hat Mono (Red Hat, OFL)
sudo apt install redhat-mono-fonts 2>/dev/null

Snel starten

# Compileren
./build.sh example.typ

# Of met watch-modus (hercompileert bij wijzigingen)
./build.sh example.typ --watch

Gebruik als Typst-package

Optie 1: Lokaal package (aanbevolen)

#import "@local/puterize-typst:0.1.0": puterize-doc

Optie 2: Relatief pad

#import "lib.typ": puterize-doc

Volledig voorbeeld

#import "lib.typ": puterize-doc

#show: puterize-doc.with(
  title-pre:    "Schaalbare",
  title-accent: "infrastructuur",
  eyebrow:      "Technische architectuur · Puterize",
  subtitle:     "Een blauwdruk voor cloud-native platformen.",
  doc-id:       "PUTERIZE · ARCHITECTUURNOTITIE",
  doc-name:     "Architectuurnotitie",
  version:      "1.0",
  author:       "Kaiyin Zhong",
  distribution: "Engineering team",
)

= Eerste hoofdstuk
...

Parameters

Parameter Standaard Beschrijving
title-pre "Project" Eerste deel titel op cover
title-accent "document" Laatste woord — blauw + oranje punt
eyebrow "Technisch document · Puterize" Mono-label boven cover-titel
subtitle "" Beschrijving op cover
doc-id "PUTERIZE · DOC" Header links
doc-name "Document" Cover meta-blok
version "1.0" Cover meta-blok
author "Kaiyin Zhong" Cover meta-blok
distribution "Intern team" Cover meta-blok
brand-tag "Puterize · Infrastructuur · 2026" Header rechts
footer-tag "Puterize · Intern document" Footer links
date vandaag Datum in DD-MM-YYYY

Markup-conventies

Typst Resultaat
= Hoofdstuk Hoofdstuk — nieuwe pagina, "HOOFDSTUK XX" label, blauw-cyaan gradient underline
== Sectie Sectie — DM Sans SemiBold, oranje blokje marker
=== Subsectie Subsectie — DM Sans Medium, grijze tint
- item Bullet-list — oranje markers
+ item Genummerde lijst — blauwe nummers
#quote(block: true) Callout — blauwe rand, "OPMERKING" in oranje
`code` Inline code — lichte blauwe chip
``` block Codeblok — donkere achtergrond, blauwe linkerrand
*strong* Vet — weight 600, ink
_emph_ Italic — grijze tint
#link("...") Link — blauw, cyaan dotted underline

Kleurenpalet

Variabele Hex Gebruik
puterize-blue #0E80EB Primair — koppen, links, accenten
puterize-cyan #00CAF0 Highlight — gradient-eindpunt
puterize-orange #F0A040 Warm accent — bullets, callout labels
puterize-ink #0F172A Hoofdtekst
puterize-ink-soft #334155 Secundaire tekst
puterize-muted #64748B Labels, meta
puterize-muted-soft #94A3B8 Paginanummers
puterize-rule #E2E8F0 Lijnen, borders
puterize-blue-soft #EFF6FF Subtiele blauwe wash (cover, callout-bg, inline code)
puterize-code-bg #0F172A Code block achtergrond
puterize-code-fg #E2E8F0 Code block tekst

Bestandsoverzicht

puterize-typst-style/
├── favicon.svg          # Logo — bron van het kleurenpalet
├── lib.typ              # Hoofdtemplate (puterize-doc)
├── example.typ          # Voorbeelddocument
├── build.sh             # Compileer-helper
├── typst.toml           # Package-configuratie
├── README.md            # Deze documentatie
└── .gitignore

Licentie

MIT — zie typst.toml

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
// =============================================================================
// Puterize Typst Style — Example Document
// =============================================================================
// Compile with: ./build.sh example.typ
// =============================================================================
#import "lib.typ": puterize-doc, puterize-blue, puterize-orange
#show: puterize-doc.with(
title-pre: "Schaalbare",
title-accent: "infrastructuur",
eyebrow: "Technische architectuur · Puterize",
subtitle: "Een blauwdruk voor het ontwerpen en beheren van cloud-native platformen met een focus op veerkracht, observability en reproduceerbaarheid.",
doc-id: "PUTERIZE · ARCHITECTUURNOTITIE",
doc-name: "Architectuurnotitie",
version: "1.0",
author: "Kaiyin Zhong",
distribution: "Engineering team",
brand-tag: "Puterize · Infrastructuur · 2026",
footer-tag: "Puterize · Intern document",
)
= Waarom deze blauwdruk en wat je ervan mag verwachten
Dit document beschrijft de technische fundering van ons platform. We zijn gegroeid van een enkele #raw("VPS") naar een gedistribueerd systeem, en dat vraagt om een doordachte aanpak.
== De aanleiding — waarom nu?
We hebben de afgelopen maanden drie incidenten gehad die rechtstreeks te herleiden zijn naar het ontbreken van een gedeelde architectuurvisie:
- Een #strong[productie-onderbreking] door een configuratiedrift tussen staging en productie
- Onverklaarbaar resource-gebruik doordat #emph[niemand] wist welke services waar draaiden
- Een deploy die twee uur duurde omdat de volgorde van componenten niet gedocumenteerd was
Dit document maakt de impliciete kennis expliciet.
== Doelstellingen en niet-doelen
#text(fill: puterize-blue, weight: 600)[Doelstellingen:]
+ Een eenduidig, grafisch overzicht van alle systeemcomponenten
+ Draaiboeken voor de drie meest voorkomende incident-scenario's
+ Een reproduceerbare opbouw vanaf nul — #raw("git clone && make up")
+ Kostenschatting per omgeving (dev, staging, prod)
#text(fill: puterize-blue, weight: 600)[Geen doelen:]
- Dit is geen vervanging voor runbooks in de monitoring-stack
- Geen security-audit of penetratietest
- Geen prestatie-benchmark
== Terminologie en conventies
Een paar termen die we consistent gebruiken door het hele document:
#table(
columns: (auto, 1fr),
[Term], [Betekenis],
[Control plane], [De set services die het platform bestuurt: CI/CD, orchestrator, secret manager.],
[Data plane], [Alles wat verkeer verwerkt: load balancers, API-gateways, workload-containers.],
[Cell], [Een onafhankelijk deploy-baar cluster; typisch één per regio of tenant.],
[Pulse], [Onze interne heartbeat-check die elke service elke 10s bevraagt.],
)
= Voorgestelde architectuur
We stellen een #strong[cell-based architecture] voor, waarbij elke cell een zelfstandig opererend cluster is met eigen control plane, data plane, en persistentie-laag.
#quote(block: true)[
De keuze voor cell-gebaseerde architectuur is niet gratuit. Tijdens het evalueren van alternatieven — monolithisch horizontaal schalen, microservices zonder harde grenzen, en event-driven mesh — bleek de cell-aanpak de beste balans te bieden tussen isolatie, operabiliteit, en kostenefficiëntie. Elke cel deelt geen fate met andere cellen; een storing in cel-A raakt cel-B niet.
]
== Componentendiagram
Dit is het overzicht op hoog niveau. Details per component volgen in hoofdstuk 3.
#table(
columns: (auto, auto, 1fr),
[Component], [Schaal], [Verantwoordelijkheid],
[API Gateway], [2+ per cel], [Rate limiting, auth, request-routing op basis van tenant-header.],
[Workload Controller], [1 per cel], [Orkestreert containers, herstart bij health-check failure, schaalt op basis van CPU/memory metrics.],
[Secret Store], [1 per cel], [Bewaart encryptie-sleutels, DB-wachtwoorden, API-tokens. Auto-rotate elke 72 uur.],
[Message Bus], [3 nodes per cel], [NATS-cluster met JetStream voor at-least-once delivery.],
[Observability Stack], [1 per cel], [OpenTelemetry collector, Tempo, Loki, Mimir — alle data blijft binnen de cel.],
)
== Configuratie-voorbeeld
Elke cel wordt beschreven door een declaratief manifest in #raw("YAML"):
```yaml
cell:
name: eu-west-1
region: europe-west4
networking:
ingress_cidr: 10.32.0.0/16
mesh_cidr: 100.64.0.0/10
control_plane:
replicas: 3
resources:
cpu: 2
memory: 4Gi
data_plane:
min_replicas: 2
max_replicas: 20
scaling:
metric: cpu_utilization
target: 0.7
persistence:
pg_version: 16
backup:
schedule: "0 */6 * * *"
retention_days: 30
```
Dit manifest is de #strong[enige bron van waarheid]. De #raw("cell-controller") leest het uit Git en brengt de werkelijkheid ernaartoe via een reconcilation loop.
= Risico's, mitigatie en open vragen
Elk ontwerp kent onzekerheden. We benoemen ze liever expliciet.
== Top-risico's
#table(
columns: (auto, auto, 1fr, auto),
[#text(fill: puterize-orange, weight: 700)[Risico]], [Kans], [Beschrijving], [Mitigatie],
[Split-brain], [Laag], [Twee cellen claimen dezelfde tenant door netwerkpartitie.], [Raft-leader election met fencing tokens.],
[Secret-lek], [Middel], [Rotatie faalt silent, credentials verlopen ongemerkt.], [#raw("rotated_secret_valid_after") check in health endpoint.],
[Cost-spike], [Hoog], [Auto-scaling schiet door bij traffic piek.], [Max-replicas cap per cel + budget-alert op 80%.],
[Data-verlies], [Laag], [Backup corrupt door bit-rot in cold storage.], [Maandelijkse restore-drill met checksum-verificatie.],
)
== Wat we nog moeten uitzoeken
- #link("https://www.cncf.io/projects/kubernetes/")[Kubernetes] vs Nomad voor de workload controller — beide voldoen, de keuze is organisatorisch, niet technisch.
- Welke #strong[echte] RTO/RPO eisen stellen de product owners? Zonder helder antwoord kunnen we niet dimensioneren.
- Moet de message bus cel-overstijgend kunnen routeren of blijft verkeer strikt lokaal?
// =============================================================================
// Puterize Typst Template — lib.typ
// =============================================================================
// Blauw-cyaan gradient huisstijl met oranje accenten.
// Fonts: DM Sans (body/display) + Red Hat Mono (code).
// Kleuren trouw aan favicon.svg: #0E80EB (blauw), #00CAF0 (cyaan), #F0A040 (oranje).
// =============================================================================
// ── COLOURS (from favicon.svg) ────────────────────────────────────────────────
#let puterize-blue = rgb("#0E80EB")
#let puterize-cyan = rgb("#00CAF0")
#let puterize-orange = rgb("#F0A040")
#let puterize-gold = rgb("#F6D040")
#let puterize-ink = rgb("#0F172A")
#let puterize-ink-soft = rgb("#334155")
#let puterize-muted = rgb("#64748B")
#let puterize-muted-soft = rgb("#94A3B8")
#let puterize-rule = rgb("#E2E8F0")
#let puterize-rule-soft = rgb("#F1F5F9")
#let puterize-blue-soft = rgb("#EFF6FF")
#let puterize-blue-pale = rgb("#DBEAFE")
#let puterize-code-bg = rgb("#0F172A")
#let puterize-code-fg = rgb("#E2E8F0")
// ── FONTS ─────────────────────────────────────────────────────────────────────
#let puterize-display = "DM Sans 9pt"
#let puterize-body = "DM Sans 9pt"
#let puterize-mono = "Red Hat Mono"
// ── HELPERS ───────────────────────────────────────────────────────────────────
/// Convert content to a plain string (for accent-word splitting).
/// Handles special characters like apostrophes gracefully.
#let to-string(content) = {
let s = repr(content)
// Strip outer brackets if the content was a sequence
if s.starts-with("[") and s.ends-with("]") {
s = s.slice(1, s.len() - 1)
}
// Collapse whitespace
s.split(" ").filter(w => w != "").join(" ")
}
/// Pad a number to two digits: 1 → "01".
#let pad2(n) = {
let s = str(n)
if s.len() < 2 { "0" + s } else { s }
}
/// Title where the *last word* is coloured blue.
/// Ends with an orange dot — the signature Puterize punctuation.
/// Falls back gracefully when content contains special characters.
#let accent-title(title, size: 32pt) = {
let raw-str = to-string(title)
let words = raw-str.split(" ")
// Detect if repr() leaked Typst syntax — fall back to plain rendering
let is-clean = words.all(w =>
not w.contains("(") and
not w.contains(")") and
not w.contains("[") and
not w.contains("]") and
not w.contains("#") and
not w.contains("smartquote")
)
if not is-clean or words.len() <= 1 {
// Fallback: render title as-is without word-splitting
[
#set text(size: size, weight: 700, font: puterize-display)
#title
#text(fill: puterize-orange, size: size * 1.15)[.]
]
} else {
let last = words.pop()
let pre = words.join(" ")
[
#set text(size: size, weight: 700, font: puterize-display)
#if pre != "" [
#pre
#h(0.15em)
]
#text(fill: puterize-blue)[#last]#text(fill: puterize-orange, size: size * 1.15)[.]
]
}
}
/// A thin horizontal rule with blue→cyan linear gradient.
#let gradient-rule(width: 100%, height: 4pt) = {
block(
width: width,
height: height,
fill: gradient.linear(
(puterize-blue, 0%),
(puterize-cyan, 100%),
angle: 90deg,
),
radius: 0.5pt,
)
}
// ── TEMPLATE ──────────────────────────────────────────────────────────────────
#let puterize-doc(
// cover
title-pre: "Project",
title-accent: "document",
eyebrow: "Technisch document · Puterize",
subtitle: "",
// meta
doc-id: "PUTERIZE · DOC",
doc-name: "Document",
version: "1.0",
author: "Kaiyin Zhong",
distribution: "Intern team",
// header / footer
brand-tag: "Puterize · Infrastructuur · 2026",
footer-tag: "Puterize · Intern document",
date: none,
// content
toc-intro: none,
body,
) = {
// derive date once
let the-date = if date != none { date } else {
datetime.today().display("[day]-[month]-[year]")
}
let toc-text = if toc-intro != none { toc-intro } else { subtitle }
// ── DOCUMENT-WIDE SETTINGS ──────────────────────────────────────────────────
set document(title: title-pre + " " + title-accent)
set text(
size: 10pt,
font: puterize-body,
tracking: -0.005em,
lang: "nl",
)
set par(
leading: 0.78em,
spacing: 1.0em,
justify: false,
)
// ── INLINE MARKUP ───────────────────────────────────────────────────────────
show strong: it => {
text(weight: 600, fill: puterize-ink, it)
}
show emph: it => {
text(style: "italic", fill: puterize-ink-soft, it)
}
show link: it => {
set text(fill: puterize-blue, weight: 500)
underline(
stroke: (paint: puterize-cyan, thickness: 0.6pt, dash: "dotted"),
offset: 2pt,
it,
)
}
// Inline code — light blue chip
show raw.where(block: false): it => {
box(
fill: puterize-blue-soft,
stroke: (paint: puterize-blue-pale, thickness: 0.5pt),
radius: 3pt,
outset: (y: 1.5pt, x: 3.5pt),
inset: (y: 1pt, x: 0pt),
text(font: puterize-mono, size: 9pt, fill: puterize-ink, it),
)
}
// ── BLOCK CODE ──────────────────────────────────────────────────────────────
show raw.where(block: true): it => {
block(
fill: puterize-code-bg,
stroke: (left: (paint: puterize-blue, thickness: 3pt)),
radius: 6pt,
inset: (top: 14pt, bottom: 14pt, left: 16pt, right: 16pt),
outset: (y: 10pt),
)[
#text(
font: puterize-mono,
size: 8.5pt,
fill: puterize-code-fg,
it,
)
]
}
// ── LISTS ───────────────────────────────────────────────────────────────────
// All lists — orange markers, comfortable spacing
show list: it => {
set list(
marker: text(fill: puterize-orange, weight: 700, size: 1.1em)[•],
indent: 2pt,
body-indent: 8pt,
)
it
}
// ── TABLES ──────────────────────────────────────────────────────────────────
// Set table defaults globally (does not affect grid elements like cover grids)
set table(
stroke: (
top: (paint: puterize-rule, thickness: 0.5pt),
bottom: (paint: puterize-rule, thickness: 0.5pt),
y: (paint: puterize-rule, thickness: 0.3pt),
x: (paint: puterize-rule, thickness: 0.3pt),
),
inset: (x: 10pt, y: 7pt),
fill: (x, y) => if y == 0 { puterize-blue-soft },
rows: 1,
)
// ── CALLOUTS (blockquote override) — panel accent with gradient top-stripe ──
show quote.where(block: true): it => {
block(
fill: puterize-blue-soft,
stroke: (paint: puterize-blue-pale, thickness: 0.8pt),
radius: 8pt,
inset: (top: 16pt, bottom: 14pt, left: 16pt, right: 16pt),
outset: (y: 12pt),
)[
// Gradient top-stripe via a zero-height block the full width
#block(
width: 100%,
height: 3pt,
fill: gradient.linear(
(puterize-blue, 0%),
(rgb("#DBEAFE"), 100%),
angle: 0deg,
),
)
#v(2pt)
#text(font: puterize-mono, size: 7.5pt, fill: puterize-orange, weight: 600)[OPMERKING]
#v(6pt)
#text(size: 10pt, fill: puterize-ink-soft, it.body)
]
}
// ── HEADINGS ────────────────────────────────────────────────────────────────
// Enable heading numbering for TOC (our show rules override display)
set heading(numbering: "1")
// Chapter counter for H1 display (start at 0, increments before each chapter)
let ch-num = counter("ch-num")
// Don't call update(0) which triggers increment; we start at 0 naturally
// H1 — Chapter: page break → watermarked number → accent title → gradient rule
show heading.where(level: 1): it => {
pagebreak(weak: false)
ch-num.step()
let n = ch-num.get().first()
v(4pt)
align(left)[
// Watermarked section number — 48pt, very faint, behind the title
#place(
top + left,
dx: -6pt,
dy: 0pt,
)[
#text(
font: puterize-display,
size: 48pt,
weight: 700,
fill: puterize-rule-soft,
tracking: -0.02em,
)[#pad2(n)]
]
// Chapter label
#text(font: puterize-mono, size: 8pt, fill: puterize-blue, weight: 600)[
HOOFDSTUK #pad2(n)
]
#v(4pt)
#accent-title(it.body, size: 28pt)
#v(10pt)
#gradient-rule(width: 100%, height: 3pt)
#v(16pt)
]
}
// H2 — orange square marker + DM Sans SemiBold
show heading.where(level: 2): it => {
v(22pt)
[
#block(
fill: puterize-orange,
width: 8pt,
height: 8pt,
radius: 1.2pt,
)
#h(8pt)
#text(font: puterize-display, weight: 600, size: 14pt, fill: puterize-ink, it.body)
]
v(8pt)
}
// H3 — DM Sans Medium, softer ink
show heading.where(level: 3): it => {
v(14pt)
[
#text(font: puterize-display, weight: 500, size: 11.5pt, fill: puterize-ink-soft, it.body)
]
v(5pt)
}
// ── COVER PAGE ──────────────────────────────────────────────────────────────
let cover = {
set page(
paper: "a4",
margin: (top: 48mm, bottom: 28mm, left: 28mm, right: 28mm),
fill: puterize-blue-soft,
)
// Cover has no header/footer
set page(numbering: none)
// Top gradient bar
place(top + left, dx: 0pt, dy: 0pt)[
#gradient-rule(width: 100%, height: 5pt)
]
// Eyebrow
text(font: puterize-mono, size: 9pt, fill: puterize-muted)[#eyebrow]
v(18pt)
// Title — DM Sans Black, 52pt
text(font: puterize-display, size: 52pt, tracking: -0.035em, fill: puterize-ink)[
#title-pre
#h(0.2em)
#text(fill: puterize-blue)[#title-accent]#text(fill: puterize-orange, size: 58pt)[.]
]
v(12pt)
// Subtitle
if subtitle != "" [
text(font: puterize-body, size: 13pt, fill: puterize-ink-soft, style: "italic")[#subtitle]
v(8pt)
]
// Bottom bar with meta
place(bottom + left, dx: 0pt, dy: 0pt)[
#block(
width: 100%,
stroke: (top: (paint: puterize-blue, thickness: 1pt)),
)[
#v(14pt)
#grid(
columns: (1fr, 1fr),
row-gutter: 8pt,
column-gutter: 16pt,
// Left: brand
[
#text(font: puterize-display, weight: 700, size: 12pt, fill: puterize-blue)[Puterize]
#v(2pt)
#text(font: puterize-mono, size: 7.5pt, fill: puterize-muted)[Document template]
],
// Right: meta table
[
#set text(font: puterize-mono, size: 7.5pt)
#grid(
columns: (auto, 1fr),
column-gutter: 16pt,
row-gutter: 4pt,
[Document:], text(fill: puterize-ink, weight: 500)[#doc-name],
[Versie:], text(fill: puterize-ink, weight: 500)[#version],
[Datum:], text(fill: puterize-ink, weight: 500)[#the-date],
[Auteur:], text(fill: puterize-ink, weight: 500)[#author],
[Distributie:], text(fill: puterize-ink, weight: 500)[#distribution],
)
],
)
]
]
}
cover
// ── PAGE SETUP (after cover) ────────────────────────────────────────────────
set page(
paper: "a4",
margin: (top: 28mm, bottom: 24mm, left: 28mm, right: 28mm),
// header
header: [
#set text(font: puterize-mono, size: 7.5pt)
#block(
width: 100%,
stroke: (bottom: (paint: puterize-blue, thickness: 0.8pt)),
)[
#grid(
columns: (1fr, 1fr),
[
#text(fill: puterize-muted)[#doc-id]
],
align(right)[
#text(fill: puterize-muted)[#brand-tag]
],
)
#v(6pt)
]
],
// footer
footer: [
#set text(font: puterize-mono, size: 7.5pt)
#block(
width: 100%,
stroke: (top: (paint: puterize-blue, thickness: 0.8pt)),
)[
#v(6pt)
#grid(
columns: (1fr, 1fr),
[
#text(fill: puterize-muted)[#footer-tag]
],
align(right)[
#text(fill: puterize-muted-soft)[#context { counter(page).display() }]
],
)
]
],
numbering: "1",
footer-descent: 6pt,
header-ascent: 6pt,
)
// ── TABLE OF CONTENTS ───────────────────────────────────────────────────────
pagebreak(weak: false)
v(10pt)
align(left)[
#text(font: puterize-mono, size: 9pt, fill: puterize-blue, weight: 600)[INHOUD]
#v(6pt)
#accent-title([Wat in dit document staat], size: 30pt)
]
v(14pt)
if toc-text != "" [
text(font: puterize-body, size: 11pt, fill: puterize-ink-soft)[#toc-text]
v(20pt)
]
// TOC with custom-styled entries via a fresh counter
let toc-counter = counter("toc-num")
toc-counter.update(0)
show outline.entry: it => {
toc-counter.step()
let n = toc-counter.get().first()
if it.level == 1 {
[
#text(font: puterize-mono, size: 8.5pt, fill: puterize-blue, weight: 600)[#pad2(n)]
#h(10pt)
#text(font: puterize-display, size: 12pt, weight: 600, fill: puterize-ink)[#it.element.body]
#v(3pt)
#line(
length: 100%,
stroke: (paint: puterize-rule, thickness: 0.5pt, dash: "dashed"),
)
#v(12pt)
]
}
}
set outline(
indent: 0pt,
)
outline(target: heading.where(level: 1))
pagebreak(weak: false)
// ── CONTENT ─────────────────────────────────────────────────────────────────
body
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment