# Zorto
> The AI-native static site generator (SSG) with executable code blocks & more.
## Zorto as a data app builder
[Zorto](/) now ships a public DuckDB database with the site.
[`/data/site.ddb`](/data/site.ddb) is generated from local repo and build metadata. [Search](/?q=zorto) reads from it. The [analytics dashboard](/analytics/) reads from it. Static hosting serves it as a file; the browser attaches it read-only when a page needs data.
No visitor analytics. No tracking, cookies, tokens, or third-party event stream. The database contains site metadata: commits, packages, content files, links, build outputs, search rows, and pipeline receipts.
The dashboard page is a thin shell. On click it lazy-loads [DuckDB-Wasm](https://duckdb.org/docs/current/clients/wasm/overview) and [Plotly.js](https://plotly.com/javascript/), fetches `/data/site.ddb`, attaches it read-only, runs configured SQL, then renders charts and tables.
Search uses the same database. That matters more than the dashboard itself: Zorto can ship one public data artifact and let different browser surfaces query it.
One panel is just SQL over the attached database:
```sql
SELECT kind, count(*) AS files, sum(bytes) AS bytes
FROM site.main.build_outputs
GROUP BY kind
ORDER BY bytes DESC;
```
## Layers
1
Content
Markdown owns pages and explanations
website/content
2
Config
TOML owns sources, limits, panels, queries
website/data
3
Code
Rust, Python, SQL, HTML, CSS, and JS do the work
crates + pipelines + static/js
The editing contract stays small.
This follows the separation I want for Zorto:
- Content: Markdown owns the page.
- Config: TOML owns the data app shape.
- Code: Rust, Python, SQL, HTML, CSS, and JavaScript handle execution.
Humans get stable files to edit. Agents get boundaries. The repo stays legible.
## Links
## Implementation
`website/bin/build-meta` is a self-contained `uv` script. It uses DuckDB, runs a timed current-code Zorto build through the Rust CLI, performs privacy checks, writes a temporary database, validates it, then atomically replaces `website/static/data/site.ddb`.
`website/data/meta.toml` owns the pipeline settings: output path, build output directory, collection limits, content include and exclude rules, privacy checks, and the build command.
`website/data/analytics.toml` owns the dashboard: views, panels, KPIs, SQL queries, table columns, and runtime assets.
The database includes `pipeline_steps`, so generation leaves receipts: step name, kind, status, duration, output count, command, and details.
## The app surface
A dashboard is data plus frontend.
Python is good for data work: pulls, transforms, validation, orchestration. Many Python dashboard tools eventually emit JavaScript anyway. Zorto keeps the split direct:
- Build data locally with `uv`, Python, and DuckDB.
- Ship `.ddb` files beside static HTML.
- Render the interface with HTML, CSS, and JavaScript.
- Query in the browser with DuckDB-Wasm when embedded data is enough.
- Use remote DuckDB when live data is worth it.
That can stay static or grow into dynamic browser interfaces. The default remains standards first: files, SQL, HTML, CSS, JavaScript, and Rust where the generator needs to be solid.
## Boundaries
This is a zorto.dev implementation, not a public Zorto data API.
There is no `[data]` config in Zorto core yet. There is no automatic pipeline hook in `zorto build`. DuckDB-Wasm and Plotly are pinned CDN-loaded runtime assets for now.
## Next
- Promote the stable pieces into Zorto after zorto.dev proves them.
- Keep `uv` as the local Python orchestration layer.
- Keep DuckDB as the local database layer.
- Use DuckDB's beta [Quack protocol](https://duckdb.org/quack/) for remote DuckDB when live access is useful.
- Use [DuckLake](https://ducklake.select/) when the data wants lakehouse-style partitioning instead of a single DuckDB database file.
Zorto remains an AI-native static site generator with executable code blocks. The `& more` is now visible: data apps built from small files, local pipelines, and browser-native interfaces.
## Executable visualizations
Zorto can now render Python visualizations inline. No configuration, no magic comments — just write your code and the chart appears.
## Matplotlib
The most popular Python plotting library. Just `import matplotlib.pyplot as plt` and plot:
```{python}
import matplotlib.pyplot as plt
import math
x = [i * 0.1 for i in range(100)]
y = [math.sin(v) for v in x]
plt.figure(figsize=(8, 4))
plt.plot(x, y, color='#7c3aed', linewidth=2)
plt.title('sin(x)')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True, alpha=0.3)
plt.tight_layout()
```
## Plotly
Interactive charts that respond to hover, zoom, and pan:
```{python}
import plotly.graph_objects as go
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
revenue = [12, 15, 13, 17, 21, 24, 22, 28, 31, 29, 35, 42]
costs = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
fig = go.Figure()
fig.add_trace(go.Scatter(x=months, y=revenue, name='Revenue', line=dict(color='#7c3aed', width=3)))
fig.add_trace(go.Scatter(x=months, y=costs, name='Costs', line=dict(color='#06b6d4', width=3)))
fig.update_layout(
title='Monthly financials',
xaxis_title='Month',
yaxis_title='$K',
template='plotly_dark',
height=400,
)
```
## Seaborn
Statistical visualizations built on matplotlib — automatically captured:
```{python}
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt
import random
random.seed(42)
data = [random.gauss(0, 1) for _ in range(500)]
plt.figure(figsize=(8, 4))
sns.histplot(data, bins=30, kde=True, color='#7c3aed', alpha=0.7, edgecolor='white')
plt.title('Normal distribution (n=500)')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
```
## Altair
Declarative statistical visualization:
```{python}
import altair as alt
data = alt.Data(values=[
{'x': i, 'y': i ** 2, 'category': 'quadratic'} for i in range(20)
] + [
{'x': i, 'y': i * 3, 'category': 'linear'} for i in range(20)
])
chart = alt.Chart(data).mark_line(strokeWidth=3).encode(
x='x:Q',
y='y:Q',
color=alt.Color('category:N', scale=alt.Scale(range=['#7c3aed', '#06b6d4'])),
).properties(
title='Growth curves',
width=600,
height=300,
)
```
## Multiple plots in one block
```{python}
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
categories = ['Rust', 'Python', 'Go', 'JS']
values = [95, 88, 72, 68]
ax1.bar(categories, values, color=['#7c3aed', '#06b6d4', '#10b981', '#f59e0b'])
ax1.set_title('Language satisfaction')
ax1.set_ylabel('%')
import random
random.seed(42)
x = [random.gauss(0, 1) for _ in range(100)]
y = [xi * 0.7 + random.gauss(0, 0.5) for xi in x]
ax2.scatter(x, y, alpha=0.6, color='#7c3aed', s=20)
ax2.set_title('Correlation')
ax2.set_xlabel('x')
ax2.set_ylabel('y')
plt.tight_layout()
```
## How it works
Zorto's executable code blocks run Python at build time via an embedded interpreter. After your code executes, Zorto checks if any visualization libraries produced output:
- **matplotlib**: Detects open figures via `plt.get_fignums()`, saves as PNG, embeds inline
- **plotly**: Detects `plotly.graph_objects.Figure` instances, embeds as interactive HTML
- **seaborn**: Uses matplotlib under the hood — automatically captured
- **altair**: Detects `altair.Chart` instances, embeds as interactive HTML
Zero configuration. Zero magic comments. Just write Python.
## Zorto roadmap to v1
The roadmap for Zorto to v1 release.
## Stable & production-ready
The release of v1.0.0 of Zorto will primarily indicate readiness for production; the APIs are stable, the code is well-tested and high-quality. Until that point, I'm favoring iteration speed. That said, Zorto is in production powering this website and all of my own static websites.
## Improved `check` for test/lint/compile
~~For people & agents, `zorto check` should provide confidence that a website is following best practices.~~ Done — `zorto check` validates broken links, frontmatter, and missing assets.
## Good default themes
~~I want at least 8, ideally 16 or more good default themes.~~ Done — 16 built-in themes shipped, all with light and dark mode support.
## Skills & more for agents
I want to build in skills that people or agents can install for ease of use. I may consider adding an MCP server.
## Desktop app and local web app (GUIs)
I want to ship a desktop app (iced GUI) and web app (TBD) that will make it even easier for users to get started with their first website. I may allow you to plug your agent into these UIs to see your website come to life as well.
## Improved executable code blocks
Currently, the executable code blocks are quite limited. I want to add support for freezing pages (i.e. caching the results), visualizations through common Python libraries, and perhaps more languages. This is an extremely powerful feature to continue building on.
## ~~Built-in search~~ (done)
Zorto search is moving to DuckDB. The current zorto.dev implementation ships `site.ddb`, loads DuckDB-Wasm on demand, and queries search data in the browser.
## Ease of use
I want Zorto to be the easiest to use SSG for people & agents in this new era of AI. Docs must be excellent. Website creation should be easy.
## Introducing Zorto!
Zorto is the AI-native static site generator (SSG) inspired by [Zola](https://www.getzola.org/) and [Quarto](https://quarto.org/).
## Built for AI and agents
Zorto is designed from the ground up for agentic software engineering. AI can create a full website in minutes and maintain it with ease: the config-driven architecture, built-in themes, and opinionated linting make it natural for both humans and agents to work with.
More on AI-native workflows coming soon as we approach v1.
## Executable code blocks
Zorto's defining feature. Fenced code blocks marked with `{python}` or `{bash}` run at build time:
```{bash}
echo "This ran at $(date +%Y-%m-%d)"
```
The output is rendered inline. This powers self-updating documentation: our [CLI reference](/docs/reference/cli/) runs `zorto --help` at build time, so the docs are always current.
## Built-in themes
Zorto ships with 16 built-in themes including `zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, and more. Set `theme = "dkdc"` in `config.toml` and you get a complete site: navbar, footer, theme toggle, responsive design.
All themes support light/dark mode toggling. Override any template or SCSS variable locally; your files always win.
## GitHub-style callouts
Write callouts with standard GitHub alert syntax:
> [!TIP]
> Zorto renders these natively using pulldown-cmark's GFM support. No shortcode needed.
Five types: `NOTE`, `TIP`, `WARNING`, `CAUTION`, `IMPORTANT`.
## Shortcodes
Nineteen built-in shortcodes, spanning content (`include`, `tabs`, `note`, `details`, `figure`, `youtube`, `gist`, `mermaid`), diagrams (`flow`, `layers`, `tree`, `compare`, `cascade`), API references (`pyref`, `configref`), and presentations (`slide_image`, `speaker_notes`, `fragment`, `columns`). Plus you can create your own with Tera templates.
## Template linting
`zorto check` warns about hardcoded strings in templates, inspired by clippy. User-facing text belongs in `config.toml` or content files, not in HTML templates. This keeps themes reusable.
## llms.txt
Zorto generates [llms.txt](/llms.txt) and [llms-full.txt](/llms-full.txt) by default: structured text files that help LLMs understand your site. This is enabled out of the box; set `generate_llms_txt = false` in `config.toml` to disable.
## What's next
We're working toward v1 with more built-in themes, support for additional languages in executable code blocks (beyond Python and Bash), and broader visualization support. More on all of that soon.
## Free and open source
Zorto is [MIT-licensed](https://github.com/dkdc-io/zorto/blob/main/LICENSE): free and open source forever. We may consider dual-licensing with Apache 2.0 in the future; [open an issue](https://github.com/dkdc-io/zorto/issues) if that would be useful for your project.
## Install
```bash
curl -LsSf https://dkdc.sh/zorto/install.sh | sh
```
Or via [crates.io](https://crates.io/crates/zorto) / [PyPI](https://pypi.org/project/zorto/).
Check out the [getting started](/docs/getting-started/) guide, or browse the [source on GitHub](https://github.com/dkdc-io/zorto).
## Free & open source
Zorto is [MIT-licensed](https://github.com/dkdc-io/zorto/blob/main/LICENSE). Use it for anything — personal sites, commercial projects, internal tools — with no restrictions beyond including the license and copyright notice.
## Why MIT?
MIT is one of the most permissive open source licenses. No copyleft requirements, no patent clauses, no usage restrictions.
Companies can embed Zorto in internal toolchains without legal review, and contributors can fork freely without license-compatibility concerns.
## Community
Zorto is developed in the open on [GitHub](https://github.com/dkdc-io/zorto). Issues, pull requests, and discussions are welcome. The project follows [semantic versioning](https://semver.org/) for both the Rust crate and Python package.
The codebase is Rust (core engine, CLI, themes) with Python bindings via PyO3. If you want to contribute, the [AGENTS.md](https://github.com/dkdc-io/zorto/blob/main/AGENTS.md) file in the repo root has the architecture overview and development commands.
## Distribution
| Channel | Command |
|---------|---------|
| Shell installer | `curl -LsSf https://dkdc.sh/zorto/install.sh \| sh` |
| PyPI | `uv tool install zorto` |
| crates.io | `cargo install zorto` |
| Source | `git clone https://github.com/dkdc-io/zorto` |
## Further reading
- [Installation](/docs/getting-started/installation/) — step-by-step install instructions
- [AI-native](/docs/concepts/ai-native/) — Zorto's design philosophy
## Executable code blocks
Zorto can execute Python and Bash code blocks at build time and render their output inline.
WriteMarkdown with code blocks
→
ParseFind executable blocks
→
ExecuteRun via PyO3 or shell
→
RenderOutput inlined in HTML
## Python blocks
Use the `{python}` language tag:
````markdown
```{python}
import math
print(f"Pi is approximately {math.pi:.4f}")
```
````
At build time, Zorto runs the code and inserts the output below the block.
## Bash blocks
Use the `{bash}` (or `{sh}`) language tag:
````markdown
```{bash}
echo "Hello from $(uname)"
date +%Y-%m-%d
```
````
## File attribute
Run a script file instead of inline code:
````markdown
```{python file="scripts/analysis.py"}
```
````
The file path is relative to the content file's directory.
## Disabling execution
To build without executing code blocks:
```bash
zorto --no-exec build
```
This renders the code blocks as plain syntax-highlighted code.
## Output rendering
- **stdout** is captured and displayed as a code block below the source
- **stderr** is displayed as a warning block
- **Non-zero exit codes** produce an error block with the return code
> [!TIP]
> Executable code blocks are great for keeping CLI references up to date. Use `zorto --help` in a `{bash}` block and the docs always match the current version.
## Python runtime
Zorto embeds Python via [PyO3](https://pyo3.rs/) — code blocks run in-process, not by shelling out. If a `.venv` directory exists at or above the site root (or `VIRTUAL_ENV` is set), Zorto activates its site-packages automatically, giving code blocks access to installed packages.
## Security considerations
Executable code blocks run with the same permissions as the `zorto` process. In CI environments, treat executable code blocks like any other build script — review content before building untrusted markdown. Use `zorto --no-exec build` to skip execution when building untrusted content.
## Further reading
- [How to use executable code blocks](/docs/how-to/executable-code-blocks/) — setup, file attribute, error handling
- [CLI reference](/docs/reference/cli/) — `--no-exec` flag and other options
- [AI-native](/docs/concepts/ai-native/) — how executable code blocks fit into Zorto's design philosophy
## Shortcodes reference
Complete reference for all built-in shortcodes. See [shortcodes concept](/docs/concepts/shortcodes/) for an overview and [how to customize your theme](/docs/how-to/customize-theme/) for creating custom shortcodes.
## include
Include the contents of another file.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `path` | string | *required* | Path to the file (relative to project root), or an `https://` URL |
| `strip_frontmatter` | bool | `false` | Remove `+++`-delimited TOML frontmatter from included content |
| `strip_heading` | bool | `false` | Remove the first ATX heading from included content |
| `rewrite_links` | bool | `false` | Rewrite relative `.md` links to clean URL paths |
**Example:**
Generates HTML with function signatures, class methods, and docstrings. Doctest examples (`>>>` lines in docstrings) are executed at build time and their output is rendered inline.
## configref
Auto-generate configuration reference from a Rust source file's doc comments and serde attributes.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `src` | string | *required* | Path to Rust source file (relative to site root) |
**Example:**
## tree
File tree visualization. Body content defines the tree structure, one entry per line.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `caption` | string | `""` | Caption text below the tree |
Lines use indentation (2 spaces per level) for nesting. Append `[tag]` for labels. Directories end with `/`.
**Example:**
## compare
Side-by-side comparison cards.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `left_title` | string | `""` | Title for the left card |
| `left` | string | *required* | Body text for the left card |
| `right_title` | string | `""` | Title for the right card |
| `right` | string | *required* | Body text for the right card |
| `left_style` | string | `"accent"` | Style: `accent` (blue), `green`, or `muted` |
| `right_style` | string | `"green"` | Style: `accent`, `green`, or `muted` |
| `caption` | string | `""` | Caption text below |
**Example:**
## speaker_notes
Speaker notes for presentation templates. Whether notes render in a speaker view depends on the deck template.
**Syntax:**
{% speaker_notes() %}
Remember to mention the key point about performance.
{% end %}
## fragment
Progressive reveal: content appears on each click/advance within a slide.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `style` | string | `"fade-in"` | Animation style. Validated against an allowlist; an unknown value errors at build time rather than rendering a no-op fragment. |
Allowed styles: `fade-in`, `fade-out`, `fade-up`, `fade-down`, `fade-left`, `fade-right`, `grow`, `shrink`, `strike`, `highlight-red`, `highlight-blue`, `highlight-green`, `highlight-current-red`, `highlight-current-blue`, `highlight-current-green`.
**Syntax:**
{% fragment(style="fade-in") %}
This appears on click.
{% end %}
## columns
Multi-column layout. Body content is split on `` markers.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `widths` | string | equal | Pipe-separated column widths (e.g. `"60%|40%"`) |
**Syntax:**
{% columns(widths="60%|40%") %}
Left column content
<!-- column -->
Right column content
{% end %}
## Customize your theme
Override templates, styles, and shortcodes without forking the theme.
Local files always take priority over theme defaults.
## Override a template
Create the same file path in your local `templates/` directory:
```
templates/page.html
```
Your local file takes priority over the theme's version. Start by copying the theme's template and modifying it — or write one from scratch that extends `base.html`.
## Override styles
Creating `sass/style.scss` in your project replaces the theme's stylesheet entirely (local files overlay theme files by filename).
For lighter customization, create `sass/custom.scss` and load it via the `extra_head` [template block](/docs/concepts/glossary/#template-block) (see below). Zorto compiles [SCSS](/docs/concepts/glossary/#scss) to CSS at build time — `sass/custom.scss` becomes `/custom.css` in the output. All built-in themes use CSS custom properties you can override:
```scss
// sass/custom.scss
:root {
--accent: #e74c3c;
--background: #fafafa;
--color: #1e293b;
--max-width: 900px;
}
```
All themes support `--accent`, `--background`, `--background-raised`, `--color`, `--color-muted`, `--border-color`, and `--code-bg`.
## Add custom shortcodes
Create templates in `templates/shortcodes/`:
```html
{# templates/shortcodes/callout.html #}
{{ body }}
```
Use in markdown:
{% callout(type="warning") %}
This is a custom callout.
{% end %}
## Inject into the base template
All built-in themes define an `extra_head` [template block](/docs/concepts/glossary/#template-block) you can fill without replacing the entire layout. Create `templates/base.html` in your project — it extends the theme's `base.html` (Zorto resolves the extends to the theme version, not to itself):
```html
{% extends "base.html" %}
{% block extra_head %}
{% endblock %}
```
## Switch themes
Change the `theme` field in `config.toml`:
```toml
theme = "dark"
```
Available themes: `zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, `charcoal`. All include light/dark mode toggling.
## Related guides
- [Themes](/docs/concepts/themes/) — how the theme system works
- [Customize navigation and footer](/docs/how-to/customize-nav-footer/) — menus, logo, social links via config
- [Templates](/docs/concepts/templates/) — the Tera template engine and block inheritance
## Add a sitemap
Zorto generates `sitemap.xml` automatically. It is enabled by default — no configuration needed.
## Verify
Build your site and confirm the sitemap exists:
```bash
zorto build
ls public/sitemap.xml
```
If you need to disable it for some reason: `generate_sitemap = false` in `config.toml`.
## Add a robots.txt
Create `static/robots.txt` to point crawlers to your sitemap:
```
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
```
Replace `https://example.com` with your `base_url`. Note: `static/robots.txt` is a plain file, not a template — you must update the URL manually if your `base_url` changes.
## Submit to search engines
Submit `https://example.com/sitemap.xml` (using your `base_url`) to [Google Search Console](https://search.google.com/search-console) and [Bing Webmaster Tools](https://www.bing.com/webmasters). Both re-crawl automatically once submitted.
## Related guides
- [Optimize for SEO](/docs/how-to/seo/) — meta tags, Open Graph, canonical URLs
- [Deploy your site](/docs/how-to/deploy/) — hosting setup for each platform
## Create charts with seaborn
Use [seaborn](https://seaborn.pydata.org/) in executable code blocks to create statistical visualizations. Seaborn builds on matplotlib, so charts render as inline PNGs.
## Setup
Add `seaborn` and `matplotlib` to your site's `pyproject.toml`:
```toml
[project]
dependencies = ["seaborn", "matplotlib"]
```
Then run `uv sync` in your site directory.
## Distribution plot
````markdown
```{python}
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt
import random
random.seed(42)
data = [random.gauss(0, 1) for _ in range(500)]
plt.figure(figsize=(8, 4))
sns.histplot(data, bins=30, kde=True, color='#7c3aed', alpha=0.7)
plt.title('Normal distribution (n=500)')
plt.tight_layout()
```
````
Here it is rendered:
```{python}
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt
import random
random.seed(42)
data = [random.gauss(0, 1) for _ in range(500)]
plt.figure(figsize=(8, 4))
sns.histplot(data, bins=30, kde=True, color='#7c3aed', alpha=0.7, edgecolor='white')
plt.title('Normal distribution (n=500)')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
```
## Box plot
```{python}
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt
import random
random.seed(42)
data = {
'Language': ['Rust'] * 50 + ['Python'] * 50 + ['Go'] * 50,
'Build time (s)': (
[random.gauss(2, 0.5) for _ in range(50)] +
[random.gauss(5, 1.5) for _ in range(50)] +
[random.gauss(3, 0.8) for _ in range(50)]
),
}
plt.figure(figsize=(8, 4))
sns.boxplot(x='Language', y='Build time (s)', data=data, palette=['#7c3aed', '#06b6d4', '#10b981'])
plt.title('Build times by language')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
```
## Heatmap
```{python}
import matplotlib
matplotlib.use('Agg')
import seaborn as sns
import matplotlib.pyplot as plt
import random
random.seed(42)
matrix = [[random.random() for _ in range(6)] for _ in range(6)]
labels = ['A', 'B', 'C', 'D', 'E', 'F']
plt.figure(figsize=(7, 5))
sns.heatmap(matrix, annot=True, fmt='.2f', xticklabels=labels, yticklabels=labels, cmap='viridis')
plt.title('Correlation matrix')
plt.tight_layout()
```
## How it works
Seaborn creates matplotlib figures under the hood, so Zorto captures them exactly the same way — by detecting open figures via `plt.get_fignums()` and saving them as inline PNGs.
Use `matplotlib.use('Agg')` before importing seaborn to ensure the non-interactive backend is used during builds.
## Related guides
- [Use executable code blocks](/docs/how-to/executable-code-blocks/) — setup and general usage
- [Create charts with matplotlib](/docs/how-to/matplotlib/) — lower-level matplotlib usage
## Create interactive charts with plotly
Use [plotly](https://plotly.com/python/) in executable code blocks to generate interactive charts with hover, zoom, and pan.
## Setup
Add `plotly` to your site's `pyproject.toml`:
```toml
[project]
dependencies = ["plotly"]
```
Then run `uv sync` in your site directory.
## Basic line chart
````markdown
```{python}
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=[1, 2, 3, 4, 5],
y=[2, 4, 7, 11, 16],
mode='lines+markers',
line=dict(color='#7c3aed', width=3),
))
fig.update_layout(title='Growth over time', height=400)
```
````
Here it is rendered — try hovering over the points:
```{python}
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=[1, 2, 3, 4, 5],
y=[2, 4, 7, 11, 16],
mode='lines+markers',
line=dict(color='#7c3aed', width=3),
))
fig.update_layout(title='Growth over time', template='plotly_dark', height=400)
```
## Scatter plot
```{python}
import plotly.graph_objects as go
import random
random.seed(42)
x = [random.gauss(0, 1) for _ in range(200)]
y = [xi * 0.7 + random.gauss(0, 0.5) for xi in x]
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x, y=y, mode='markers',
marker=dict(color='#7c3aed', size=6, opacity=0.6),
))
fig.update_layout(
title='Correlation',
xaxis_title='x', yaxis_title='y',
template='plotly_dark', height=400,
)
```
## Bar chart
```{python}
import plotly.graph_objects as go
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
revenue = [12, 15, 13, 17, 21, 24]
costs = [10, 11, 12, 13, 14, 15]
fig = go.Figure()
fig.add_trace(go.Bar(x=months, y=revenue, name='Revenue', marker_color='#7c3aed'))
fig.add_trace(go.Bar(x=months, y=costs, name='Costs', marker_color='#06b6d4'))
fig.update_layout(
title='Monthly financials',
barmode='group',
template='plotly_dark', height=400,
)
```
## How it works
Zorto detects `plotly.graph_objects.Figure` instances in your code's local variables after execution. Each figure is converted to standalone HTML (with the plotly.js CDN) and embedded directly in the page.
The output is fully interactive — readers can hover for tooltips, zoom, pan, and export.
## Related guides
- [Use executable code blocks](/docs/how-to/executable-code-blocks/) — setup and general usage
- [Create charts with altair](/docs/how-to/altair/) — another interactive charting library
## Shortcodes
Shortcodes let you embed rich, structured content in markdown without writing raw HTML. They bridge the gap between markdown's simplicity and the flexibility of full templates.
## Syntax
**Inline shortcodes** (no body) use double curly braces:
## Shortcodes vs. callouts vs. templates
| Tool | Best for |
|------|----------|
| Callouts | Inline alerts in prose (note, warning, tip) |
| Shortcodes | Reusable rich components (figures, tabs, embeds) |
| Templates | Full page layouts and structural HTML |
## Further reading
- [Shortcodes reference](/docs/reference/shortcodes/) — all 19 built-in shortcodes with live examples
- [Callouts](/docs/concepts/callouts/) — GitHub-style alert boxes
- [Templates](/docs/concepts/templates/) — the Tera template engine
- [How to customize your theme](/docs/how-to/customize-theme/) — create custom shortcodes
## Installation
Zorto runs on macOS and Linux. Windows support is available via WSL.
The quickest way to install:
```bash
curl -LsSf https://dkdc.sh/zorto/install.sh | sh
```
Alternatively, install from PyPI (requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) or pip):
```bash
uv tool install zorto
```
The Python package includes the same Rust engine — there is no performance difference.
Or build from source (requires [Rust](https://www.rust-lang.org/tools/install) 1.85+):
```bash
cargo install zorto
```
Confirm it worked:
```bash
zorto --version
```
You should see something like `zorto 0.x.y`. If the command is not found, make sure the install location is in your `PATH`.
You're ready for the [tutorial](/docs/getting-started/quick-start/).
## Live reload
Zorto's dev server watches your files, rebuilds automatically, and refreshes the browser — no manual reload needed.
## Usage
```bash
zorto preview
```
This starts a local server (default port 1111). Add `--open` to automatically open your site in the browser.
## What gets watched
The server monitors all files that affect your site:
| Directory | Triggers rebuild on |
|-----------|-------------------|
| `content/` | Any `.md` file change |
| `templates/` | Any `.html` file change |
| `sass/` | Any `.scss` file change |
| `static/` | Any file change (copied to output) |
| `config.toml` | Configuration change |
## How it works
SaveEdit a file
→
DetectFilesystem event
→
RebuildSite recompiles (~100ms)
→
PushSSE event sent
→
RefreshBrowser reloads
Uses server-sent events (SSE). No browser extension required.
1. You save a file.
2. Zorto detects the change via filesystem events.
3. The site rebuilds (typically under 100ms).
4. The server pushes a reload event over [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (server-sent events).
5. A small injected script in every page listens for the event and triggers a browser refresh.
## Options
```bash
zorto preview --port 8080 # custom port
zorto preview --open # auto-open browser
zorto preview --interface 0.0.0.0 # expose to local network
zorto --no-exec preview # skip executable code blocks
```
## Network access
By default, the server binds to `127.0.0.1` (localhost only). Use `--interface 0.0.0.0` to make the preview accessible from other devices on your network — useful for testing on phones or tablets.
## Further reading
- [Fast](/docs/concepts/fast/) — why build times are under 100ms
- [CLI reference](/docs/reference/cli/) — all preview server options
- [Troubleshooting](/docs/how-to/troubleshooting/) — preview server issues and fixes
## Configuration reference
Complete reference for `config.toml`. See [configuration concept](/docs/concepts/configuration/) for the mental model and examples. The tables below are auto-generated from the Zorto source code.
Top-level settings
Top-level site configuration, loaded from `config.toml`.
Field
Type
Default
Description
base_url
string
required
Site base URL without trailing slash (e.g. `"https://example.com"`).
title
string
""
Site title, used in feeds, templates, and `llms.txt`.
description
string
""
Site description, used in feeds and `llms.txt`.
default_language
string
"en"
Default language code (default: `"en"`).
compile_sass
bool
true
Compile SCSS files from `sass/` directory (default: `true`).
generate_feed
bool
false
Generate an Atom feed at `/atom.xml` (default: `false`).
generate_sitemap
bool
true
Generate a sitemap at `/sitemap.xml` (default: `true`).
generate_llms_txt
bool
true
Generate `llms.txt` and `llms-full.txt` (default: `true`).
theme
string
null
Built-in theme name (e.g. `"zorto"`, `"dkdc"`, `"default"`). When set, the theme provides default templates and SCSS. Local `templates/` and `sass/` files override theme defaults.
extra
table
{}
Arbitrary extra values accessible in templates as `config.extra`.
generate_md_files
bool
false
Generate `.md` output files alongside HTML for every page (default: `false`).
compile_all_themes
bool
false
Compile CSS for all available themes as `style-{name}.css` (default: `false`). When enabled, every built-in theme's SCSS is compiled in addition to the active theme's `style.css`. Useful for theme preview/switcher pages.
Configuration for the Markdown rendering pipeline.
Field
Type
Default
Description
highlight_code
bool
true
Enable syntax highlighting for fenced code blocks (default: `true`).
insert_anchor_links
string
"none"
Insert anchor links on headings.
highlight_theme
string
null
Syntect theme name (default: `"base16-ocean.dark"`).
external_links_target_blank
bool
false
Open external links in a new tab.
external_links_no_follow
bool
false
Add `rel="nofollow"` to external links.
external_links_no_referrer
bool
false
Add `rel="noreferrer"` to external links.
smart_punctuation
bool
false
Enable smart punctuation (curly quotes, em dashes, etc.).
[[taxonomies]]
A taxonomy definition from `[[taxonomies]]` in `config.toml`.
Field
Type
Default
Description
name
string
required
Taxonomy name (e.g. `"tags"`, `"categories"`).
[[content_dirs]]
Configuration for loading an external directory of plain markdown as content.
Field
Type
Default
Description
path
string
required
Path to the external directory (relative to site root).
url_prefix
string
required
URL prefix for generated pages (e.g. `"docs"` → `/docs/...`).
template
string
"page.html"
Template for generated pages (default: `"page.html"`).
section_template
string
"section.html"
Template for generated sections (default: `"section.html"`).
sort_by
string
null
Sort order for pages within generated sections.
rewrite_links
bool
false
Rewrite relative `.md` links in content to clean URL paths.
exclude
string[]
[]
Files to exclude (relative to the external directory, e.g. `"reference/cli.md"`). Excluded files are expected to exist as manual content in `content/`.
## Section frontmatter
Used in `_index.md` files:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | string | `""` | Section title |
| `description` | string | `""` | Section description |
| `sort_by` | string | `"date"` | Sort pages by: `"date"`, `"title"` |
| `paginate_by` | int | `0` | Pages per pagination page (0 = no pagination) |
| `template` | string | `"section.html"` | Custom template for this section |
| `[extra]` | table | `{}` | Custom data for templates |
## Page frontmatter
Used in regular `.md` files:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | string | `""` | Page title |
| `date` | string | `""` | Publication date (YYYY-MM-DD) |
| `author` | string | `""` | Author name |
| `description` | string | `""` | Page description for SEO |
| `draft` | bool | `false` | Exclude from production builds |
| `slug` | string | filename | Override the URL slug |
| `template` | string | `"page.html"` | Custom template |
| `aliases` | array of strings | `[]` | Redirect old URLs to this page |
| `sort_by` | string | `"date"` | Sort child pages by: `"date"`, `"title"` |
| `paginate_by` | int | `0` | Pages per pagination page (0 = no pagination) |
| taxonomy fields | array of strings | `[]` | Taxonomy values as top-level arrays (e.g. `tags = ["rust"]`) |
| `[extra]` | table | `{}` | Custom data for templates |
## Create a presentation
This guide walks through creating a slide deck with Zorto. The example uses a reveal.js template, but the content model also works with native HTML/CSS/JS deck templates.
## 1. Create the presentation template
Add a `presentation.html` template to your site's `templates/` directory. This template assembles slides into a deck. See the [zorto.dev source](https://github.com/dkdc-io/zorto/tree/main/website/templates) for working template examples.
The template iterates `section.pages` and wraps each page's content in a `` element. It can map `page.extra` fields to CSS classes, data attributes, backgrounds, keyboard behavior, or whatever the deck runtime expects.
## 2. Create the section
Create a directory for your presentation with an `_index.md`:
```
content/presentations/my-talk/_index.md
```
```toml
+++
title = "My Talk"
description = "A presentation about something interesting."
template = "presentation.html"
sort_by = "weight"
render_pages = false
[extra]
width = 1050
height = 700
transition = "slide"
reveal_theme = "black"
+++
```
Key settings:
- `template = "presentation.html"`: uses your presentation template
- `sort_by = "weight"`: orders slides by their weight field
- `render_pages = false`: slides only exist in the assembled deck
- `[extra]`: passes deck-level settings to the template
## 3. Add slides
Each slide is a markdown file in the presentation directory. Use `weight` to control order:
```toml
+++
title = "Welcome"
weight = 10
[extra]
layout = "center"
background_color = "#1a1a2e"
+++
# Welcome to my talk
*A subtitle goes here*
```
Increment weights by 10 to leave room for inserting slides later.
## 4. Use layouts and backgrounds
Control slide appearance via `[extra]` frontmatter:
| Field | Effect |
|-------|--------|
| `layout` | CSS class: `center`, `image-left`, `image-right`, `image-full`, `title` |
| `background_color` | Solid background color (e.g. `"#1a1a2e"`) |
| `background_image` | Background image path or URL |
| `background_size` | CSS background-size (e.g. `"cover"`, `"contain"`) |
| `background_opacity` | Background opacity (e.g. `"0.3"`) |
| `transition` | Template-specific transition setting |
## 5. Use presentation shortcodes
**Progressive reveal**: content appears on each click:
{% fragment(style="fade-in") %}
This appears first.
{% end %}
{% fragment(style="fade-in") %}
This appears second.
{% end %}
**Multi-column layout**:
{% columns() %}
Left column content
<!-- column -->
Right column content
{% end %}
With custom widths: `columns(widths="60%|40%")`.
**Speaker notes**: keep notes beside the slide source. Speaker-view behavior depends on the template:
{% speaker_notes() %}
Remember to mention the key point here.
{% end %}
**Positioned images**: place images at arbitrary coordinates:
## 6. Build and preview
```bash
zorto preview --open
```
Navigation depends on the template. The zorto.dev native deck supports arrow keys, space, and fullscreen.
## Data apps
Zorto can publish data apps: HTML, CSS, JavaScript, and DuckDB data served locally, shipped as static assets, or reached remotely over time.
This is experimental today. The zorto.dev analytics page is the first prototype, not a stable public Zorto API.
## Current shape
The zorto.dev analytics prototype uses three layers:
- **Content** lives in Markdown and owns the page title, description, and explanatory prose.
- **Config** lives in TOML and owns pipeline paths, runtime asset URLs, dashboard views, SQL queries, panel bindings, and table columns.
- **Code** is machinery: a self-contained `uv` script builds the database, a template defines the page shell, and JavaScript loads DuckDB-Wasm and Plotly after user intent.
The public artifact is `site.ddb`, a DuckDB database shipped beside the site. The browser fetches it, attaches it read-only with DuckDB-Wasm, and runs dashboard queries locally. That is the static-first path, not the ceiling.
## What ships
The prototype emits:
- `website/static/data/site.ddb` — public repository, site, build, content, package, and pipeline metadata
- `website/static/data/analytics-dashboard.json` — generated dashboard manifest compiled from TOML
- `website/static/js/data-app-runtime.js` — reusable static data-app loading and query machinery
- `website/static/js/analytics-dashboard.js` — analytics-specific renderers
The metadata generator intentionally avoids private data: no author emails, no absolute repo paths, no environment variables, no tokens, and no untracked filenames.
## Runtime assets
The analytics page lazy-loads pinned CDN assets for DuckDB-Wasm and Plotly after the visitor clicks the load control. Normal pages do not load those assets.
Vendored runtime assets, offline builds, and a supported asset policy are future design work.
## What is not stable yet
These pieces are still website-local:
- The `website/data/meta.toml` pipeline manifest shape
- The `website/data/analytics.toml` dashboard manifest shape
- The `pipeline_steps` receipt schema
- The browser data-app runtime
- The exact public database naming and promotion path for generated site data
Zorto core still builds static sites. It does not yet provide `[data]` config, `zorto data`, automatic pipeline hooks, or a general dashboard scaffold.
## Why this fits static sites
Static hosting can serve database files the same way it serves images or JavaScript. The server still does not run application code or query a server-side database. The visitor's browser fetches public data files and executes local queries.
That keeps deployment simple while opening a path to richer docs, dashboards, search, catalogs, and local data apps. Future dynamic modes can keep the same content/config/code boundary with remote DuckDB, Quack, HTMX, or plain JavaScript.
## Further reading
- [Search](/docs/concepts/search/): DuckDB-backed client-side search
- [AI-native](/docs/concepts/ai-native/) — content, config, and code boundaries for agent-friendly sites
- [Executable code blocks](/docs/concepts/executable-code/) — build-time code execution
## Add search
Zorto search is a DuckDB data app. Generate a public `.ddb` file with a `search_pages` table, ship it as a static asset, and point the theme at it.
## Configure the theme
Add the database location to `config.toml`:
```toml
[extra]
search_database_url = "/data/site.ddb"
search_database_file = "site.ddb"
search_database_schema = "site"
```
When `search_database_url` is set, the built-in theme adds the search button and modal. The browser imports DuckDB-Wasm only when someone opens search.
## Create `search_pages`
Your `.ddb` should include:
| Column | Type |
|--------|------|
| `title` | text |
| `url` | text |
| `description` | text |
| `content` | text |
| `title_lower` | text |
| `description_lower` | text |
| `content_lower` | text |
zorto.dev builds this with `website/bin/build-meta`, a self-contained `uv` script that writes `website/static/data/site.ddb`.
## Inspect the data
Use DuckDB directly:
```bash
duckdb static/data/site.ddb "SELECT title, url FROM search_pages LIMIT 5;"
```
## Related guides
- [Search concepts](/docs/concepts/search/): how the browser search query works
- [Data apps](/docs/concepts/data-apps/): dashboards and `.ddb` files
## Themes
Zorto's theme system provides a complete visual starting point — templates, styles, and light/dark mode — that you can use as-is or progressively override.
## Architecture
A theme is a bundle of Tera templates and SCSS stylesheets embedded directly in the Zorto binary. Set `theme = "zorto"` in your config and the build pipeline uses that theme's templates and styles as defaults.
Zorto ships 16 built-in themes: `zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, and `charcoal`.
## The override model
Themes follow a layered precedence model:
1. **Theme defaults** — the templates and styles bundled with the theme
2. **Local overrides** — any file you place in `templates/` or `sass/` in your project
FallbackTheme defaults — templates and styles bundled with the themedefault
PriorityYour project — files in templates/ and sass/wins
Local files always take priority. Override one template or all of them.
If Zorto finds `templates/page.html` in your project, it uses that instead of the theme's `page.html`. The same applies to styles: a local `sass/style.scss` replaces the theme's `style.scss` entirely. Only files with the same name get replaced.
Override a single template while keeping everything else from the theme, or replace all templates entirely. You can also extend a theme's base layout and only replace specific blocks.
## Light and dark mode
Every built-in theme includes a toggle in the navbar. The user's preference is saved to `localStorage` and respected on return visits. The default follows the operating system preference via `prefers-color-scheme`. No configuration needed.
## When to use which theme
- **`default`** or **`slate`** — clean, minimal starting points. Good for documentation sites or simple blogs where you want to add your own personality through style overrides.
- **`zorto`** or **`dkdc`** — opinionated designs with color schemes and animations. Good for project landing pages or branded sites.
- **`ember`**, **`forest`**, **`ocean`**, **`rose`** — color-themed variants. Each provides a distinct palette while sharing the same template structure.
If none of the built-in themes fit, start with `default` and override the styles. The template structure is the same across all themes.
## Further reading
- [Templates](/docs/concepts/templates/) — how Tera templating and block inheritance work
- [How to customize your theme](/docs/how-to/customize-theme/) — step-by-step overrides for templates and styles
## CLI reference
The `zorto` command-line interface. See [installation](/docs/getting-started/installation/) for setup, [live reload](/docs/concepts/live-reload/) for preview server details, and [troubleshooting](/docs/how-to/troubleshooting/) for common issues.
## zorto
```{bash}
zorto --help
```
## zorto build
```{bash}
zorto build --help
```
## zorto preview
```{bash}
zorto preview --help
```
## zorto init
```{bash}
zorto init --help
```
## zorto check
```{bash}
zorto check --help
```
## zorto clean
```{bash}
zorto clean --help
```
## Rust API
Zorto's Rust API is documented on [docs.rs](https://docs.rs/zorto-core).
## Crates
| Crate | docs.rs | crates.io | Description |
|-------|---------|-----------|-------------|
| `zorto-core` | [docs](https://docs.rs/zorto-core) | [crate](https://crates.io/crates/zorto-core) | Core library: site model, build pipeline, rendering |
| `zorto` | [docs](https://docs.rs/zorto) | [crate](https://crates.io/crates/zorto) | CLI binary + preview server |
## Quick start
```toml
# Cargo.toml
[dependencies]
zorto-core = "0.14"
```
```rust
use std::path::Path;
use zorto_core::site::Site;
fn main() -> anyhow::Result<()> {
let root = Path::new("my-site");
let output = root.join("public");
let mut site = Site::load(root, &output, false /* drafts */)?;
site.build()?;
Ok(())
}
```
## Key types
| Module | Type | Description |
|--------|------|-------------|
| `zorto_core::site` | `Site` | Loaded site with config, sections, pages |
| `zorto_core::config` | `Config` | Parsed `config.toml` |
| `zorto_core::content` | `Page` | Content page with frontmatter and rendered HTML |
| `zorto_core::content` | `Section` | Section with child pages |
| `zorto_core::themes` | `Theme` | Built-in theme enum |
For full API documentation, type signatures, and examples, see [docs.rs/zorto-core](https://docs.rs/zorto-core).
## Create charts with altair
Use [Altair](https://altair-viz.github.io/) in executable code blocks to create declarative statistical visualizations that render as interactive HTML.
## Setup
Add `altair` to your site's `pyproject.toml`:
```toml
[project]
dependencies = ["altair"]
```
Then run `uv sync` in your site directory.
## Basic chart
````markdown
```{python}
import altair as alt
data = alt.Data(values=[
{'x': i, 'y': i ** 2} for i in range(20)
])
chart = alt.Chart(data).mark_line(strokeWidth=3).encode(
x='x:Q',
y='y:Q',
).properties(title='Quadratic growth', width=600, height=300)
```
````
Here it is rendered:
```{python}
import altair as alt
data = alt.Data(values=[
{'x': i, 'y': i ** 2} for i in range(20)
])
chart = alt.Chart(data).mark_line(
strokeWidth=3, color='#7c3aed'
).encode(
x='x:Q',
y='y:Q',
).properties(title='Quadratic growth', width=600, height=300)
```
## Layered chart
Combine multiple marks by layering charts with `+`:
```{python}
import altair as alt
import math
data = alt.Data(values=[
{'x': i * 0.1, 'sin': math.sin(i * 0.1), 'cos': math.cos(i * 0.1)}
for i in range(100)
])
sin_line = alt.Chart(data).mark_line(strokeWidth=2, color='#7c3aed').encode(
x='x:Q', y='sin:Q',
)
cos_line = alt.Chart(data).mark_line(strokeWidth=2, color='#06b6d4').encode(
x='x:Q', y='cos:Q',
)
chart = (sin_line + cos_line).properties(
title='Trigonometric functions', width=600, height=300,
)
```
## Interactive selection
Altair supports interactive selections — click and drag to highlight:
```{python}
import altair as alt
import random
random.seed(42)
data = alt.Data(values=[
{'x': random.gauss(0, 1), 'y': random.gauss(0, 1), 'group': 'A' if random.random() > 0.5 else 'B'}
for _ in range(200)
])
selection = alt.selection_point(fields=['group'])
chart = alt.Chart(data).mark_circle(size=60).encode(
x='x:Q',
y='y:Q',
color=alt.condition(selection, 'group:N', alt.value('lightgray'),
scale=alt.Scale(range=['#7c3aed', '#06b6d4'])),
opacity=alt.condition(selection, alt.value(0.8), alt.value(0.2)),
).add_params(selection).properties(
title='Click a legend item to filter', width=600, height=300,
)
```
## How it works
Zorto detects `altair.Chart` instances (and `LayerChart`, `HChart`, `VChart`, `ConcatChart`) in your code's local variables after execution. Each chart is converted to standalone HTML using Vega-Embed and embedded directly in the page.
The output is interactive — readers can hover for tooltips and interact with selections.
## Related guides
- [Use executable code blocks](/docs/how-to/executable-code-blocks/) — setup and general usage
- [Create interactive charts with plotly](/docs/how-to/plotly/) — another interactive charting library
## Python API
Zorto's Python API lets you load, inspect, and build sites programmatically. Install the package:
```bash
uv add zorto
```
The Python package includes the same Rust engine compiled as a native extension — there is no performance difference from the CLI.
## Quick start
```python
import zorto
# Load a site — returns a Site object
site = zorto.load(root=".") # Site
# Access site data
print(site.config.title) # Config
for page in site.pages: # list[Page]
print(page.title, page.permalink)
# Build the site
zorto.build(root=".")
```
## CLI from Python
You can also invoke the full CLI from Python:
```python
import zorto
zorto.run_cli(["build"]) # same as `zorto build`
zorto.run_cli(["preview", "--open"]) # same as `zorto preview --open`
```
## API surface
The Python package exposes a small wrapper around the Rust engine:
| Name | Kind | Purpose |
| --- | --- | --- |
| `build(root=".")` | Function | Build a site from Python. |
| `load(root=".")` | Function | Load a site and inspect its config, sections, and pages. |
| `run_cli(argv=None)` | Function | Run the Zorto CLI from Python. |
| `version()` | Function | Return the installed Zorto version. |
| `Config` | Class | Site configuration. |
| `Site` | Class | Loaded site model. |
| `Page` | Class | Renderable content page. |
| `Section` | Class | Content section with child pages. |
## Templates
Zorto uses the [Tera](https://keats.github.io/tera/) template engine, which has Jinja2-like syntax.
## Template hierarchy
Zorto looks for these templates (themes provide defaults for all of them):
| Template | Used for |
|----------|----------|
| `base.html` | Base layout all others extend |
| `index.html` | Site homepage (`content/_index.md`) |
| `section.html` | Section pages (`content/*/_index.md`) |
| `page.html` | Individual pages |
| `404.html` | Not-found page |
| `{taxonomy}/list.html` | Taxonomy index (e.g., `tags/list.html` for `/tags/`) |
| `{taxonomy}/single.html` | Single taxonomy term (e.g., `tags/single.html` for `/tags/rust/`) |
## Template context
Each template receives context variables:
| Variable | Available in | Description |
|----------|-------------|-------------|
| `page` | `page.html` | Current page object (title, content, date, permalink, extra, etc.) |
| `section` | `section.html`, `index.html` | Current section object (title, pages, subsections, etc.) |
| `config` | All | Site configuration (`config.title`, `config.extra`, etc.) |
| `paginator` | Paginated sections | Pagination info (pages, current_index, number_pagers) |
Use `page.permalink` or `section.permalink` to get the current page's full URL.
## Custom functions
| Function | Description |
|----------|-------------|
| `get_url(path)` | Get the permalink for a path |
| `get_section(path)` | Load a section and its pages |
| `get_taxonomy_url(kind, name)` | URL for a taxonomy term |
| `now()` | Current timestamp |
Example:
{% set posts = get_section(path="posts/_index.md") %}
{% for page in posts.pages %}
<a href="{{ page.permalink }}">{{ page.title }}</a>
{% endfor %}
## Worked example
Here's a complete `page.html` showing how template variables, filters, and blocks work together:
{{ page.content | safe }}
{% endblock %}
```
## Discovering theme templates
To see what templates and blocks a theme provides, check the theme source in the Zorto repository under `crates/zorto-core/themes//templates/`. Each theme defines the same set of templates with different styling.
## Macros
Define reusable template fragments:
## Further reading
- [Themes](/docs/concepts/themes/) — how the theme system provides and layers templates
- [Content model](/docs/concepts/content-model/) — how sections and pages map to templates
- [How to customize your theme](/docs/how-to/customize-theme/) — step-by-step template overrides
- [Tera documentation](https://keats.github.io/tera/docs/) — full syntax reference
## AI-native
Zorto is a static site generator built for workflows where humans and AI agents collaborate on websites.
## Design principles
- **Explicit contracts.** One `config.toml` and markdown files with TOML frontmatter. No implicit file conventions or magic directories.
- **Markdown-first content.** AI models understand markdown natively. No proprietary content formats.
- **Predictable output.** Same input produces the same output. No server-side runtime state, no server-side database, no visitor-side writes.
- **Strings in config, not templates.** Text content belongs in `config.toml` or frontmatter, not hard-coded in HTML.
- **Executable code blocks.** Dynamic output at build time, baked into static HTML.
## Build-time validation
Zorto validates your site during the build:
WriteContent and config
→
BuildCompile the site
→
CheckValidate structure and links
→
OutputHTML or clear errors
Current checks: internal link validation (broken `@/` references), template syntax, missing files, invalid configuration. Planned: mobile-friendliness, accessibility, semantic HTML validation.
## llms.txt
Zorto generates an [llms.txt](https://llmstxt.org/) file at build time by default — a machine-readable index of your entire site. An agent can read a single URL and understand the site's structure and content without crawling.
This creates two files:
- `/llms.txt` — links to markdown versions of every page
- `/llms-full.txt` — the full content of every page in a single file
This is for consumption, not editing. Agents that need to modify a site work directly on the filesystem.
## Markdown file generation
When enabled, Zorto generates a `.md` version of every page alongside the HTML, accessible at the same URL with a `.md` extension:
```toml
generate_md_files = true
```
## Further reading
- [Configuration](/docs/concepts/configuration/) — the `config.toml` contract
- [Executable code blocks](/docs/concepts/executable-code/) — dynamic output at build time
- [Content model](/docs/concepts/content-model/) — sections, pages, and frontmatter
- [How to optimize for SEO](/docs/how-to/seo/) — llms.txt, Open Graph, and discoverability
## Add a blog
Set up a blog with posts, tags, pagination, and an Atom feed.
📂content/posts/
├── 📝_index.mdsection: blog config
├── 📝my-first-post.mdpage
├── 📝another-post.mdpage
What your blog structure will look like after this guide.
## Create the section
Create `content/posts/_index.md` with this [frontmatter](/docs/concepts/glossary/#frontmatter):
```toml
+++
title = "Blog"
sort_by = "date"
paginate_by = 10
+++
```
This creates a paginated section at `/posts/` that sorts posts by date (newest first).
## Write a post
Create `content/posts/my-first-post.md`:
```markdown
+++
title = "My first post"
date = "2026-01-15"
description = "A short introduction."
tags = ["intro"]
+++
A short summary of the post goes here.
The full content continues after the "more" marker. Everything above it becomes the summary shown on listing pages.
```
## Enable tags
Add a [taxonomy](/docs/concepts/glossary/#taxonomy) to `config.toml`:
```toml
[[taxonomies]]
name = "tags"
```
Zorto automatically generates:
- `/tags/` — list of all tags
- `/tags/intro/` — all posts with the "intro" tag
## Add an Atom feed
Enable [feed generation](/docs/concepts/glossary/#atom-feed) in `config.toml`:
```toml
# config.toml
generate_feed = true
```
The feed is available at `/atom.xml`. Add a `` tag in your base template's `` so feed readers can discover it automatically:
```html
```
## Drafts
Set `draft = true` in a post's frontmatter to exclude it from production builds. To preview drafts locally, pass the `--drafts` flag:
```bash
zorto preview --drafts
```
## Related guides
- [Blog, events, and more](/docs/concepts/blog/) — how sections, pagination, and feeds work under the hood
- [Set up multiple authors](/docs/how-to/multiple-authors/) — attribute posts to different authors
- [Organize content](/docs/how-to/organize-content/) — nested sections and external content directories
## Build a blog
Create a blog from scratch with posts, tags, pagination, custom themes, and deployment.
📂my-blog/
├── 📝config.tomlsite config
├── 📂content/
├── 📝_index.mdsection: homepage
├── 📂posts/
├── 📝_index.mdsection: paginated post listing
├── 📝hello-world.mdpage
├── 📝getting-started.mdpage
├── 📂static/images, favicon
What your blog will look like after this guide.
## Initialize the project
Scaffold a new blog with the `blog` template:
```bash
zorto init --template blog my-blog
cd my-blog
```
This creates a site with an Atom feed, code highlighting, and two example posts. Start the dev server:
```bash
zorto preview --open
```
Your blog is live at `http://localhost:1111`.
## Understand the structure
The generated `config.toml` looks like this:
```toml
base_url = "http://localhost:1111"
title = "My Blog"
theme = "default"
generate_feed = true
[markdown]
highlight_code = true
[extra]
copyright_html = 'My Blog by Author via Zorto'
```
The homepage (`content/_index.md`) is a section that sorts by date and paginates:
```toml
+++
title = "Home"
sort_by = "date"
paginate_by = 10
+++
```
The posts section (`content/posts/_index.md`) lists all blog posts:
```toml
+++
title = "Posts"
sort_by = "date"
+++
```
## Write a new post
Create `content/posts/my-new-post.md`:
```markdown
+++
title = "My new post"
date = "2026-04-04"
description = "A short description for SEO and feeds."
tags = ["tutorial"]
+++
A short summary of the post appears here.
The full content continues after the "more" marker. Everything above it becomes the summary shown on listing pages and in the Atom feed.
## A heading in the post
Regular markdown works: **bold**, *italic*, `code`, [links](https://example.com), and images.
```
The `date` field determines sort order (newest first) and inclusion in the Atom feed. The `` marker splits the summary from the full content.
## Enable tags
Add a taxonomy to `config.toml`:
```toml
[[taxonomies]]
name = "tags"
```
Then assign tags in each post's frontmatter:
```toml
+++
title = "My new post"
date = "2026-04-04"
tags = ["tutorial", "rust"]
+++
```
Zorto automatically generates:
- `/tags/` — list of all tags
- `/tags/tutorial/` — all posts tagged "tutorial"
- `/tags/rust/` — all posts tagged "rust"
### Add categories too
You can define multiple taxonomies:
```toml
[[taxonomies]]
name = "tags"
[[taxonomies]]
name = "categories"
```
Then use both in frontmatter:
```toml
+++
title = "My new post"
date = "2026-04-04"
tags = ["tutorial"]
categories = ["tech"]
+++
```
## Configure pagination
Control how many posts appear per page in the section's `_index.md`:
```toml
+++
title = "Posts"
sort_by = "date"
paginate_by = 5
+++
```
Zorto generates paginated pages: `/posts/`, `/posts/page/2/`, `/posts/page/3/`, and so on.
## Choose a theme
Change the theme in `config.toml`:
```toml
theme = "ocean"
```
Available themes: `zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, `charcoal`. All support light and dark mode.
Preview different themes by changing the value and checking the dev server — live reload picks up config changes.
## Customize colors
Override CSS variables without changing the theme. Create `sass/custom.scss`:
```scss
:root {
--accent: #e74c3c;
--background: #1a1a2e;
--max-width: 800px;
}
```
Then load it via the `extra_head` block. Create `templates/base.html`:
```html
{% extends "base.html" %}
{% block extra_head %}
{% endblock %}
```
Zorto compiles SCSS to CSS at build time — `sass/custom.scss` becomes `/custom.css` in the output.
## Add an about page
Create `content/about.md`:
```markdown
+++
title = "About"
+++
This is my blog about Rust, static sites, and building things.
```
This creates a standalone page at `/about/` — it is not part of any section.
## Set up multiple authors
If your blog has multiple authors, add an `authors` taxonomy:
```toml
# config.toml
[[taxonomies]]
name = "authors"
```
Then use it in each post:
```toml
+++
title = "Guest post"
date = "2026-04-04"
authors = ["Alice"]
+++
```
Zorto generates `/authors/` and `/authors/alice/` automatically.
## Use drafts
Mark a post as a draft to exclude it from production builds:
```toml
+++
title = "Work in progress"
date = "2026-04-04"
draft = true
+++
```
Preview drafts locally:
```bash
zorto preview --drafts
```
Drafts are excluded from `zorto build` output and the Atom feed.
## Add co-located images
For posts with images, use a directory instead of a single file:
```bash
mkdir -p content/posts/photo-gallery
```
Create `content/posts/photo-gallery/index.md`:
```markdown
+++
title = "Photo gallery"
date = "2026-04-04"
+++

```
Place `sunset.jpg` alongside `index.md`. Zorto copies the image to the output directory, preserving the relative path.
## Set the base URL for production
Before deploying, update `base_url` in `config.toml`:
```toml
base_url = "https://myblog.com"
```
This affects permalinks, the Atom feed URL, sitemaps, and Open Graph tags. The dev server overrides it to `localhost` automatically.
## Deploy
Build the site:
```bash
zorto build
```
The output goes to `public/`. Deploy it to any static host. For Netlify, create `netlify.toml`:
```toml
[build]
command = "curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build"
publish = "public"
```
See [Deploy your site](/docs/how-to/deploy/) for GitHub Pages, Vercel, and Cloudflare Pages instructions.
## Related guides
- [Add a blog](/docs/how-to/add-blog/) — quick reference for blog sections, tags, feeds, and drafts
- [Blog, events, and more](/docs/concepts/blog/) — how sections and pagination work under the hood
- [Customize your theme](/docs/how-to/customize-theme/) — override templates, styles, and shortcodes
- [Customize styles](/docs/how-to/custom-css/) — CSS variables, light/dark mode, fonts
- [Set up multiple authors](/docs/how-to/multiple-authors/) — taxonomies for author pages
## Organize content with sections
Structure your site for different content types, audiences, or topics using Zorto's [section](/docs/concepts/glossary/#section) system.
## Multiple content sections
A typical site might have:
📂content/
├── 📝_index.mdsection: homepage
├── 📂posts/
├── 📝_index.mdsection: blog
├── 📂docs/
├── 📝_index.mdsection: documentation
├── 📂projects/
├── 📝_index.mdsection: portfolio
├── 📝about.mdpage
Each section is independent — its own sort order, pagination, and template.
## Section-specific templates
Assign templates per section:
```toml
# content/docs/_index.md
+++
title = "Documentation"
template = "docs-section.html"
+++
```
Pages within the section can also use a custom template:
```toml
# content/docs/getting-started.md
+++
title = "Getting started"
template = "docs-page.html"
+++
```
## Nested sections
Sections can nest:
📂content/docs/
├── 📝_index.mdsection
├── 📂getting-started/
├── 📝_index.mdsection
├── 📝installation.mdpage
├── 📂reference/
├── 📝_index.mdsection
├── 📝cli.mdpage
Access subsections in templates via `section.subsections`.
## External content directories
Pull content from outside the `content/` directory:
```toml
# config.toml
[[content_dirs]]
path = "../docs"
url_prefix = "docs"
template = "docs.html"
section_template = "docs-section.html"
sort_by = "title"
rewrite_links = true
```
This is useful for documentation that lives alongside source code in a separate directory or repository.
## Related guides
- [Content model](/docs/concepts/content-model/) — sections, pages, and frontmatter in depth
- [Configuration reference](/docs/reference/config/) — full `content_dirs` field reference
## Deploy your site
Zorto builds [static files](/docs/concepts/glossary/#static-site) — HTML, CSS, JS — that can be hosted on any [static hosting](/docs/concepts/glossary/#static-hosting) provider. No server-side runtime required.
Buildzorto build
→
UploadPush public/ to host
→
LiveSite served globally
## Build for production
```bash
zorto build
```
This generates your site in `public/`. Upload that directory to any static hosting provider.
## Netlify
Create a `netlify.toml` in your project root:
```toml
[build]
command = "curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build"
publish = "public"
```
Push to GitHub and connect the repo in Netlify's dashboard. Every push triggers a build.
## GitHub Pages
Add a workflow at `.github/workflows/deploy.yml`:
```yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
- run: curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build
- uses: actions/upload-pages-artifact@v3
with:
path: public
- id: deployment
uses: actions/deploy-pages@v4
```
## Vercel
Create a `vercel.json`:
```json
{
"buildCommand": "curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build",
"outputDirectory": "public"
}
```
## Cloudflare Pages
Connect your Git repository in the Cloudflare Pages dashboard and configure:
- **Build command**: `curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build`
- **Build output directory**: `public`
Every push to your production branch triggers a build. Cloudflare Pages also creates preview deployments for pull requests automatically.
## Custom headers
Create a `static/_headers` file (Netlify) or configure headers in your platform's config. Zorto copies everything in `static/` to `public/` at build time.
## Next steps
- [Set up a custom domain](/docs/how-to/custom-domain/) — DNS records for each platform
- [Optimize for SEO](/docs/how-to/seo/) — meta tags, Open Graph, canonical URLs
- [Add a sitemap](/docs/how-to/add-sitemap/) — submit your site to search engines
## Content model
Zorto organizes content into sections and pages, derives URLs from the file structure, supports internal linking with build-time validation, and lets you co-locate assets alongside your markdown.
## Sections vs pages
Zorto content has two types:
- **Sections** are directories with an `_index.md` file. They can list their child pages and subsections.
- **Pages** are individual `.md` files. They render as standalone pages.
Section
A directory with _index.md. Lists child pages, supports pagination and sorting.
Page
An individual .md file. Renders as a standalone URL at its file path.
A file at `content/posts/_index.md` creates a section at `/posts/`. A file at `content/about.md` creates a page at `/about/`.
📂content/
├── 📝_index.mdsection → /
├── 📝about.mdpage → /about/
├── 📂posts/
├── 📝_index.mdsection → /posts/
├── 📝first-post.mdpage → /posts/first-post/
├── 📝second-post.mdpage → /posts/second-post/
Directories are sections, files are pages. The _index.md file turns a directory into a section.
## Frontmatter
Every content file starts with TOML frontmatter between `+++` delimiters:
```toml
+++
title = "My page"
date = "2026-01-15"
author = "Cody"
description = "A short summary for SEO and feeds."
draft = true
slug = "custom-url"
template = "custom-page.html"
tags = ["rust", "ssg"]
[extra]
custom_field = "any value you want"
+++
```
| Field | Type | Description |
|-------|------|-------------|
| `title` | string | Page title (required) |
| `date` | string | Publication date (YYYY-MM-DD) |
| `author` | string | Author name |
| `description` | string | Summary for SEO and feeds |
| `draft` | bool | If true, excluded from builds (default: false) |
| `slug` | string | Override the URL slug |
| `template` | string | Use a custom template |
| `aliases` | array of strings | Redirect old URLs to this page |
| `sort_by` | string | Sort child pages: `"date"` (newest first) or `"title"` (sections only) |
| `paginate_by` | int | Number of items per page, 0 = no pagination (sections only) |
| taxonomy fields | array of strings | Taxonomy values as top-level arrays (e.g. `tags = ["rust", "ssg"]`) |
| `[extra]` | table | Arbitrary custom data, accessible in templates |
## Permalinks
Every page and section gets a **permalink** — an absolute URL combining `base_url` from your config with the page's path. The path is derived from the file's location in the content directory:
| File path | URL path | Permalink (with `base_url = "https://example.com"`) |
|-----------|----------|------------------------------------------------------|
| `content/about.md` | `/about/` | `https://example.com/about/` |
| `content/posts/hello.md` | `/posts/hello/` | `https://example.com/posts/hello/` |
| `content/posts/_index.md` | `/posts/` | `https://example.com/posts/` |
| `content/posts/my-post/index.md` | `/posts/my-post/` | `https://example.com/posts/my-post/` |
Permalinks are available in templates as `page.permalink` and `section.permalink`. They are used for canonical URLs, Open Graph tags, feeds, and sitemaps.
## Slugs
The **slug** is the URL-safe name for a page, derived from the filename by default. Zorto uses the `slug` crate to convert filenames to lowercase, ASCII-only strings with hyphens:
| Source | Slug |
|--------|------|
| `My First Post.md` | `my-first-post` |
| `Héllo Wörld.md` | `hello-world` |
| `posts/my-post/index.md` | `my-post` (from the directory name) |
Override the slug in frontmatter to decouple the URL from the filename:
```toml
+++
title = "A very long title that you do not want in the URL"
slug = "short-url"
+++
```
This page renders at `/short-url/` regardless of its filename. Co-located pages (`index.md` inside a directory) derive their slug from the directory name, not the filename — the custom `slug` field overrides that too.
## Internal links with `@/`
Link to other content files using the `@/` prefix:
```text
[About](/docs/concepts/@/about/)
[First post](/docs/concepts/@/posts/first-post/)
[Blog section](/docs/concepts/@/posts/_index/)
```
Zorto resolves these to the correct URLs at build time. The path after `@/` is relative to the `content/` directory.
Anchor links work too:
```text
[Installation section](/docs/concepts/@/getting-started/#installation)
```
If the target file does not exist, Zorto emits a warning during the build:
```
unresolved internal link: posts/missing.md (no matching page or section found)
```
This gives you broken-link detection without an external tool. Use `zorto check` to validate all internal links without building the full site.
## Summaries
Use `` in a page's body to mark where the summary ends:
```markdown
This is the summary shown on listing pages.
The full content continues here.
```
Everything above the marker becomes the page's `summary`, used in section listings and feeds. The summary is rendered as HTML — markdown formatting, links, and inline code all work.
If no `` marker is present, the `summary` field is `None` in templates. Use the `description` frontmatter field as a fallback for feed entries and meta tags.
In templates, use the summary like this:
## Co-located assets
Place images and other assets next to your markdown files:
📂content/posts/my-post/
├── 📝index.mdpage
├── 📝photo.jpg
├── 📝diagram.svg
Content and assets live together — no separate static/images/ directory needed.
Reference them with relative paths in your markdown:
```markdown

```
During the build, Zorto copies co-located assets to the page's output directory, preserving the relative path relationship. The output looks like:
📂public/posts/my-post/
├── 📝index.html
├── 📝photo.jpg
├── 📝diagram.svg
Assets are copied alongside the rendered HTML.
Any non-markdown file inside a content directory is treated as a co-located asset. This includes images (`.jpg`, `.png`, `.svg`, `.gif`, `.webp`), PDFs, data files, and anything else.
For site-wide assets that are not tied to a specific page (favicons, global images, fonts), use the `static/` directory instead. See [Asset management](/docs/how-to/assets/).
## Further reading
- [Configuration reference](/docs/reference/config/) — complete frontmatter field list
- [Blog, events, and more](/docs/concepts/blog/) — using sections for date-ordered content
- [Templates](/docs/concepts/templates/) — how sections and pages map to templates
- [Organize content](/docs/how-to/organize-content/) — nested sections and external content directories
- [Asset management](/docs/how-to/assets/) — static files, co-located content, fonts, and images
## Set up a custom domain
Connect your own [domain](/docs/concepts/glossary/#domain-name) to a Zorto site hosted on any [static hosting](/docs/concepts/glossary/#static-hosting) provider.
UpdateSet base_url in config
→
DNSAdd records at your registrar
→
WaitDNS propagates (minutes to an hour)
→
LiveHTTPS works automatically
The entire process takes minutes of work, then waiting for DNS propagation.
## Update base_url
Set your production domain in `config.toml`:
```toml
base_url = "https://yourdomain.com"
```
This ensures all generated URLs (sitemap, feeds, canonical links) point to the correct domain.
## Configure [DNS](/docs/concepts/glossary/#dns)
Add [DNS records](/docs/concepts/glossary/#cname-record) for your domain. The exact records depend on your hosting provider.
**Netlify:**
Add a CNAME record pointing your domain to your Netlify site:
| Type | Name | Value |
|-------|------|------------------------------|
| CNAME | www | your-site.netlify.app |
| A | @ | 75.2.60.5 |
Then add your domain under Site settings > Domain management in Netlify.
**GitHub Pages:**
Add these records and configure the domain in your repository's Pages settings:
| Type | Name | Value |
|-------|------|------------------------------|
| CNAME | www | username.github.io |
| A | @ | 185.199.108.153 |
| A | @ | 185.199.109.153 |
| A | @ | 185.199.110.153 |
| A | @ | 185.199.111.153 |
Create a `static/CNAME` file containing your domain (e.g. `yourdomain.com`). Zorto copies it to `public/` at build time.
**Cloudflare Pages:**
Add a CNAME record and configure the custom domain in the Cloudflare Pages dashboard:
| Type | Name | Value |
|-------|------|------------------------------|
| CNAME | www | your-site.pages.dev |
| CNAME | @ | your-site.pages.dev |
**Vercel:**
Add a CNAME record and configure the domain in Vercel's project settings:
| Type | Name | Value |
|-------|------|------------------------------|
| CNAME | www | cname.vercel-dns.com |
| A | @ | 76.76.21.21 |
## [HTTPS](/docs/concepts/glossary/#https--ssl)
All major static hosting providers provision [TLS certificates](/docs/concepts/glossary/#https--ssl) automatically for custom domains. No additional configuration is needed — HTTPS works once DNS propagation completes, typically within a few minutes to an hour.
## Related guides
- [Deploy your site](/docs/how-to/deploy/) — build commands and hosting setup for each platform
- [Optimize for SEO](/docs/how-to/seo/) — ensure `base_url` is correct for canonical URLs
## Customize navigation and footer
The built-in themes render navigation and footer from `config.toml` values. No template overrides needed for common cases.
## Navigation menu
Define `menu_items` under `[extra]` to populate the left-side nav links:
```toml
[extra]
menu_items = [
{ name = "Docs", url = "/docs/" },
{ name = "Blog", url = "/blog/" },
{ name = "GitHub", url = "https://github.com/you/repo", external = true },
]
```
Setting `external = true` opens the link in a new tab and adds an external-link icon.
## Right-side menu items
Use `menu_items_right` for call-to-action buttons on the right side of the navbar:
```toml
[extra]
menu_items_right = [
{ name = "Get started", url = "/docs/getting-started/" },
]
```
## Logo
```toml
[extra]
logo_text = "My Site" # text next to the logo (defaults to config.title)
logo_tld = ".dev" # optional TLD suffix rendered in accent color
logo_image = "/logo.svg" # optional image (place in static/)
```
## Social links
```toml
[extra]
social_links = [
{ name = "GitHub", url = "https://github.com/you", icon = "github" },
]
```
Built-in icons: `github`, `linkedin`. To add other icons, override `base.html` and add SVG icons for your additional social links in the navbar section.
## Footer
The footer renders `config.extra.copyright_html` if set, otherwise falls back to `config.extra.author` with the current year:
```toml
[extra]
copyright_html = "Built with Zorto"
# or simply:
author = "Your Name"
```
## [Template blocks](/docs/concepts/glossary/#template-block) to override
If you need deeper customization, override these blocks from `base.html` in your own templates (see [how to customize your theme](/docs/how-to/customize-theme/)):
| Block | Purpose |
|-------|---------|
| `title` | Page `` |
| `extra_head` | Inject CSS/JS into `` |
| `content` | Main page content |
| `open_graph` | Open Graph meta tags |
| `extra_body` | Inject scripts before `` |
See [Customize your theme](/docs/how-to/customize-theme/) for examples of overriding these blocks.
## Manage assets
Serve images, fonts, and other static files alongside your Zorto site.
Zorto provides two mechanisms for assets: the `static/` directory for site-wide files, and co-located assets for page-specific files.
## Static files
Place site-wide assets in the `static/` directory at your project root. Everything inside is copied verbatim to the output directory during build:
📂my-site/
├── 📂static/
├── 📝favicon.ico→ /favicon.ico
├── 📂images/
├── 📝logo.png→ /images/logo.png
├── 📂fonts/
├── 📝custom.woff2→ /fonts/custom.woff2
├── 📝robots.txt→ /robots.txt
Files in static/ are copied to the root of your output directory.
Reference them with absolute paths in templates or markdown:
```html
```
```markdown

```
The directory structure is preserved. Nested directories work as expected.
## Co-located assets
Place images and files next to a page's markdown by using a directory with an `index.md` file:
📂content/posts/my-post/
├── 📝index.mdpage → /posts/my-post/
├── 📝photo.jpg→ /posts/my-post/photo.jpg
├── 📝diagram.svg→ /posts/my-post/diagram.svg
├── 📝data.json→ /posts/my-post/data.json
Assets live alongside the page that uses them.
Reference co-located assets with relative paths in your markdown:
```markdown


```
Any non-markdown file inside a content directory is treated as a co-located asset. During the build, Zorto copies these files to the page's output directory, preserving the relative path relationship.
### When to use each approach
| Approach | Use for | Reference with |
|----------|---------|----------------|
| `static/` | Favicons, global images, fonts, `robots.txt`, `_headers` | Absolute paths (`/zorto-mark-transparent.png`) |
| Co-located | Page-specific images, diagrams, downloads | Relative paths (`photo.jpg`) |
## Images
For images in markdown, use standard markdown syntax:
```markdown


```
> [!TIP]
> Always include alt text for accessibility. Keep image file sizes small — Zorto does not optimize images at build time. Use tools like `imagemagick`, `sharp`, or `squoosh` before adding images to your project.
Supported formats include `.jpg`, `.png`, `.svg`, `.gif`, `.webp`, and any other format browsers can display.
## Fonts
Host fonts locally by placing them in `static/fonts/`:
📂static/fonts/
├── 📝inter-regular.woff2
├── 📝inter-bold.woff2
Load them in your stylesheet:
```scss
// sass/custom.scss
@font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: "Inter";
src: url("/fonts/inter-bold.woff2") format("woff2");
font-weight: 700;
font-display: swap;
}
body {
font-family: "Inter", sans-serif;
}
```
Use `font-display: swap` to prevent invisible text while fonts load.
## Caching and cache busting
Static files are served with whatever caching policy your hosting provider applies. Zorto does not add cache-busting hashes to filenames.
For aggressive caching strategies, use your hosting provider's headers configuration. For example, with Netlify:
```
# static/_headers
/fonts/*
Cache-Control: public, max-age=31536000, immutable
/images/*
Cache-Control: public, max-age=86400
```
SCSS-compiled CSS files are regenerated on every build, so browsers fetch the latest version when the content changes and the hosting provider's cache expires.
## Related guides
- [Content model](/docs/concepts/content-model/) — co-located assets and page structure in depth
- [Customize styles](/docs/how-to/custom-css/) — loading fonts and overriding styles
- [Deploy your site](/docs/how-to/deploy/) — hosting setup and cache headers
- [Optimize for SEO](/docs/how-to/seo/) — favicons, Open Graph images
## Fast
Zorto's build pipeline typically completes in under 100ms. The build runs on every save during development and on every push in CI, so build speed directly affects iteration speed.
## Benchmark
Build the zorto.dev site (40 pages, shortcodes, executable code blocks):
```bash
time zorto build
```
Typical result: under 1 second total. The build pipeline itself takes ~50ms; the rest is executable code block runtime.
## Architecture
- **Rust.** Compiled to native code. No garbage collector pauses.
- **Efficient pipeline.** Minimal allocations, streaming I/O, parallel page rendering.
- **Embedded themes.** Templates and styles compiled into the binary.
- **Self-contained.** No server-side runtime dependencies for published sites. Python is optional at build time for executable code blocks.
## In practice
Executable code blocks add time proportional to the code being run — a `{python}` block that queries an API takes as long as the API call. The build pipeline itself stays fast regardless of site size. Use `zorto --no-exec preview` during development to skip code execution while editing prose.
## Further reading
- [Live reload](/docs/concepts/live-reload/) — the dev server's rebuild-on-save loop
- [Executable code blocks](/docs/concepts/executable-code/) — `--no-exec` flag
- [How to deploy](/docs/how-to/deploy/) — `zorto build` in CI/CD pipelines
## Add a favicon
Place your favicon file in the `static/` directory at your project root. Zorto copies everything in `static/` to `public/` at build time, so `static/favicon.svg` becomes `/favicon.svg`.
## Configure in config.toml
```toml
[extra]
favicon = "/favicon.svg"
favicon_mimetype = "image/svg+xml"
```
The built-in themes read `config.extra.favicon` and `config.extra.favicon_mimetype` in the `` of `base.html`:
```html
```
If `config.extra.favicon` is not set, no `` tag is rendered.
## Supported formats
Use any format your target browsers support. Common choices:
| File | Mimetype |
|------|----------|
| `favicon.png` | `image/png` |
| `favicon.ico` | `image/x-icon` |
| `favicon.svg` | `image/svg+xml` |
SVG is the most flexible option — it scales to any size and supports dark mode via CSS media queries.
## Related guides
- [Customize your theme](/docs/how-to/customize-theme/) — override templates and styles
- [Optimize for SEO](/docs/how-to/seo/) — Open Graph images and other meta tags
## Set up multiple authors
Use [taxonomies](/docs/concepts/glossary/#taxonomy) and page frontmatter to attribute content to different authors.
## Add an authors taxonomy
Define an `authors` taxonomy in `config.toml`:
```toml
base_url = "https://example.com"
[[taxonomies]]
name = "tags"
[[taxonomies]]
name = "authors"
```
This generates an author listing page at `/authors/` and individual pages for each author (e.g. `/authors/jane-doe/`).
## Tag pages with authors
Use the `authors` taxonomy in your page frontmatter:
```markdown
+++
title = "Building a blog with Zorto"
date = "2025-03-15"
authors = ["Jane Doe"]
+++
Your content here.
```
Multiple authors on a single page:
```markdown
+++
title = "Collaborative guide"
date = "2025-03-20"
authors = ["Jane Doe", "Alex Kim"]
+++
```
## Add author metadata
Use the `[extra]` table for additional author information like bio or avatar:
```markdown
+++
title = "Why static sites win"
date = "2025-04-01"
authors = ["Jane Doe"]
[extra]
author_url = "https://janedoe.com"
author_image = "/images/jane.jpg"
+++
```
Access these in templates:
```html
{% if page.extra.author_url %}
{% endif %}
```
## Create author templates
Add `templates/authors/single.html` for individual author pages (e.g., `/authors/jane-doe/`):
```html
```
## Related guides
- [Add a blog](/docs/how-to/add-blog/) — posts, tags, pagination
- [Templates](/docs/concepts/templates/) — template context variables and custom functions
## Callouts reference
Zorto supports GitHub-style callout alerts. Use blockquote syntax with a type marker. See [callouts concept](/docs/concepts/callouts/) for when to use callouts vs shortcodes.
## Syntax
```markdown
> [!TYPE]
> Content goes here.
```
## All callout types
### Note
> [!NOTE]
> Highlights information that users should take into account, even when skimming.
```markdown
> [!NOTE]
> Highlights information that users should take into account, even when skimming.
```
### Tip
> [!TIP]
> Optional information to help a user be more successful.
```markdown
> [!TIP]
> Optional information to help a user be more successful.
```
### Important
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
```markdown
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
```
### Warning
> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.
```markdown
> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.
```
### Caution
> [!CAUTION]
> Negative potential consequences of an action.
```markdown
> [!CAUTION]
> Negative potential consequences of an action.
```
## Rich content
Callouts support full markdown inside:
> [!TIP]
> You can use:
>
> - **Bold** and *italic* text
> - `inline code` and code blocks
> - [Links](https://zorto.dev)
> - Lists and other block elements
## Create charts with matplotlib
Use [matplotlib](https://matplotlib.org/) in executable code blocks to generate static charts that render as inline PNGs.
## Setup
Add `matplotlib` to your site's `pyproject.toml`:
```toml
[project]
dependencies = ["matplotlib"]
```
Then run `uv sync` in your site directory.
## Basic line chart
````markdown
```{python}
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 7, 11, 16]
plt.figure(figsize=(8, 4))
plt.plot(x, y, color='#7c3aed', linewidth=2, marker='o')
plt.title('Growth over time')
plt.xlabel('Month')
plt.ylabel('Users (k)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
```
````
Here it is rendered:
```{python}
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 7, 11, 16]
plt.figure(figsize=(8, 4))
plt.plot(x, y, color='#7c3aed', linewidth=2, marker='o')
plt.title('Growth over time')
plt.xlabel('Month')
plt.ylabel('Users (k)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
```
## Bar chart
```{python}
import matplotlib.pyplot as plt
categories = ['Rust', 'Python', 'Go', 'TypeScript']
values = [95, 88, 72, 68]
plt.figure(figsize=(8, 4))
plt.bar(categories, values, color=['#7c3aed', '#06b6d4', '#10b981', '#f59e0b'])
plt.title('Language satisfaction (%)')
plt.ylabel('Satisfaction')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
```
## Subplots
Use `plt.subplots()` to create multiple plots in one figure:
```{python}
import matplotlib.pyplot as plt
import math
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
x = [i * 0.1 for i in range(100)]
ax1.plot(x, [math.sin(v) for v in x], color='#7c3aed', linewidth=2)
ax1.set_title('sin(x)')
ax1.grid(True, alpha=0.3)
ax2.plot(x, [math.cos(v) for v in x], color='#06b6d4', linewidth=2)
ax2.set_title('cos(x)')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
```
## How it works
Zorto detects matplotlib figures after your code executes by checking `plt.get_fignums()`. Each figure is saved as a PNG and embedded inline as a base64 data URI. No files are written to disk.
You don't need `plt.show()` — Zorto captures figures automatically. Call `plt.tight_layout()` for best results.
## Related guides
- [Use executable code blocks](/docs/how-to/executable-code-blocks/) — setup and general usage
- [Create charts with seaborn](/docs/how-to/seaborn/) — statistical plots built on matplotlib
## Configuration
Zorto is configured via `config.toml` in your project root.
1
Identity
Who is this site?
base_url, title
2
Build behavior
What outputs to produce?
feeds, sitemap
3
Content processing
How to parse and organize content?
markdown, taxonomies
4
Theme and custom data
How should the site look?
theme, extra
Four conceptual layers, one file. Everything from identity to appearance in config.toml.
## Minimal example
```toml
base_url = "https://example.com"
title = "My site"
```
## Full example
```toml
base_url = "https://example.com"
title = "My site"
description = "A site built with Zorto"
theme = "dkdc"
compile_sass = true
generate_feed = true
generate_sitemap = true
generate_llms_txt = true
generate_md_files = true
compile_all_themes = false
default_language = "en"
[[content_dirs]]
path = "../docs"
url_prefix = "docs"
sort_by = "title"
rewrite_links = true
[cache]
enable = true
[markdown]
highlight_code = true
insert_anchor_links = "right"
external_links_target_blank = true
external_links_no_follow = true
external_links_no_referrer = true
smart_punctuation = true
[[taxonomies]]
name = "tags"
[[taxonomies]]
name = "categories"
[extra]
author = "Your Name"
# Any custom data: accessible as config.extra in templates
```
## Key sections
### Top-level settings
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `base_url` | string | *required* | Full URL of your site |
| `title` | string | `""` | Site title |
| `description` | string | `""` | Site description |
| `theme` | string | `""` | Theme name (one of 16 built-in themes: `zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, `charcoal`) |
| `compile_sass` | bool | `true` | Compile SCSS to CSS |
| `generate_feed` | bool | `false` | Generate Atom feed |
| `generate_sitemap` | bool | `true` | Generate sitemap.xml |
| `generate_llms_txt` | bool | `true` | Generate llms.txt and llms-full.txt |
| `generate_md_files` | bool | `false` | Generate .md versions of every page alongside HTML |
| `compile_all_themes` | bool | `false` | Compile CSS for every built-in theme |
| `default_language` | string | `"en"` | Default language code |
### `[markdown]`
Controls how Markdown is rendered to HTML. The most commonly used options:
- `highlight_code` — syntax highlighting for fenced code blocks
- `insert_anchor_links` — add `#` links to headings (`"right"` or `"none"`)
- `external_links_target_blank` — open external links in a new tab
- `smart_punctuation` — convert `"quotes"` to "quotes" and `--` to —
See the [config reference](/docs/reference/config/) for all fields.
### `[[taxonomies]]`
Define taxonomies like tags and categories. Each entry creates listing pages automatically:
```toml
[[taxonomies]]
name = "tags"
```
This generates `/tags/` (all tags) and `/tags//` (pages with that tag). Add as many taxonomies as you need — tags, categories, authors, etc.
### `[[content_dirs]]`
Pull external directories into your site as content. Each entry maps an external path to a URL prefix:
```toml
[[content_dirs]]
path = "../docs"
url_prefix = "docs"
sort_by = "title"
rewrite_links = true
```
See [content directories reference](/docs/reference/content-dirs/) and [how to build a docs site](/docs/how-to/build-docs-site/) for details.
### `[cache]`
Cache executable code block results to speed up rebuilds:
```toml
[cache]
enable = true
```
When enabled, Zorto caches code block output and reuses it if the code has not changed. See [build optimization](/docs/how-to/build-optimization/) for details.
### `[extra]`
A free-form table for any custom data your templates need. Zorto passes it through as `config.extra` without interpreting it:
```toml
[extra]
author = "Your Name"
github = "https://github.com/you"
menu_items = [
{ name = "Docs", url = "/docs/" },
{ name = "Blog", url = "/posts/" },
]
search_database_url = "/data/site.ddb"
search_database_file = "site.ddb"
search_database_schema = "site"
```
Access in templates: `{{ config.extra.author }}`, `{% for item in config.extra.menu_items %}`.
The built-in Zorto theme also reads `search_database_url` to enable DuckDB-backed search.
## Further reading
- [Configuration reference](/docs/reference/config/) — complete field list with types and defaults
- [Themes](/docs/concepts/themes/) — how the `theme` setting works
- [Content model](/docs/concepts/content-model/) — how frontmatter relates to config
- [How to customize your theme](/docs/how-to/customize-theme/) — override styles and templates
- [How to deploy](/docs/how-to/deploy/) — build commands for each hosting provider
## Glossary
Definitions for web and SSG terms used throughout the Zorto docs. Skip to the section you need:
- [Web fundamentals](#web-fundamentals) — domains, DNS, HTTPS, hosting
- [Content and structure](#content-and-structure) — markdown, frontmatter, sections, taxonomies
- [Build and deploy](#build-and-deploy) — templates, SCSS, executable code, CI/CD
- [SEO and discovery](#seo-and-discovery) — sitemap, Open Graph, llms.txt
## Web fundamentals
### URLs and domain names
A URL (Uniform Resource Locator) is the full address of any page on the web. Every URL has the same anatomy:
`https://` `app.` `zorto` `.dev` `/docs/concepts/`
| Part | Name | What it is |
|------|------|-----------|
| `https://` | Protocol | Secure connection |
| `app.` | Subdomain | Optional prefix pointing to a different server |
| `zorto` | Second-level domain | The name you buy and own |
| `.dev` | Top-level domain (TLD) | The extension (.com, .dev, .org, etc.) |
| `/docs/concepts/` | Path | Which page on the site |
When you set `base_url = "https://zorto.dev"` in Zorto's `config.toml`, you're setting the protocol, domain, and TLD. Zorto generates the paths from your content file structure.
### Domain name
Your website's identity on the internet. A domain name has two parts you choose: the **second-level domain** (the name itself, like `zorto`) and the **top-level domain** (the extension, like `.dev`). Together they form the address people type in their browser: `zorto.dev`.
You buy a domain from a registrar (Namecheap, Cloudflare, Squarespace, etc.), typically for $10–20/year. Once you own it, nobody else in the world can have the same one. Think of it like property — you own the address.
RegisterBuy from a registrar ($10-20/yr)
→
ConfigurePoint DNS at your host
→
LiveVisitors type your domain
From registration to a live website.
### Top-level domains (TLDs)
The last part of a domain — `.com`, `.dev`, `.org`, etc. Different TLDs have different associations:
.dev — developers and tech. .io — startups and tech. .ai — AI companies. .shop, .blog, .design — industry-specific.
`.com` is the most common and widely recognized. Newer TLDs like `.dev` and `.io` are popular in tech. The right choice depends on your audience and brand.
### Subdomains
A prefix before your domain name, separated by a dot. `app.zorto.dev` is a subdomain of `zorto.dev`. Each subdomain can point to a completely different server:
| Subdomain | Points to |
|-----------|-----------|
| `app.zorto.dev` | A VPS at 164.90.252.58 |
| `www.zorto.dev` | Redirects to `zorto.dev` |
| `zorto.dev` | Netlify |
Each subdomain can point to a completely different server.
The old `www.` prefix is actually a subdomain too — it was common in the early web but most modern sites redirect it to the bare domain.
### DNS
The Domain Name System — the internet's phone book. When someone types your domain, DNS translates it to the IP address where your site actually lives:
Typezorto.dev
→
LookupDNS finds the IP
→
ConnectBrowser reaches the server
→
LoadPage appears
This happens in milliseconds, every time anyone visits any website.
When you deploy a Zorto site, you configure DNS records to point your domain at your hosting provider. There are two main types:
### CNAME record
A DNS record that maps one domain name to another — an alias. Instead of saying "my site lives at IP 75.2.60.5," a CNAME says "my site is the same as `my-site.netlify.app`."
CNAME record
www.example.com → my-site.netlify.app. Points to another domain name. Used for subdomains (www, app, etc.).
A record
example.com → 75.2.60.5. Points directly to an IP address. Used for the root domain (no www).
Your hosting provider tells you which to create. CNAME for subdomains, A records for root domains. See the [custom domain guide](/docs/how-to/custom-domain/) for exact records per provider.
### A record
A DNS record that maps a domain name directly to an IP address. Used for the root domain (`example.com` without `www`) because CNAME records technically can't be used there.
When you deploy, your hosting provider gives you one or more IP addresses. Create A records pointing your root domain to those IPs. The [deploy guide](/docs/how-to/deploy/) shows the exact IPs for Netlify, GitHub Pages, Vercel, and Cloudflare.
### HTTPS / SSL
HTTPS encrypts the connection between your visitor's browser and your server. Look for the padlock icon in your browser's address bar right now — that's HTTPS in action.
HTTPS (secure)
Encrypted connection. Padlock icon in browser. Required by modern browsers and search engines.
HTTP (insecure)
Unencrypted. Browser shows a 'Not secure' warning. Bad for trust and SEO.
Every static hosting provider provisions HTTPS certificates automatically for your domain. No configuration needed. The underlying technology is TLS (Transport Layer Security), sometimes still called SSL (its predecessor's name).
### IP address
A numeric address that identifies a device on the internet. You encounter them when setting up DNS A records:
IPv4
75.2.60.5 — four numbers separated by dots. The format you will see most often when configuring DNS.
IPv6
2604:a880:4:1d0::5a:c000 — longer, with hexadecimal and colons. Created because the world ran out of IPv4 addresses.
Domain names exist so humans don't have to remember these numbers. DNS translates between the two automatically.
### Static site
A website made entirely of pre-built files — HTML, CSS, JavaScript, images, and optional static data files — served directly to browsers. The server just sends files; it doesn't run application code, query a server-side database, or generate pages on the fly.
Static site
Pre-built files. Served from CDN. No server-side code. Zorto is a static site generator.
Dynamic site
Generated per request. Requires a running server and database. WordPress, Rails, Django.
### Static hosting
A service optimized for serving pre-built static files to visitors worldwide:
| Provider | Highlights |
|----------|-----------|
| [Netlify](https://netlify.com) | Auto-deploy from Git, instant rollbacks |
| [Vercel](https://vercel.com) | Fast edge network, preview deployments |
| [Cloudflare Pages](https://pages.cloudflare.com) | Global CDN, unlimited bandwidth |
| [GitHub Pages](https://pages.github.com) | Free for public repos |
All handle HTTPS, CDN, and continuous deployment automatically. See [how to deploy](/docs/how-to/deploy/).
Push to your Git repo and your site updates within seconds.
### CDN
A Content Delivery Network — a global network of servers that cache copies of your site close to your visitors:
DeployYour site in Virginia
→
CacheCDN copies files to 200+ locations
→
ServeVisitor in Tokyo gets the nearby copy
Static hosting providers include CDN automatically.
### 404 page
The error page visitors see when they request a URL that doesn't exist. "404" is the HTTP status code for "not found." Zorto generates `public/404.html` from your `templates/404.html` — customize it with a friendly message and a link back to your homepage. Static hosting providers serve it automatically.
## Content and structure
### Markdown
A text format that uses simple symbols for formatting. You're probably already familiar with it from GitHub, Slack, or Discord:
- `**bold**` → **bold**
- `*italic*` → *italic*
- `# Heading` → a heading
- `[link text](url)` → a clickable link
- `` `code` `` → inline code
Zorto uses Markdown as its content format. Your content files are `.md` files with TOML metadata at the top. See [content model](/docs/concepts/content-model/) for how Zorto organizes Markdown files into sections and pages.
### TOML
A configuration file format designed for humans to read and write. Zorto uses TOML for `config.toml` and for frontmatter in content files. It looks like this:
```toml
title = "My site"
base_url = "https://example.com"
[markdown]
highlight_code = true
```
Zorto uses TOML rather than YAML or JSON. TOML supports comments, doesn't rely on indentation for structure, and handles nested tables cleanly. See [configuration](/docs/concepts/configuration/) for how `config.toml` is structured.
### Frontmatter
The metadata block at the top of every Markdown content file, enclosed between `+++` markers:
```markdown
+++
title = "My page"
date = "2026-01-15"
tags = ["rust", "tutorial"]
+++
Your content starts here.
```
Frontmatter controls how Zorto processes the page — its title, date, template, URL slug, draft status, and any custom data you need in templates. The format is TOML. See [content model](/docs/concepts/content-model/) for the complete list of frontmatter fields.
### Section
A directory inside `content/` that contains an `_index.md` file. Sections are how Zorto organizes content into groups — a blog, a docs area, a portfolio. Each section:
- Lists its child pages at its URL (e.g., `/posts/` shows all blog posts)
- Can sort pages by date or title
- Can paginate (show 10 per page)
- Can use a custom template
Without an `_index.md`, a directory is just a namespace — it doesn't generate a listing page. See [content model](/docs/concepts/content-model/) for the full explanation and [how to organize content](/docs/how-to/organize-content/) for nested sections.
### Taxonomy
A way to classify content across sections. Tags and categories are the most common taxonomies. When you add `tags = ["rust", "tutorial"]` to a page's frontmatter, Zorto automatically generates:
- `/tags/` — a page listing all tags
- `/tags/rust/` — a page listing all pages tagged "rust"
- `/tags/tutorial/` — a page listing all pages tagged "tutorial"
You can define any taxonomy — not just tags. Authors, categories, topics, or anything else that groups pages by shared attributes. See [how to add a blog](/docs/how-to/add-blog/) for tags setup and [how to set up multiple authors](/docs/how-to/multiple-authors/) for custom taxonomies.
### Atom feed
An XML file that lets readers subscribe to your site using feed readers (like Feedly or NetNewsWire). When you publish new content, subscribers see it automatically without visiting your site.
Zorto generates an Atom feed at `/atom.xml` when you set `generate_feed = true` in config. Pages need a `date` in frontmatter to appear in the feed. If you're searching for "RSS" — Atom and RSS serve the same purpose; Atom is what Zorto generates. See [how to add a blog](/docs/how-to/add-blog/) for setup.
### Shortcode
A named, reusable content component you embed in Markdown instead of writing raw HTML. For example, instead of writing `` tags manually, you write:
Zorto replaces this with properly structured HTML at build time. There are two types: **inline** shortcodes (single line, double curly braces) and **body** shortcodes (wrap content, curly-percent delimiters). Zorto includes 15 built-in shortcodes for tabs, figures, diagrams, embeds, and more. See [shortcodes](/docs/concepts/shortcodes/) for the full list.
### Co-located assets
Images, scripts, and other files placed in the same directory as a content page. Instead of managing a separate `static/images/` folder, you keep assets next to the content that uses them:
📂content/posts/my-post/
├── 📝index.mdpage
├── 📝photo.jpg
├── 📝chart.svg
Assets live next to content. Reference with relative paths.
Reference them in Markdown with relative paths: ``. Zorto copies them to the output alongside the HTML. See [content model](/docs/concepts/content-model/) for details.
### Pagination
Splitting a long list of pages across multiple pages — showing 10 posts per page instead of all 200 at once. Controlled by the `paginate_by` field in a section's frontmatter:
```toml
+++
title = "Blog"
sort_by = "date"
paginate_by = 10
+++
```
Zorto generates `/posts/`, `/posts/page/2/`, `/posts/page/3/`, etc. Templates receive a `paginator` variable for building navigation. See [blog](/docs/concepts/blog/) for the full pattern.
### Draft
A page with `draft = true` in its frontmatter. Drafts are excluded from production builds (`zorto build`) but can be previewed locally with `zorto preview --drafts`. Use drafts for work-in-progress content you're not ready to publish.
### Theme
A bundled set of templates and styles that controls your site's appearance. Zorto ships 16 built-in themes (`zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, `charcoal`) embedded directly in the binary. Set one in config with `theme = "zorto"` and you get a complete, working site without creating any templates or stylesheets. Override any template or style file locally to customize. See [themes](/docs/concepts/themes/).
### Slug
The URL-friendly version of a page's filename. `content/posts/my-first-post.md` becomes `/posts/my-first-post/` in the URL. Override it with the `slug` frontmatter field if you want a different URL than what the filename suggests.
### Base URL
The root URL of your site, set as `base_url` in `config.toml`. Every generated URL — feeds, sitemaps, canonical links, Open Graph tags — is built from this. Set it to your production domain before deploying:
```toml
base_url = "https://example.com"
```
## Build and deploy
### Tera
The template engine Zorto uses for HTML rendering. Templates are HTML files with special syntax for dynamic content:
- `{{ page.title }}` — insert a value
- `{% for page in section.pages %}` — loop over pages
- `{% block content %}{% endblock %}` — define overridable regions
If you've used Jinja2 (Python), Twig (PHP), or Liquid (Jekyll), Tera will feel familiar. See [templates](/docs/concepts/templates/).
### PyO3
The technology that lets Zorto run Python code blocks. When you write a `{python}` code block in your Markdown, PyO3 runs it inside the Zorto process — no separate Python shell needed. You never interact with PyO3 directly. See [executable code blocks](/docs/concepts/executable-code/) and the [how-to guide](/docs/how-to/executable-code-blocks/).
### Build command
The command that generates your final site files. For Zorto, it's `zorto build`, which reads your content, templates, and config, then writes the complete static site to `public/`. In CI/CD, the build command also typically includes installing Zorto first:
```bash
curl -LsSf https://dkdc.sh/zorto/install.sh | sh && zorto build
```
### SCSS
A superset of CSS that adds features plain CSS lacks — variables, nesting, and reusable mixins. Instead of repeating the same color value in 20 places, you define it once:
```scss
$accent: #3b82f6;
.button { background: $accent; }
.link { color: $accent; }
```
Zorto compiles `.scss` files from your `sass/` directory into plain CSS at build time. `sass/style.scss` becomes `public/style.css`. See [how to customize your theme](/docs/how-to/customize-theme/) for overriding styles.
### Template block
A named region in a template that child templates can override. Here's how it works in practice:
```html
{# base.html defines the skeleton with named blocks #}
{% block content %}{% endblock %}
```
```html
{# page.html fills in just the content block #}
{% extends "base.html" %}
{% block content %}
{{ page.title }}
{% endblock %}
```
Navigation and footer come from the base. Each page only writes the part that differs. This is how all Zorto themes work — you can override individual blocks without rewriting the entire layout.
### Executable code blocks
Fenced code blocks tagged with `{python}` or `{bash}` that Zorto runs at build time. The output is captured and rendered inline in the HTML:
````markdown
```{python}
print(f"Built on: {datetime.now():%Y-%m-%d}")
```
````
This keeps documentation always up to date — output is regenerated on every build, so it stays in sync with the code. Use it for CLI help text, data tables, generated charts, or anything that should match the current state of your code. See [executable code blocks](/docs/concepts/executable-code/).
### Continuous deployment
A workflow where pushing code to a Git repository automatically triggers a build and deploy. All major static hosting providers support this. The typical flow:
PushCommit to main
→
BuildHost runs zorto build
→
DeployNew files go live
→
DoneSite updated in seconds
## SEO and discovery
### Favicon
The small icon next to your site's name in browser tabs, bookmarks, and history. Place your favicon file in `static/` and configure it in `config.toml`:
```toml
[extra]
favicon = "/favicon.svg"
favicon_mimetype = "image/svg+xml"
```
SVG scales to any size and can adapt to dark mode via CSS. PNG (32x32) and ICO are also supported. See [Add a favicon](/docs/how-to/add-favicon/).
### Sitemap
An XML file listing every page on your site so search engines can discover and index them efficiently. Zorto generates `sitemap.xml` automatically. See [how to add a sitemap](/docs/how-to/add-sitemap/) for robots.txt setup and search engine submission.
### robots.txt
A plain-text file at `/robots.txt` that tells search engine crawlers what to index. Create it as `static/robots.txt` in your Zorto project:
```
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
```
Replace `https://example.com` with your `base_url`. See [how to add a sitemap](/docs/how-to/add-sitemap/).
### SEO
Search engine optimization — how people find your site through Google (and increasingly, through AI tools like ChatGPT and Claude). Good SEO means structuring your site so search engines understand what each page is about and rank it for relevant searches.
The basics: every page needs a clear `title` and `description` in frontmatter. Beyond that, clean URL structure, fast load times, HTTPS, a sitemap, and Open Graph tags all contribute. Zorto generates sitemaps and llms.txt automatically; built-in themes include Open Graph and canonical URL tags. See the [SEO guide](/docs/how-to/seo/) for specifics.
### Open Graph
A protocol that controls how your pages appear when shared on social media — the title, description, and preview image in those link cards on Twitter, Facebook, LinkedIn, etc. Implemented via `` tags in your template's ``. The built-in Zorto themes include Open Graph tags automatically.
### Canonical URLs
When the same page is reachable at multiple URLs (e.g., with and without `www`, or with and without a trailing slash), search engines need to know which version is the "official" one. A canonical URL tag in the `` tells them, preventing duplicate-content penalties. Zorto generates absolute URLs from your `base_url` configuration.
### llms.txt
A proposed standard file (like `robots.txt` but for AI) that gives large language models a structured index of your site's content. Instead of crawling and parsing HTML, an agent can read a single URL and understand your entire site.
Zorto generates two files automatically:
- `/llms.txt` — links to Markdown versions of every page
- `/llms-full.txt` — the full content of every page in one file
This is for **consumption**, not editing. Agents that need to modify your site work directly on the filesystem. `llms.txt` is for agents that need to understand and explain your content.
### Structured data
Machine-readable metadata embedded in your pages that tells search engines what your content represents — not just the words on the page, but that this page is a *recipe*, or an *event*, or a *FAQ*. This powers rich results in search: recipe cards with cooking times, event listings with dates, FAQ dropdowns, product ratings with stars.
The most common format is JSON-LD, a block of JSON in a `