Renderer adapters
A renderer adapter lets one explicit Markdown request run one configured external program and return either checked embedded markup or one generated resource. Heine owns the request syntax, input boundary, timeout, cache, and published output. The external program remains a tool you install and choose.
An embedded SVG request may add a normalized class list to its checked root
element. This lets site CSS style an inline SVG without a wrapper. HTML and
MathML results remain renderer-owned fragments, so they do not accept class.
This guide starts with three working profile shapes. Each example assumes its
program is already available on PATH, except PlantUML, whose JAR path must
be replaced with the local absolute path. First add the profile to the root
heine.toml, then add the Markdown request, and build with explicit
permission:
heine --allow-external-renderersHeine does not discover or install a renderer. Check the program yourself
before building, for example with katex --version, typst --version, or
java -version. The profile's cache_identity is your declared version or
environment identity. Change it after changing a renderer or a meaningful part
of its environment.
When a renderer fails, Heine labels the complete authored request and presents the program's diagnostic output separately. Heine preserves that output as the renderer emitted it. For a workspace request with a body, paths and line numbers in that output refer to the isolated staged input.
KaTeX embedded mathematics
Install the KaTeX command-line program so katex is available on PATH.
This profile fixes all tool options and accepts only authored TeX bodies:
[renderers.katex]
protocol = "stream"
program = "katex"
timeout_seconds = 10
cache_identity = "katex-0.16.27"
inputs = ["body"]
result = "html"
[renderers.katex.inline]
arguments = ["--format", "htmlAndMathml"]
[renderers.katex.block]
arguments = ["--display-mode", "--format", "htmlAndMathml"]Use it inline or as a block:
The area of a circle is :render[katex]`\pi r^2`.
:::render[katex]
\int_0^1 x^2\,dx = \frac{1}{3}
:::KaTeX writes HTML to standard output. Heine checks UTF-8, replaces the request
with that trusted embedded HTML, and caches the checked result below
.heine/cache/renderers/v1/.
PlantUML SVG image
Install Java and download a PlantUML JAR. Replace /absolute/path/plantuml.jar
with the absolute path to that JAR. The profile produces an SVG resource, so
the Markdown request must provide alternative text:
[renderers.plantuml]
protocol = "stream"
program = "java"
timeout_seconds = 20
cache_identity = "plantuml-1.2025.7"
inputs = ["body", "file"]
result = "svg"
materializations = ["resource"]
[renderers.plantuml.block]
arguments = ["-jar", "/absolute/path/plantuml.jar", "-tsvg", "-pipe"]:::render[plantuml alt="Browser request and application response"]
@startuml
Browser -> Application: request
Application --> Browser: response
@enduml
:::Heine verifies that the returned document has an SVG root element, publishes
the original bytes as heine-renderers/sha256-<digest>.svg, and replaces the
request with an img element. The URL includes the configured site.base_path.
To use a checked page-owned file instead of an inline diagram, declare it in
the adjacent .page file:
[render.architecture]
file = "diagrams/architecture.puml"Then select it from a block request with an empty body:
:::render[plantuml source="architecture" alt="Application architecture"]
:::The declared file is renderer input, not a copied asset.
Typst PDF download
Install the typst command-line program. A workspace profile receives an
isolated input, output, and work directory. Typst writes its PDF only to
Heine's selected primary output file:
[renderers.typst-report]
protocol = "workspace"
program = "typst"
timeout_seconds = 30
cache_identity = "typst-0.15.0"
inputs = ["body", "file", "bundle"]
input_extension = "typ"
result = "link"
media_type = "application/pdf"
extension = "pdf"
[renderers.typst-report.block]
arguments = ["compile", "{input_file}", "{output_file}"]Heine keeps one reusable workspace slot for each workspace profile during a build session. It clears that slot before and after each request. If a platform lock prevents cleanup, Heine keeps a successful result but reports the retained path; later requests retry cleanup instead of allocating unbounded temporary directories.
:::render[typst-report label="Download the one-page report (PDF)"]
= Report title
This PDF was generated while building the site.
:::Heine replaces the request with an a element whose visible text is the
required label. The PDF receives a digest-addressed URL below
heine-renderers/, so changed bytes receive a new public URL.
For a multi-file Typst project, declare a checked source bundle in the page:
[render.report]
root = "reports/quarterly"
entry = "main.typ"The profile can use {input_dir} instead of {input_file} when its fixed
arguments need the staged bundle directory. Heine copies only the declared
bundle files, hashes every checked file into the cache key, and rejects an
extra renderer output file.
Light and dark generated images
result = "picture" represents one meaningful image in fixed light and dark
forms. The profile owns both renderer invocations; a Markdown request supplies
one alternative text and optional picture presentation fields. Heine checks
both resources, emits a native picture, and uses the light source as its
fallback. Read the generated-picture design for
the complete contract.
For complete local Typst SVG and KaTeX HTML walkthroughs, including installation, profiles, stylesheets, accessibility, and test commands, see reproducible renderer examples. That guide is the single maintained source for those external-tool recipes.
Build and development behavior
The first permitted build runs each selected renderer. Later builds reuse a
typed cache entry when the profile, request form, and complete checked input
are unchanged. Use this command when deliberately checking the installed
tools or after an external change not represented by cache_identity:
heine --allow-external-renderers --refresh-renderersAt the start of each build session, Heine removes incomplete cache files from
an interrupted atomic write. It does not scan the complete cache for every
request, and it does not evict valid entries automatically. Remove .heine/
when you want to discard the private cache completely.
heine serve --allow-external-renderers applies the same permission to every
complete watched rebuild. Renderer source files and bundles live below
content/, so changes trigger a rebuild. A --quick build can update a
selected request after a present body or declared input changes, but it remains
incomplete: change profiles, request structure, source declarations, names, or
paths with a full build.
When identical renderer bytes produce the same digest-addressed resource, Heine publishes that resource once. If another producer claims its path, Heine's collision report identifies every Markdown request that contributed to the shared resource.
Checking local renderer installations
Heine's ordinary test suite does not require an external renderer. This keeps
a source checkout testable on systems that have only Rust installed. The
repository also includes one ignored integration test that runs the locally
installed pandoc and typst programs. It checks Pandoc HTML replacements
and Typst PDF, SVG, and PNG resources through the same public build boundary
that a site uses:
cargo +1.88.0 test --test external_renderers -- --ignoredRun that command deliberately after installing both programs. It fails when a selected program is absent or does not produce its checked result. A published source checkout therefore retains the examples and test without silently skipping a requested external-tool check.
Read the reference for every field and the renderer-adapter design for the trust and output-ownership boundaries.