Offline rendering & diagram backends
Diagrams, charts and maps can be rendered three different ways, and — since
v2.13.0 — through two different backends. This page covers what each
combination costs, and how to build a document without Chromium installed at
all.
Both CLIs share these flags. Where they differ, it is called out.
Render modes
Section titled “Render modes”--render-mode decides when interactive content is rendered, and applies to
mermaid, PlantUML, charts and maps at once.
| Mode | What happens | Needs internet at build | Needs internet at view |
|---|---|---|---|
browser (default) |
The HTML links CDN scripts; the reader’s browser renders | No | Yes |
offline-assets |
Rendered at build time into an assets/ folder next to the output |
Yes | No |
offline-inline |
Rendered at build time and embedded in the HTML | Yes | No |
browser produces the smallest file but is useless offline. offline-inline
produces one self-contained file — the right choice for email, archives, or
feeding the HTML to an external PDF service.
Diagram backends
Section titled “Diagram backends”--diagram-backend selects what renders mermaid and PlantUML in the
offline-* modes (and for --format pdf in slidelang).
| Backend | Renders mermaid/PlantUML with | Chromium on the build machine |
|---|---|---|
chromium (default) |
Headless Chrome, via chromedp |
Required (~180 MB) |
kroki |
HTTP requests to a Kroki server | Not required |
doclang build report.doclang \ --render-mode offline-inline \ --diagram-backend kroki \ --kroki-server https://kroki.internal.example.comKroki does use Chromium internally to render mermaid — but on the server,
which is the operator’s concern, not the build machine’s. That is the trade: one
service the organization runs, instead of ~180 MB every user downloads. Omit
--kroki-server to use the public https://kroki.io.
Building with no Chromium at all
Section titled “Building with no Chromium at all”The CLIs decide whether to start Chromium per element type actually present in the document, not per requested mode. A document reaches zero Chromium when every one of its rich elements is covered:
| Element | Covered without Chromium | How |
|---|---|---|
| PlantUML | ✅ Always | HTTP to a PlantUML server, no browser involved even on the default backend |
| Mermaid | ✅ With --diagram-backend kroki |
HTTP to the Kroki server |
| Charts | ✅ Conditionally — see below | Rasterized natively in Go (go-analyze/charts) |
| Maps | ❌ Never | Leaflet tiles require a browser |
| Math | ❌ Never | MathJax requires a browser |
A chart renders natively only when all three hold:
- its type is
bar,line,pieordoughnut; - it carries no
options:block (arbitrary Chart.js config has no native equivalent, and silently approximating it would draw a different chart than the author asked for); - it is not in JSON mode.
--image-format webp also disables the native path — WebP encoding goes through
the browser.
Anything not covered falls back to Chromium deterministically. So:
# Zero Chromium: text + mermaid + a plain bar chartdoclang build report.doclang \ --render-mode offline-inline \ --diagram-backend kroki# Still needs Chromium: the document contains a mapdoclang build with-map.doclang \ --render-mode offline-inline \ --diagram-backend kroki # mermaid is covered, the map is notIf you want certainty rather than inference, watch the build log: doclang
reports when it skips the browser for natively-rendered charts, and logs
Initializing Chromium renderer... whenever it does start one. No such line
means no browser was launched.
Kroki trust model
Section titled “Kroki trust model”The Kroki server is read from --kroki-server, a flag, never frontmatter.
This is deliberate and load-bearing.
Frontmatter is document content, and document content is attacker-controllable —
a kroki_server: key would let a document redirect its own diagram traffic to a
host of the attacker’s choosing. A flag has the same trust level as --theme:
it comes from the operator running the build.
Consequently, the initial request to --kroki-server is not IP-restricted, so
an on-prem Kroki on 10.x or 192.168.x works with no special configuration.
Two restrictions do apply, and both bite in real deployments:
- Any redirect to a private or loopback IP is rejected, even one issued by your own server. A Kroki behind a load balancer or ingress that redirects internally will fail.
- Redirects are HTTPS-only.
http://kroki.internal→https://10.0.0.5fails on both counts.
Neither restriction is new — they are the same rules that have always governed
--plantuml-server.
Requests use Kroki’s POST /{type}/{format} endpoint, sending the diagram source
in the body as plain text, so diagram size is not bounded by URL length.
Output formats
Section titled “Output formats”--plantuml-format (svg or png) is honoured by both backends, and applies to
PlantUML only — mermaid is always fetched as SVG.
offline-inline requires SVG by construction: an inline diagram is embedded as
markup, not as a binary blob, so PNG bytes injected inline would be binary
garbage rather than a diagram. --plantuml-format png is therefore only
meaningful with offline-assets.
Combining png with offline-inline is not silently downgraded to SVG — each
affected diagram is replaced by a visible <div class="plantuml-error"> in the
output, and the build still succeeds. PDF output is unaffected either way: both
CLIs force SVG internally for --format pdf.
--image-format (png or webp) applies to charts and maps in offline modes
only.
Air-gapped builds
Section titled “Air-gapped builds”Fully offline builds need every remote dependency replaced:
doclang build report.doclang \ --render-mode offline-inline \ --diagram-backend kroki \ --kroki-server https://kroki.internal.example.com \ --plantuml-server https://plantuml.internal.example.comMaps are the exception with no offline story: tiles are fetched from a remote
tile server at build time, and a failure leaves a permanent
<div class="map-error"> in the output. Documents intended for air-gapped builds
should avoid map elements.
Further reading
Section titled “Further reading”- Kroki documentation — running a server on-prem with Docker
- Sanitization — how rendered output is escaped