Spec-driven document rendering engine
Transform Markdown into publication-ready PDF and HTML — defined once, rendered anywhere.
Renderflow is a config-driven rendering engine that transforms Markdown documents into polished PDF and HTML output — no complex shell scripts, no Pandoc flags to memorize.
Define your output spec in YAML. Point it at your Markdown. Run one command.
- 📄 Multi-format output — Render to PDF, HTML, and DOCX from a single config file
- 🗂️ YAML-driven spec — Declarative, repeatable, version-controllable builds
- 🖼️ Asset management — Automatically resolves and validates image paths
- 🔄 Transform pipeline — Pluggable in-memory content transforms
- 🧩 Custom templates — Per-output Jinja2-compatible templates via Tera
- 🔍 Dry-run mode — Preview what will be built without writing any files
- 👁️ Watch mode — Automatically rebuild on file changes
- 🦀 Built with Rust — Fast, safe, and reliable
1. Create your Markdown document (input.md):
# My Document

Welcome to my publication.2. Create a config file (renderflow.yaml):
input: "input.md"
output_dir: "dist"
outputs:
- type: pdf
- type: html3. Render:
renderflow buildOutput files appear in dist/:
dist/
├── input.pdf
└── input.html
Pre-built binaries are available for Linux, macOS, and Windows on the Releases page.
Linux:
curl -L https://github.com/egohygiene/renderflow/releases/latest/download/renderflow-linux -o renderflow
chmod +x renderflow
sudo mv renderflow /usr/local/bin/macOS:
curl -L https://github.com/egohygiene/renderflow/releases/latest/download/renderflow-macos -o renderflow
chmod +x renderflow
sudo mv renderflow /usr/local/bin/Windows:
Download renderflow-windows.exe from the Releases page and place it somewhere on your PATH.
cargo install --path .# Render using the default renderflow.yaml config
renderflow build
# Render using a custom config file
renderflow build --config my-project.yaml
# Shorthand: pass the config file directly
renderflow my-project.yaml
# Preview what would be built, without writing any files
renderflow build --dry-run
# Enable verbose or debug logging
renderflow build --verbose
renderflow build --debugNote: The argument to
renderflowis always a YAML config file. The Markdown source is specified inside the config via theinputkey (e.g.input: "input.md").
Produce PDF, HTML, and DOCX from one config:
input: "report.md"
output_dir: "dist"
outputs:
- type: pdf
- type: html
template: "default"
- type: docxInject dynamic values that are replaced at build time:
input: "report.md"
output_dir: "dist"
variables:
title: "Q4 Report"
author: "Jane Smith"
outputs:
- type: htmlreport.md:
# {{title}}
*Written by {{author}}*Point an output at a custom Tera template stored in templates/:
input: "report.md"
output_dir: "dist"
outputs:
- type: html
template: "newsletter"Renderflow will render using templates/newsletter.html (Jinja2-compatible Tera syntax).
Watch mode monitors your source files and automatically rebuilds whenever a change is detected.
# Watch using the default renderflow.yaml config
renderflow watch
# Watch using a custom config file
renderflow watch my-project.yaml
# Override the debounce delay (default: 500 ms)
renderflow watch my-project.yaml --debounce 300How it works:
- An initial build runs immediately when watch mode starts.
- Renderflow watches the config file, the input document, and the
templates/directory for changes. - After a file change is detected, Renderflow waits for the debounce delay (default: 500 ms) before triggering a rebuild — so rapid successive saves don't cause redundant builds.
- Build errors are logged but do not stop the watcher; the next save will trigger another attempt.
File watching scope:
| Watched path | Mode | Notes |
|---|---|---|
| Config file | Non-recursive | e.g. renderflow.yaml |
| Input document | Non-recursive | Path from the input key |
templates/ dir |
Recursive | Watched when the directory exists |
Press Ctrl+C to stop watch mode.
Renderflow is entirely driven by a YAML spec file (default: renderflow.yaml):
input: "input.md" # Path to your source document
input_format: markdown # Optional: override auto-detected format
output_dir: "dist" # Output directory (default: dist)
variables: # Optional: key/value pairs for substitution
title: "My Document"
author: "Jane Smith"
outputs:
- type: pdf # Render to PDF (requires Pandoc + Tectonic)
- type: html # Render to HTML
template: "default" # Optional: use a custom Tera template
- type: docx # Render to Word document| Key | Required | Default | Description |
|---|---|---|---|
input |
✅ Yes | — | Path to the source document (Markdown, HTML, RST, etc.) |
input_format |
❌ No | auto-detect | Override the input format; auto-detected from file extension when omitted |
output_dir |
❌ No | dist |
Directory where output files are written |
outputs |
✅ Yes | — | List of one or more output targets (must contain at least one entry) |
outputs[].type |
✅ Yes | — | Output format: html, pdf, or docx |
outputs[].template |
❌ No | — | Name of a Tera template in the templates/ directory to use for this output |
variables |
❌ No | {} |
Map of string key/value pairs injected into the document via {{key}} placeholders |
The input_format key (or the file extension of input) controls how Pandoc reads the source document.
| Value | File Extensions | Notes |
|---|---|---|
markdown |
.md, .markdown |
Default when extension is unknown |
html |
.html, .htm |
|
rst |
.rst |
reStructuredText |
docx |
.docx |
|
epub |
.epub |
|
latex |
.tex |
When input_format is omitted, Renderflow auto-detects the format from the file extension and falls back to markdown when the extension is unrecognised.
| Type | Description | Requirements |
|---|---|---|
html |
Renders to HTML | Pandoc |
pdf |
Renders to PDF via LaTeX | Pandoc + Tectonic |
docx |
Renders to Word document | Pandoc |
Not every input → output combination is supported. For example, epub and latex inputs cannot currently be converted to docx. Renderflow reports a clear error when an unsupported combination is specified.
Templates live in a templates/ directory and use Tera syntax (Jinja2-compatible). Specify a template per output with the template key. A default HTML template is included out of the box.
Define a variables map in your config to inject dynamic values into your document:
variables:
title: "Q4 Report"
author: "Jane Smith"
version: "1.0"Reference them in your Markdown using {{key}} syntax:
# {{title}}
*Written by {{author}}*
Version: {{version}}Placeholders for undefined keys are left unchanged, and a warning is emitted so you can spot typos.
Before any output is written, Renderflow applies a series of in-memory text transforms to the source document. Transforms run in order after the file is read and before Pandoc processes it.
Replaces emoji characters with the literal text [emoji].
Why: PDF and some LaTeX backends cannot render Unicode emoji directly. This transform ensures the pipeline doesn't crash on emoji-heavy content.
Example:
| Input | Output |
|---|---|
Hello 😀 World |
Hello [emoji] World |
🎉 Party time! 🎉 |
[emoji] Party time! [emoji] |
Limitation: The replacement is a plain-text placeholder. Full SVG/image-based emoji embedding is planned for a future release.
Replaces {{key}} placeholders in the source document with values defined in the variables map of your config.
When it runs: Before Pandoc, so substituted values are part of the rendered content.
Behaviour:
- Keys are matched exactly (whitespace around the key name is trimmed, so
{{ title }}and{{title}}are equivalent). - If a placeholder references a key that is not in
variables, the placeholder is left unchanged and a warning is emitted. - Unclosed placeholders (e.g.
{{unclosed) are also left unchanged.
Example:
Config:
variables:
title: "Annual Report"
year: "2024"Document:
# {{title}} — {{year}}Rendered:
# Annual Report — 2024Normalises the language tags on fenced code blocks (```) to lowercase with surrounding whitespace stripped.
When it runs: Before Pandoc, ensuring consistent language identifiers are passed to the syntax highlighting engine.
Example:
| Input fence | Normalised fence |
|---|---|
```Rust |
```rust |
``` Python |
```python |
```JavaScript |
```javascript |
Limitation (V1): Only the opening fence language tag is normalised. The code body itself is passed through unchanged.
Renderflow processes documents through a two-phase pipeline:
Input Markdown
│
▼
┌─────────────────────┐
│ Transform Phase │ In-memory text transformations (emoji, etc.)
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Step Phase │ I/O and external tool execution (Pandoc, Tectonic)
└─────────────────────┘
│
▼
Output Files (PDF / HTML)
Key design patterns:
- Pipeline — Ordered, composable steps with clean error propagation
- Strategy — Each output format is an independent, swappable rendering strategy
- Transform — Pure in-memory text transforms applied before any I/O
- Built-in stylesheet themes
- SVG / emoji embedding in PDFs
- Plugin system for custom transforms
- Automated release workflow for pre-built binaries
MIT © Ego Hygiene