# 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.
BuildRust CLI renders the site
Generateuv script writes site.ddb
Shipstatic hosting serves HTML and data
QueryDuckDB-Wasm runs in the browser

Local pipeline. Static artifact. Browser query.

## Current slice
📂website/
├── 📝bin/build-metauv script
├── 📝data/meta.tomlpipeline config
├── 📝data/analytics.tomldashboard config
├── 📝static/data/site.ddbpublic DuckDB file
├── 📝static/data/analytics-dashboard.jsonruntime manifest
├── 📝static/js/analytics-dashboard.jsbrowser app
├── 📝content/analytics/_index.mdpage

The zorto.dev data-app files today.

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
Dashboard /analytics/ Database /data/site.ddb Manifest analytics-dashboard.json Pipeline website/bin/build-meta Metadata config website/data/meta.toml Dashboard config website/data/analytics.toml
## 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:**
{{ include(path="README.md", strip_frontmatter="true") }}
## tabs Render tabbed content panels. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `labels` | string | *required* | Pipe-separated tab labels | Each tab's content is separated by `` in the body. **Example:**
`cargo install zorto`
`uv tool install zorto`
`curl -LsSf https://dkdc.sh/zorto/install.sh | sh`
## note Styled callout box. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `type` | string | *required* | Style: `info`, `warning`, `tip`, `danger` | **Examples:**

Note

This is an info note.

Warning

This is a warning note.

Tip

This is a tip note.

Danger

This is a danger note.
**Syntax:** ```

Warning

Be careful with this operation.
``` ## details Collapsible disclosure section. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `summary` | string | *required* | Text shown in the clickable summary | | `open` | bool | `false` | Whether the section starts expanded | **Example:**
Click to expand
Hidden content revealed on click. You can include any markdown here: **bold**, `code`, lists, etc.
**Syntax:** ```
Click to expand
This starts expanded.
``` ## figure Image with optional caption. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `src` | string | *required* | Image URL or path | | `alt` | string | `""` | Alt text | | `caption` | string | `""` | Caption displayed below the image | | `width` | string | `""` | CSS width (e.g. `"80%"`, `"400px"`) | **Syntax:** ```
Zorto logo
The dashboard view
``` ## youtube Embed a YouTube video. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `id` | string | *required* | YouTube video ID | **Syntax:** ```
``` ## gist Embed a GitHub Gist. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `url` | string | *required* | Full Gist URL | | `file` | string | `""` | Specific file from the gist to embed | **Syntax:** ```
``` ## mermaid Render a Mermaid diagram. **Example:**
graph LR
    A[Markdown] --> B[Zorto]
    B --> C[HTML]
    B --> D[CSS]
    B --> E[Sitemap]
**Syntax:** ```
graph LR
    A[Markdown] --> B[Zorto]
    B --> C[HTML]
``` ## pyref Auto-generate Python API reference documentation by introspecting a module at build time. Requires the `python` feature. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `module` | string | *required* | Python module name to document | | `recursive` | bool | `true` | Walk submodules | | `exclude` | string | `""` | Comma-separated names to exclude | | `include` | string | `""` | Comma-separated allowlist (only document these) | | `private` | bool | `false` | Include `_private` members | **Example:**
{{ pyref(module="zorto", exclude="main,core", recursive="false") }}
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:**
{{ configref(src="../crates/zorto-core/src/config.rs") }}
Parses struct definitions, field types, `///` doc comments, and `#[serde(...)]` attributes to generate HTML tables. ## flow Horizontal step flow diagram with arrows between steps. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `steps` | string | *required* | Pipe-delimited steps, each as `Label:Description` or just `Label` | | `caption` | string | `""` | Caption text below the diagram | **Example:**
WriteMarkdown content
BuildCompile site
DeployPush to production

A typical workflow.

**Syntax:**
{{ flow(steps="Write:Content|Build:Compile|Deploy:Ship", caption="Development workflow.") }}
## layers Vertical layered stack diagram with numbered items. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `items` | string | *required* | Pipe-delimited items, each as `Title:Description:badge` | | `caption` | string | `""` | Caption text below the diagram | **Example:**
1
Identity
Site name and URL
base_url
2
Build
Output toggles
feeds, sitemap
3
Theme
Visual appearance
theme

Configuration layers.

**Syntax:**
{{ layers(items="Layer 1:Description:badge|Layer 2:Description:badge") }}
## 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:**
📂my-site/
├── 📝config.toml
├── 📂content/
    ├── 📝_index.mdsection
    ├── 📝about.mdpage
├── 📂templates/
├── 📂sass/

A typical Zorto project.

**Syntax:**
{% tree(caption="Project structure.") %}
content/
  _index.md  [section]
  about.md  [page]
{% end %}
## 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:**
Before
Manual process, error-prone, slow.
After
Automated, validated, fast.
**Syntax:**
{{ compare(left_title="Option A", left="Description A", right_title="Option B", right="Description B") }}
## cascade Override/priority cascade diagram. The last item is highlighted as the winner. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `items` | string | *required* | Pipe-delimited items, each as `Priority:Label:badge` | | `caption` | string | `""` | Caption text below | **Example:**
DefaultBuilt-in theme templatesfallback
OverrideYour local templates/wins

Local files always take priority.

**Syntax:**
{{ cascade(items="Low:Default value:default|High:Your override:wins") }}
## Presentation shortcodes The following shortcodes are designed for use in [presentations](/docs/concepts/presentations/) but work in any page. ## slide_image Absolutely positioned image for slide layouts. **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `src` | string | *required* | Image path or URL | | `alt` | string | `""` | Alt text | | `top` | string | | CSS top position (e.g. `"10%"`, `"50px"`) | | `left` | string | | CSS left position | | `right` | string | | CSS right position | | `bottom` | string | | CSS bottom position | | `width` | string | | CSS width | | `height` | string | | CSS height | **Syntax:**
{{ slide_image(src="logo.png", top="10%", right="5%", width="200px") }}
## 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.
FallbackTheme defaults — bundled templates, styles, shortcodesdefault
PriorityYour project — templates/, sass/, templates/shortcodes/wins

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:
{{ figure(src="/images/photo.jpg", alt="A photo", caption="My caption") }}
**Body shortcodes** (wrap content) use curly-percent:
{% note(type="warning") %}
Be careful with this operation.
{% end %}
## Built-in shortcodes | Shortcode | Type | Description | |-----------|------|-------------| | `include` | Inline | Include another file's content | | `tabs` | Body | Tabbed content panels | | `note` | Body | Styled callout box | | `details` | Body | Collapsible `
` section | | `figure` | Inline | Image with optional caption | | `youtube` | Inline | Embedded YouTube video | | `gist` | Inline | Embedded GitHub gist | | `mermaid` | Body | Mermaid.js diagram | | `pyref` | Inline | Python API reference (requires `python` feature) | | `configref` | Inline | Config reference from Rust source doc comments | | `flow` | Inline | Horizontal step flow diagram | | `layers` | Inline | Vertical layered stack diagram | | `tree` | Body | File tree visualization | | `compare` | Inline | Side-by-side comparison cards | | `cascade` | Inline | Priority/override cascade diagram | | `slide_image` | Inline | Absolutely positioned image (presentations) | | `speaker_notes` | Body | Reveal.js speaker notes (presentations) | | `fragment` | Body | Progressive reveal on click (presentations) | | `columns` | Body | Multi-column layout (presentations) | The diagram shortcodes (`flow`, `layers`, `tree`, `compare`, `cascade`) render pure CSS/HTML visuals with no JavaScript. They are used throughout these docs — see [content model](/docs/concepts/content-model/) and [AI-native](/docs/concepts/ai-native/) for examples. The presentation shortcodes (`slide_image`, `speaker_notes`, `fragment`, `columns`) are designed for use in [presentations](/docs/concepts/presentations/) but work in any page. See the [shortcodes reference](/docs/reference/shortcodes/) for parameters and live examples. ## Custom shortcodes Create a template in `templates/shortcodes/` to define your own: ```html

Hello, {{ name }}!

``` Use it in markdown:
{{ greeting(name="world") }}
Body shortcodes receive the inner content as `body`: ```html

{{ title }}

{{ body }}
```
{% card(title="My card") %}
Card content goes here.
{% end %}
## 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`.

FieldTypeDefaultDescription
base_urlstringrequiredSite base URL without trailing slash (e.g. `"https://example.com"`).
titlestring""Site title, used in feeds, templates, and `llms.txt`.
descriptionstring""Site description, used in feeds and `llms.txt`.
default_languagestring"en"Default language code (default: `"en"`).
compile_sassbooltrueCompile SCSS files from `sass/` directory (default: `true`).
generate_feedboolfalseGenerate an Atom feed at `/atom.xml` (default: `false`).
generate_sitemapbooltrueGenerate a sitemap at `/sitemap.xml` (default: `true`).
generate_llms_txtbooltrueGenerate `llms.txt` and `llms-full.txt` (default: `true`).
themestringnullBuilt-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.
extratable{}Arbitrary extra values accessible in templates as `config.extra`.
generate_md_filesboolfalseGenerate `.md` output files alongside HTML for every page (default: `false`).
compile_all_themesboolfalseCompile 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.
cachecacheconfig""Code block execution cache configuration.
executeexecuteconfig""Code block execution configuration (timeouts, etc.).

[markdown]

Configuration for the Markdown rendering pipeline.

FieldTypeDefaultDescription
highlight_codebooltrueEnable syntax highlighting for fenced code blocks (default: `true`).
insert_anchor_linksstring"none"Insert anchor links on headings.
highlight_themestringnullSyntect theme name (default: `"base16-ocean.dark"`).
external_links_target_blankboolfalseOpen external links in a new tab.
external_links_no_followboolfalseAdd `rel="nofollow"` to external links.
external_links_no_referrerboolfalseAdd `rel="noreferrer"` to external links.
smart_punctuationboolfalseEnable smart punctuation (curly quotes, em dashes, etc.).

[[taxonomies]]

A taxonomy definition from `[[taxonomies]]` in `config.toml`.

FieldTypeDefaultDescription
namestringrequiredTaxonomy name (e.g. `"tags"`, `"categories"`).

[[content_dirs]]

Configuration for loading an external directory of plain markdown as content.

FieldTypeDefaultDescription
pathstringrequiredPath to the external directory (relative to site root).
url_prefixstringrequiredURL prefix for generated pages (e.g. `"docs"` → `/docs/...`).
templatestring"page.html"Template for generated pages (default: `"page.html"`).
section_templatestring"section.html"Template for generated sections (default: `"section.html"`).
sort_bystringnullSort order for pages within generated sections.
rewrite_linksboolfalseRewrite relative `.md` links in content to clean URL paths.
excludestring[][]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:
{{ slide_image(src="logo.png", top="10%", right="5%", width="200px") }}
## 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:
{% extends "base.html" %}

{% block content %}
<article>
  <h1>{{ page.title }}</h1>

  {% if page.date %}
  <time>{{ page.date | date(format="%B %d, %Y") }}</time>
  {% endif %}

  {% if page.taxonomies.tags %}
  <div class="tags">
    {% for tag in page.taxonomies.tags %}
    <a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">{{ tag }}</a>
    {% endfor %}
  </div>
  {% endif %}

  {{ page.content | safe }}
</article>
{% endblock %}
## Filters and tests Tera provides filters (transform values) and tests (check conditions). Common ones:
{{ page.date | date(format="%B %Y") }}  <!-- "January 2026" -->
{{ pages | slice(start=0, end=5) }}     <!-- first 5 items -->
{{ count | pluralize }}                  <!-- "s" if count != 1 -->
{% if path is starting_with("/docs") %}...{% endif %}
See [Tera's documentation](https://keats.github.io/tera/docs/) for the full list of filters and tests. ## Blocks and inheritance
Parentbase.html — shared layout (nav, footer, HTML skeleton)extends
Childindex.html, section.html, page.html — fill in specific blocksoverrides

Child templates extend base.html and override only the blocks they need.

Templates use block inheritance: ```html {# base.html #} {% block content %}{% endblock %} ``` ```html {# page.html #} {% extends "base.html" %} {% block content %}

{{ page.title }}

{{ 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:
{% macro card(title, url) %}
  <a href="{{ url }}" class="card">{{ title }}</a>
{% endmacro %}

{{ self::card(title="Home", url="/") }}
## 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" +++ ![Sunset](sunset.jpg) ``` 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:
{%- if page.summary %}
  {{ page.summary | safe }}
{%- elif page.description %}
  {{ page.description }}
{%- endif %}
## 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 ![A photo](photo.jpg) ``` 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 `<head>` | | `content` | Main page content | | `open_graph` | Open Graph meta tags | | `extra_body` | Inject scripts before `</body>` | 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: <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">my-site/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">static/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">favicon.ico</span><span class="cv-tree__tag cv-tree__tag--url">→ /favicon.ico</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">images/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">logo.png</span><span class="cv-tree__tag cv-tree__tag--url">→ /images/logo.png</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">fonts/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">custom.woff2</span><span class="cv-tree__tag cv-tree__tag--url">→ /fonts/custom.woff2</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">robots.txt</span><span class="cv-tree__tag cv-tree__tag--url">→ /robots.txt</span></div> </div> <p class="cv-caption">Files in static/ are copied to the root of your output directory.</p> </div> Reference them with absolute paths in templates or markdown: ```html <link rel="icon" href="/favicon.ico"> <img src="/zorto-mark-transparent.png" alt="Logo"> ``` ```markdown ![Logo](/zorto-mark-transparent.png) ``` 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: <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">content/posts/my-post/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">index.md</span><span class="cv-tree__tag cv-tree__tag--page">page → /posts/my-post/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">photo.jpg</span><span class="cv-tree__tag cv-tree__tag--url">→ /posts/my-post/photo.jpg</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">diagram.svg</span><span class="cv-tree__tag cv-tree__tag--url">→ /posts/my-post/diagram.svg</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">data.json</span><span class="cv-tree__tag cv-tree__tag--url">→ /posts/my-post/data.json</span></div> </div> <p class="cv-caption">Assets live alongside the page that uses them.</p> </div> Reference co-located assets with relative paths in your markdown: ```markdown ![A photo](photo.jpg) ![A diagram](diagram.svg) ``` 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 ![Alt text](photo.jpg) ![Alt text](/zorto-logo-dark.png) ``` > [!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/`: <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">static/fonts/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">inter-regular.woff2</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">inter-bold.woff2</span></div> </div> </div> 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 `<head>` of `base.html`: ```html <link rel="icon" type="{{ config.extra.favicon_mimetype | default(value="image/png") }}" href="{{ config.extra.favicon }}"> ``` If `config.extra.favicon` is not set, no `<link rel="icon">` 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 %} <a href="{{ page.extra.author_url }}"> <img src="{{ page.extra.author_image }}" alt="{{ page.taxonomies.authors | first }}" /> </a> {% endif %} ``` ## Create author templates Add `templates/authors/single.html` for individual author pages (e.g., `/authors/jane-doe/`): ```html <h1>{{ term.name }}</h1> <ul> {% for page in term.pages %} <li><a href="{{ page.permalink }}">{{ page.title }}</a></li> {% endfor %} </ul> ``` Add `templates/authors/list.html` for the authors index at `/authors/`: ```html <h1>Authors</h1> <ul> {% for term in terms %} <li><a href="{{ term.permalink }}">{{ term.name }}</a> ({{ term.pages | length }} posts)</li> {% endfor %} </ul> ``` ## 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. <div class="cv-visual cv-visual--center"> <div class="cv-layers"> <div class="cv-layers__item"><div class="cv-layers__num">1</div><div class="cv-layers__content"><div class="cv-layers__title">Identity</div><div class="cv-layers__desc">Who is this site?</div></div><span class="cv-layers__badge cv-layers__badge--blue">base_url, title</span></div> <div class="cv-layers__item"><div class="cv-layers__num">2</div><div class="cv-layers__content"><div class="cv-layers__title">Build behavior</div><div class="cv-layers__desc">What outputs to produce?</div></div><span class="cv-layers__badge cv-layers__badge--blue">feeds, sitemap</span></div> <div class="cv-layers__item"><div class="cv-layers__num">3</div><div class="cv-layers__content"><div class="cv-layers__title">Content processing</div><div class="cv-layers__desc">How to parse and organize content?</div></div><span class="cv-layers__badge cv-layers__badge--blue">markdown, taxonomies</span></div> <div class="cv-layers__item"><div class="cv-layers__num">4</div><div class="cv-layers__content"><div class="cv-layers__title">Theme and custom data</div><div class="cv-layers__desc">How should the site look?</div></div><span class="cv-layers__badge cv-layers__badge--blue">theme, extra</span></div> </div> <p class="cv-caption">Four conceptual layers, one file. Everything from identity to appearance in config.toml.</p> </div> ## 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/<term>/` (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. <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-flow"> <div class="cv-flow__step"><div class="cv-flow__label"><strong>Register</strong>Buy from a registrar ($10-20/yr)</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Configure</strong>Point DNS at your host</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--green"><div class="cv-flow__label"><strong>Live</strong>Visitors type your domain</div></div> </div> <p class="cv-caption">From registration to a live website.</p> </div> ### Top-level domains (TLDs) The last part of a domain — `.com`, `.dev`, `.org`, etc. Different TLDs have different associations: <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-compare"> <div class="cv-compare__card cv-compare__card--accent"><div class="cv-compare__title">The classics</div><div class="cv-compare__body">.com — most recognized worldwide. .org — traditionally organizations. .net — traditionally networks.</div></div> <div class="cv-compare__card cv-compare__card--green"><div class="cv-compare__title">Newer options</div><div class="cv-compare__body">.dev — developers and tech. .io — startups and tech. .ai — AI companies. .shop, .blog, .design — industry-specific.</div></div> </div> </div> `.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: <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-flow"> <div class="cv-flow__step"><div class="cv-flow__label"><strong>Type</strong>zorto.dev</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Lookup</strong>DNS finds the IP</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Connect</strong>Browser reaches the server</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--green"><div class="cv-flow__label"><strong>Load</strong>Page appears</div></div> </div> <p class="cv-caption">This happens in milliseconds, every time anyone visits any website.</p> </div> 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`." <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-compare"> <div class="cv-compare__card cv-compare__card--accent"><div class="cv-compare__title">CNAME record</div><div class="cv-compare__body">www.example.com → my-site.netlify.app. Points to another domain name. Used for subdomains (www, app, etc.).</div></div> <div class="cv-compare__card cv-compare__card--green"><div class="cv-compare__title">A record</div><div class="cv-compare__body">example.com → 75.2.60.5. Points directly to an IP address. Used for the root domain (no www).</div></div> </div> </div> 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. <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-compare"> <div class="cv-compare__card cv-compare__card--green"><div class="cv-compare__title">HTTPS (secure)</div><div class="cv-compare__body">Encrypted connection. Padlock icon in browser. Required by modern browsers and search engines.</div></div> <div class="cv-compare__card cv-compare__card--muted"><div class="cv-compare__title">HTTP (insecure)</div><div class="cv-compare__body">Unencrypted. Browser shows a 'Not secure' warning. Bad for trust and SEO.</div></div> </div> </div> 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: <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-compare"> <div class="cv-compare__card cv-compare__card--accent"><div class="cv-compare__title">IPv4</div><div class="cv-compare__body">75.2.60.5 — four numbers separated by dots. The format you will see most often when configuring DNS.</div></div> <div class="cv-compare__card cv-compare__card--green"><div class="cv-compare__title">IPv6</div><div class="cv-compare__body">2604:a880:4:1d0::5a:c000 — longer, with hexadecimal and colons. Created because the world ran out of IPv4 addresses.</div></div> </div> </div> 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. <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-compare"> <div class="cv-compare__card cv-compare__card--accent"><div class="cv-compare__title">Static site</div><div class="cv-compare__body">Pre-built files. Served from CDN. No server-side code. Zorto is a static site generator.</div></div> <div class="cv-compare__card cv-compare__card--green"><div class="cv-compare__title">Dynamic site</div><div class="cv-compare__body">Generated per request. Requires a running server and database. WordPress, Rails, Django.</div></div> </div> </div> ### 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: <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-flow"> <div class="cv-flow__step"><div class="cv-flow__label"><strong>Deploy</strong>Your site in Virginia</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Cache</strong>CDN copies files to 200+ locations</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--green"><div class="cv-flow__label"><strong>Serve</strong>Visitor in Tokyo gets the nearby copy</div></div> </div> <p class="cv-caption">Static hosting providers include CDN automatically.</p> </div> ### 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 `<figure>` tags manually, you write: <pre><code>{{ figure(src="/photo.jpg", caption="My photo") }}</code></pre> 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: <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">content/posts/my-post/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">index.md</span><span class="cv-tree__tag cv-tree__tag--page">page</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">photo.jpg</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">chart.svg</span></div> </div> <p class="cv-caption">Assets live next to content. Reference with relative paths.</p> </div> Reference them in Markdown with relative paths: `![Photo](photo.jpg)`. 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 #} <nav>...</nav> {% block content %}{% endblock %} <footer>...</footer> ``` ```html {# page.html fills in just the content block #} {% extends "base.html" %} {% block content %} <h1>{{ page.title }}</h1> {% 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: <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-flow"> <div class="cv-flow__step"><div class="cv-flow__label"><strong>Push</strong>Commit to main</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Build</strong>Host runs zorto build</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Deploy</strong>New files go live</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--green"><div class="cv-flow__label"><strong>Done</strong>Site updated in seconds</div></div> </div> </div> ## 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 `<meta>` tags in your template's `<head>`. 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 `<head>` 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 `<script>` tag in your page's `<head>`. Add it to Zorto pages by overriding the `extra_head` block in your template (see [Customize your theme](/docs/how-to/customize-theme/)): <pre><code>{% block extra_head %} <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "headline": "{{ page.title }}" } </script> {% endblock %}</code></pre> Zorto's built-in themes don't include structured data by default — add it for pages where rich results would help. ## Troubleshooting Common issues and how to fix them. ## Build errors ### "shortcode template not found" You referenced a shortcode that doesn't exist. Check the spelling — Zorto's built-in shortcodes are: `include`, `tabs`, `note`, `details`, `figure`, `youtube`, `gist`, `mermaid`, `pyref`, `configref`, `flow`, `layers`, `tree`, `compare`, `cascade`. This also happens when Tera template syntax appears in your Markdown content and gets interpreted as a shortcode. Any pattern with double curly braces and parentheses is treated as a shortcode call — even inside fenced code blocks. To show template syntax safely in documentation, use HTML entities inside `<pre><code>` blocks. ### "path escapes sandbox boundary" The `include` or `configref` shortcode tried to read a file outside the allowed directory. Zorto restricts file access for security. Use the `--sandbox` flag to widen the boundary: ```bash zorto --root mysite --sandbox . build ``` ### Executable code block errors If a `{python}` block fails, check: - **Python not found**: Zorto embeds Python via PyO3, but your system needs Python available. The shell installer and PyPI package include it. - **Missing packages**: If your code imports third-party packages, create a virtual environment with `uv init --bare && uv add <package>`. Zorto automatically activates `.venv` at or above the site root. - **Stderr output**: stderr renders as a warning block, not as an error. Check the rendered page for warning-styled output. Use `zorto --no-exec build` to skip code execution entirely. ### Template errors Tera template syntax errors produce messages like `Failed to render template`. Common causes: - Missing `{% endblock %}` or `{% endif %}` - Using `{{ page.title }}` in a section template (use `section.title` instead) - Accessing a variable that doesn't exist (use `| default(value="")` to handle missing values) ## Preview server issues ### Port already in use If port 1111 is occupied, specify a different one: ```bash zorto preview --port 8080 ``` ### Not accessible from phone/tablet The preview server binds to `127.0.0.1` by default (localhost only). To access from other devices on your network: ```bash zorto preview --interface 0.0.0.0 ``` ### Changes not showing The preview server watches for file changes and rebuilds automatically. If changes aren't appearing: - Check that you saved the file - Check the terminal for build errors - Rust code changes (if building from source) require restarting the server ## Deployment issues ### Build succeeds locally but fails in CI - Make sure the CI environment installs Zorto before building - Check that executable code blocks have their dependencies available in CI (or use `--no-exec`) - Verify `base_url` in `config.toml` matches your production domain ### Broken links after deploy If internal links break after deployment, check that [`base_url`](/docs/concepts/glossary/#base-url) is set correctly. Links generated with the [`@/` prefix](/docs/concepts/content-model/#internal-links) are resolved at build time — they should work if the target file exists. ## Getting help - [GitHub issues](https://github.com/dkdc-io/zorto/issues) — report bugs and request features - [CLI reference](/docs/reference/cli/) — all available commands and flags - [Configuration reference](/docs/reference/config/) — every config option ## Next steps Now that you have a working site, here are some directions to explore. ## Recommended first: use a built-in theme The tutorial used hand-written templates. Zorto ships with 16 built-in themes (`zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, `midnight`, `sunset`, `mint`, `plum`, `sand`, `arctic`, `lime`, `charcoal`) that provide ready-made templates and styles. Add one line to `config.toml`: ```toml theme = "zorto" ``` With a theme active, you can delete the `templates/` directory entirely — the theme provides everything. You can still override individual templates by placing files in `templates/`. See [Themes](/docs/concepts/themes/) for details. ## Deploy your site Your site is ready to go live. See [Deploy your site](/docs/how-to/deploy/) for setup with Netlify, Vercel, Cloudflare Pages, or GitHub Pages. ## Learn the concepts - [Content model](/docs/concepts/content-model/): sections, pages, frontmatter, and how Zorto organizes content - [AI-native](/docs/concepts/ai-native/): explicit contracts, build-time validation, llms.txt - [Executable code blocks](/docs/concepts/executable-code/): run Python and Bash at build time - [Templates](/docs/concepts/templates/): the Tera template engine and available context variables - [Configuration](/docs/concepts/configuration/): every `config.toml` option explained ## Follow the how-to guides - [Add a blog](/docs/how-to/add-blog/): full guide with pagination and summaries - [Customize a theme](/docs/how-to/customize-theme/): override templates and styles - [Add a custom domain](/docs/how-to/custom-domain/): DNS records for each hosting provider - [Organize content](/docs/how-to/organize-content/): nested sections, co-located assets - [SEO](/docs/how-to/seo/): meta tags, Open Graph, llms.txt ## Customize styles with SCSS and CSS Override colors, fonts, spacing, and layout without forking a theme. All built-in themes use CSS custom properties (variables) for colors and layout. Override them in a custom stylesheet to change the look of your site without replacing any templates. ## CSS variables available in all themes Every built-in theme defines these variables on `:root` (dark mode) and `[data-theme="light"]` (light mode): | Variable | Purpose | Example value | |----------|---------|---------------| | `--accent` | Primary accent color (links, highlights) | `#3b82f6` | | `--accent-alpha-70` | Accent at 70% opacity | `rgba(59, 130, 246, .7)` | | `--accent-alpha-20` | Accent at 20% opacity | `rgba(59, 130, 246, .2)` | | `--accent-secondary` | Secondary accent (hover states) | `#22d3ee` | | `--accent-secondary-alpha-20` | Secondary accent at 20% opacity | `rgba(34, 211, 238, .2)` | | `--background` | Page background | `#0f172a` | | `--background-raised` | Raised surface (cards, nav) | `#1e293b` | | `--color` | Primary text color | `#e2e8f0` | | `--color-muted` | Secondary text (captions, metadata) | `rgba(226, 232, 240, .6)` | | `--border-color` | Border and divider lines | `rgba(59, 130, 246, .15)` | | `--code-bg` | Code block background | `rgba(0, 0, 0, 0.3)` | | `--max-width` | Maximum content width | `1200px` | ## Override with a custom stylesheet Create `sass/custom.scss` in your project root: ```scss // sass/custom.scss :root { --accent: #e74c3c; --background: #1a1a2e; --color: #eaeaea; --max-width: 900px; } ``` Zorto compiles SCSS to CSS at build time — `sass/custom.scss` becomes `/custom.css` in the output. Load it via the `extra_head` template block: ```html {% extends "base.html" %} {% block extra_head %} <link rel="stylesheet" href="/custom.css"> {% endblock %} ``` This approach layers your styles on top of the theme's defaults. You only override what you need. ## Light and dark mode patterns Themes default to dark mode on `:root` and define light mode overrides on `[data-theme="light"]`. To customize both modes: ```scss // sass/custom.scss // Dark mode (default) :root { --accent: #ff6b6b; --background: #1e1e2e; --background-raised: #2a2a3e; --color: #cdd6f4; --color-muted: rgba(205, 214, 244, .6); --border-color: rgba(255, 107, 107, .15); --code-bg: rgba(0, 0, 0, 0.3); } // Light mode [data-theme="light"] { --background: #ffffff; --background-raised: #f5f5f5; --color: #1e1e2e; --color-muted: rgba(30, 30, 46, .6); --border-color: #e0e0e0; --code-bg: #f1f5f9; } ``` The theme's JavaScript toggles the `data-theme` attribute on the `<html>` element. Your CSS variables respond automatically — no JavaScript changes needed. ## Replace the theme stylesheet entirely If you want full control, create `sass/style.scss` in your project. This replaces the theme's `style.scss` entirely (local files overlay theme files by filename). You will need to provide all the styles yourself, including layout, typography, and responsive breakpoints. > [!TIP] > Start by copying a theme's `style.scss` as a starting point and modify from there. Theme stylesheets import shared partials from `_structure.scss` and `_components.scss`. ## SCSS features Zorto compiles SCSS, so you can use: - **Variables**: `$phone-max-width: 683px;` - **Nesting**: `.navbar { &__inner { ... } }` - **Partials and imports**: `@import 'custom-components';` - **Mixins and functions**: standard Sass features Create additional partials in `sass/` and import them from your main stylesheet. ## Add custom fonts Load fonts via the `extra_head` block or import them in your SCSS: ```scss // sass/custom.scss @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap'); body { font-family: "Inter", sans-serif; } ``` Or use locally hosted fonts from the `static/` directory: ```scss @font-face { font-family: "CustomFont"; src: url("/fonts/custom.woff2") format("woff2"); font-display: swap; } body { font-family: "CustomFont", sans-serif; } ``` ## Related guides - [Customize your theme](/docs/how-to/customize-theme/) — template overrides, shortcodes, and the `extra_head` block - [Themes](/docs/concepts/themes/) — how the theme system works - [Asset management](/docs/how-to/assets/) — serving fonts and other static files ## Callouts Callouts are styled alert boxes rendered from standard markdown blockquote syntax. Zorto uses the [GitHub-style](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) `[!TYPE]` format — no shortcodes or HTML needed. ## Syntax ```markdown > [!NOTE] > Your content here. Supports **bold**, `code`, [links](https://example.com), > and multiple paragraphs. ``` ## Types There are five callout types, each with a distinct color and icon: > [!NOTE] > Highlights information that users should take into account. > [!TIP] > Optional information to help users succeed. > [!IMPORTANT] > Key information users need to know. > [!WARNING] > Critical content that requires user attention due to potential risks. > [!CAUTION] > Negative potential consequences of an action. ## Rich content Callouts support the full range of markdown inside them: ```markdown > [!TIP] > You can nest **bold**, `inline code`, and [links](https://zorto.dev). > > Multiple paragraphs work. So do: > - Bullet lists > - Code blocks (indented) > - Images ``` ## When to use callouts vs. shortcodes Callouts are best for inline alerts within prose — notes, warnings, tips. For more structured content like collapsible sections or styled cards, use [shortcodes](/docs/concepts/shortcodes/). Zorto also has a `note` shortcode (`{% note(type="info") %}`) that produces similar-looking boxes. Prefer callouts for standard prose alerts — they use standard Markdown syntax and render on GitHub too. Use the `note` shortcode only when you need programmatic control (e.g., in a template or shortcode body). ## Further reading - [Callouts reference](/docs/reference/callouts/) — all five types with rich content examples - [Shortcodes](/docs/concepts/shortcodes/) — the `note` shortcode and other rich content components ## Add a 404 page The built-in themes ship with a `404.html` template that renders automatically. If you want a custom one, create your own `templates/404.html` in your project root. ## Custom 404 template Create `templates/404.html`: ```html {% extends "base.html" %} {% block title %}404 | {{ config.title }}{% endblock title %} {% block content %} <div style="text-align: center; padding: 4rem 1rem;"> <h1>404</h1> <p>Page not found.</p> <a href="/">Go home</a> </div> {% endblock content %} ``` Your project-level template takes precedence over the theme's built-in `404.html`. To verify it looks correct, run `zorto build` and open `public/404.html` in your browser. > [!NOTE] > During `zorto preview`, the dev server may not route unknown paths to your 404 page. Test the final result by opening the built file directly or deploying to your hosting provider. ## Static host configuration Zorto builds `404.html` into `public/404.html`. Most static hosts serve this automatically: - **GitHub Pages** -- serves `404.html` by default - **Netlify** -- serves `404.html` by default - **Cloudflare Pages** -- serves `404.html` by default - **Vercel** -- serves `404.html` by default No additional Zorto configuration is needed. ## Related guides - [Customize your theme](/docs/how-to/customize-theme/) — how template overrides work - [Deploy your site](/docs/how-to/deploy/) — hosting setup for each platform ## Template functions and filters Zorto registers custom template functions, filters, and tests on top of the [Tera template engine](https://keats.github.io/tera/docs/). This page is the complete reference. ## Functions Functions are called with named arguments inside templates. ### get_url Returns the full permalink for a content path or static file. **Signature:** `get_url(path)` | Argument | Type | Description | |----------|------|-------------| | `path` | string | Content path (`@/` prefix), static file path, or external URL | **Content paths** use the `@/` prefix to reference files in the `content/` directory: <pre><code>{{ get_url(path="posts/hello.md") }} <!-- https://example.com/posts/hello/ --> {{ get_url(path="posts/_index.md") }} <!-- https://example.com/posts/ --></code></pre> In content files, prefix paths with `@/` for build-time link validation. **Static file paths** are resolved relative to the site root: <pre><code>{{ get_url(path="/img/photo.png") }} <!-- https://example.com/img/photo.png --></code></pre> **External URLs** are returned as-is: <pre><code>{{ get_url(path="https://github.com") }} <!-- https://github.com --></code></pre> ### get_section Loads a section object by its `_index.md` path. Returns the full section with its pages, useful for cross-referencing content. **Signature:** `get_section(path)` | Argument | Type | Description | |----------|------|-------------| | `path` | string | Relative path to the section's `_index.md` | <pre><code>{% set posts = get_section(path="posts/_index.md") %} {% for page in posts.pages %} <a href="{{ page.permalink }}">{{ page.title }}</a> {% endfor %}</code></pre> The returned section object has all the fields documented in the [frontmatter reference](/docs/reference/frontmatter/) (title, pages, path, permalink, extra, etc.). <div class="callout callout--warning"><p class="callout__title"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg> Warning</p><div class="callout__body"> `get_section` raises an error if the path does not match any loaded section. Double-check the path matches the actual `_index.md` location. </div></div> ### get_taxonomy_url Returns the permalink for a specific taxonomy term page. **Signature:** `get_taxonomy_url(kind, name)` | Argument | Type | Description | |----------|------|-------------| | `kind` | string | Taxonomy name (e.g. `"tags"`, `"categories"`) | | `name` | string | Term value (e.g. `"rust"`) | <pre><code>{{ get_taxonomy_url(kind="tags", name="rust") }} <!-- https://example.com/tags/rust/ --></code></pre> The term name is slugified to form the URL (e.g. `"My Tag"` becomes `my-tag`). ### now Returns the current local timestamp as a string in `YYYY-MM-DDTHH:MM:SS` format. **Signature:** `now()` <pre><code><footer>Built at {{ now() }}</footer> <!-- Built at 2026-04-04T14:30:00 --></code></pre> ## Filters Filters transform values using the pipe (`|`) syntax. Zorto provides these custom filters in addition to [Tera's built-in filters](https://keats.github.io/tera/docs/#built-in-filters). ### date Formats a date string using [chrono format specifiers](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `format` | string | `"%Y-%m-%d"` | Output format string | Accepts `YYYY-MM-DD` and `YYYY-MM-DDTHH:MM:SS` input formats. Returns the original string unchanged if parsing fails. <pre><code>{{ page.date | date(format="%B %d, %Y") }} <!-- June 15, 2025 --> {{ page.date | date(format="%Y") }} <!-- 2025 --></code></pre> ### pluralize Returns `"s"` when the value is not 1, empty string when it is 1. Useful for English pluralization. <pre><code>{{ count }} item{{ count | pluralize }} <!-- "1 item" or "5 items" --></code></pre> Works with integers and floats (floats are truncated to integers). ### slice Extracts a sub-array from an array. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `start` | int | `0` | Start index (inclusive) | | `end` | int | array length | End index (exclusive) | <pre><code>{% for page in section.pages | slice(end=5) %} <!-- First 5 pages --> {% endfor %} {% for page in section.pages | slice(start=2, end=7) %} <!-- Pages 3 through 7 --> {% endfor %}</code></pre> Out-of-bounds values are clamped to the array length. ## Tests Tests check conditions using the `is` keyword. ### starting_with Tests whether a string starts with the given prefix. <pre><code>{% if page.path is starting_with("/docs") %} <!-- Documentation page --> {% endif %}</code></pre> ## Tera built-in filters Zorto inherits all of Tera's built-in filters. Commonly used ones: | Filter | Description | Example | |--------|-------------|---------| | `safe` | Mark HTML as safe (no escaping) | `{{ page.content \| safe }}` | | `length` | Array or string length | `{{ items \| length }}` | | `upper` | Uppercase a string | `{{ title \| upper }}` | | `lower` | Lowercase a string | `{{ title \| lower }}` | | `replace` | Replace substring | `{{ title \| replace(from="old", to="new") }}` | | `truncate` | Truncate string | `{{ desc \| truncate(length=100) }}` | | `default` | Fallback value | `{{ author \| default(value="Anonymous") }}` | | `join` | Join array to string | `{{ tags \| join(sep=", ") }}` | | `first` | First element | `{{ items \| first }}` | | `last` | Last element | `{{ items \| last }}` | | `reverse` | Reverse array | `{{ items \| reverse }}` | | `sort` | Sort array | `{{ items \| sort }}` | | `json_encode` | Serialize to JSON | `{{ data \| json_encode }}` | See the [Tera documentation](https://keats.github.io/tera/docs/#built-in-filters) for the complete list. ## Further reading - [Templates concept](/docs/concepts/templates/) — template hierarchy and context variables - [Advanced templating](/docs/how-to/advanced-templating/) — macros, block inheritance, config.extra access - [Frontmatter reference](/docs/reference/frontmatter/) — all fields available on `page` and `section` objects ## Use executable code blocks Run Python and Bash at build time to generate dynamic content in your static site. <div class="cv-visual cv-visual--wide cv-visual--center"> <div class="cv-flow"> <div class="cv-flow__step"><div class="cv-flow__label"><strong>Write</strong>Code in markdown</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--accent"><div class="cv-flow__label"><strong>Build</strong>Zorto executes blocks</div></div> <div class="cv-flow__arrow">→</div> <div class="cv-flow__step cv-flow__step--green"><div class="cv-flow__label"><strong>Render</strong>Output baked into HTML</div></div> </div> <p class="cv-caption">Code runs at build time — the output becomes part of the static site.</p> </div> ## Setup No setup required for Bash blocks. For Python blocks, Zorto uses an embedded interpreter. If you need third-party packages, create a virtual environment: ```bash uv init --bare uv add pandas matplotlib ``` Zorto automatically activates the `.venv` in your project root. ## Write a Python block Use the `{python}` language tag in your markdown: ````markdown ```{python} from datetime import datetime print(f"Last built: {datetime.now():%Y-%m-%d %H:%M}") ``` ```` The output appears below the code block in the rendered page. ## Write a Bash block ````markdown ```{bash} echo "Running on $(uname -s) $(uname -m)" ``` ```` ## Run a script file For longer scripts, use the `file` attribute: ````markdown ```{python file="scripts/generate_chart.py"} ``` ```` The path is relative to the content file's directory. ## Keep CLI docs up to date Use executable blocks to ensure documentation always matches the current version: ````markdown ```{bash} zorto --help ``` ```` Every build regenerates the output, keeping the docs in sync with the current version. ## Preview without execution ```bash zorto --no-exec preview ``` Code blocks render as static syntax-highlighted code. Useful for faster iteration when you're editing prose, not code. ## Error handling - **stdout** renders as a code block below the source - **stderr** renders as a warning block - **Non-zero exit codes** render as an error block with the return code Errors don't stop the build — other pages continue rendering. ## Related guides - [Executable code blocks](/docs/concepts/executable-code/) — concept overview, Python runtime, security - [CLI reference](/docs/reference/cli/) — `--no-exec` flag and other options ## Your first site By the end of this tutorial you will have a working site with: - A homepage and a standalone About page - A blog section with two posts, tags, and an Atom feed - An executable Python code block that generates output at build time - A production build ready to deploy Prerequisites: [install Zorto](/docs/getting-started/installation/) first. ## Create a new site Run the following to scaffold a project: ```bash zorto init my-site cd my-site ``` Zorto creates the directory and populates it with starter files. Let's look at what it generated: <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">my-site/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">config.toml</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">content/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">_index.md</span><span class="cv-tree__tag cv-tree__tag--section">section: homepage</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">posts/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">_index.md</span><span class="cv-tree__tag cv-tree__tag--section">section: blog</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">hello.md</span><span class="cv-tree__tag cv-tree__tag--page">page: first post</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">static/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">templates/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">base.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">page.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">section.html</span></div> </div> <p class="cv-caption">Don't worry about memorizing this — you will explore each piece as you work through the tutorial.</p> </div> Here's what each part does: - `config.toml` — site configuration (title, URL, features) - `content/` — your Markdown content files. Files named `_index.md` define sections (collections); regular `.md` files are individual pages. - `static/` — files copied as-is to the output (images, fonts, CSS) - `templates/` — Tera HTML templates that control how content is rendered into web pages. `base.html` is the outer shell; `page.html` and `section.html` fill in the content. ## Start the preview server Launch the dev server so you can see changes in real time: ```bash zorto preview --open ``` Your browser opens `http://127.0.0.1:1111` and you should see a minimal homepage with a link to the "Hello World" post. If the browser doesn't open automatically, navigate to that URL manually. The server watches for file changes and reloads automatically. Leave this running in the background for the rest of the tutorial. ## Understand the configuration Open `config.toml` in your editor. It looks like this: ```toml base_url = "https://example.com" title = "My Site" generate_feed = true ``` - `base_url` is where the site will eventually live. For local development it doesn't matter, but you will want to set it before deploying. - `title` appears in templates and in the Atom feed. - `generate_feed` tells Zorto to produce an Atom feed at `/atom.xml`. You will come back to this file later to enable more features. For now, leave it as-is. ## Edit a page and see live reload Open `content/_index.md`. This is the homepage. It contains frontmatter (the metadata between `+++` markers) and optional body content: ```markdown +++ title = "Home" sort_by = "date" +++ ``` Add some text below the closing `+++`: ```markdown +++ title = "Home" sort_by = "date" +++ Welcome to my site! This is built with Zorto. ``` Save the file. Your browser reloads automatically and shows the new text on the homepage. What you just learned: content files are Markdown with TOML frontmatter. The `sort_by = "date"` on a section page tells Zorto to list child pages by date, newest first. ## Explore the blog section Zorto generated a blog section for you at `content/posts/`. Open `content/posts/_index.md`: ```markdown +++ title = "Blog" sort_by = "date" +++ ``` This is a section index — the underscore in `_index.md` marks it as the section's own page, not a child page. It renders as a listing page at `/posts/` showing all posts in the section. The frontmatter configures how the section behaves — `sort_by = "date"` means newest posts appear first. Any regular `.md` file you place in this directory becomes a blog post. Now open the sample post at `content/posts/hello.md`: ```markdown +++ title = "Hello World" date = "2025-01-01" description = "My first post" tags = ["hello"] +++ Welcome to my new site built with [zorto](https://github.com/dkdc-io/zorto)! ``` Notice the `date` field -- this is what `sort_by = "date"` uses for ordering. The `description` appears in post listings and the Atom feed. And `tags` assigns this post to the "hello" tag. Visit `http://127.0.0.1:1111/posts/hello/` in your browser to see the rendered post. ## Add a new blog post Create a file at `content/posts/learning-zorto.md`: ```markdown +++ title = "Learning Zorto" date = "2026-03-31" description = "Notes from working through the Zorto tutorial." +++ Today I built my first site with Zorto. Here are a few things I noticed: - The preview server reloads instantly when I save a file. - Content is just Markdown with TOML frontmatter. - Sections are directories with an `_index.md` file. ``` Save the file. Your browser reloads and the homepage now lists two posts, with "Learning Zorto" on top because its date is more recent. What you just learned: adding a page to a section is as simple as dropping a Markdown file into the directory. No configuration changes needed. ## Add tags Your new post does not have tags yet. Let's add some. Open `content/posts/learning-zorto.md` and add a `tags` field to the frontmatter: ```markdown +++ title = "Learning Zorto" date = "2026-03-31" description = "Notes from working through the Zorto tutorial." tags = ["tutorial", "getting-started"] +++ ``` Save the file. Zorto automatically generates pages for each tag. Visit `http://127.0.0.1:1111/tags/` to see all tags, and `http://127.0.0.1:1111/tags/tutorial/` to see posts tagged "tutorial". What you just learned: Zorto includes a `tags` taxonomy by default. You can add custom taxonomies in `config.toml`, but tags work without extra configuration. ## Check the Atom feed Because `generate_feed = true` is already in your config, Zorto has been generating an Atom feed at `/atom.xml` this whole time. Visit `http://127.0.0.1:1111/atom.xml` in your browser to see it. It includes the title, description, and content of each post. If you ever want to disable the feed, set `generate_feed = false` in `config.toml`. ## Add a standalone page Not everything belongs in the blog. Create `content/about.md`: ```markdown +++ title = "About" +++ This site is built with [Zorto](https://zorto.dev), an AI-native static site generator with executable code blocks. ``` Visit `http://127.0.0.1:1111/about/` to see it. Pages outside of a section directory render as standalone pages -- they don't appear in any listing unless you link to them from a template or another page. ## Try an executable code block Zorto can run code blocks at build time. Blocks tagged with `{python}` or `{bash}` execute during the build and their output is rendered inline. > [!NOTE] > This requires Python to be available on your system. If you don't have Python installed, skip to the next section — everything else works without it. Open `content/about.md` and add a Python code block: ````markdown +++ title = "About" +++ This site is built with [Zorto](https://zorto.dev). ```{python} from datetime import datetime print(f"Last built: {datetime.now():%Y-%m-%d %H:%M}") ``` ```` Save the file. The preview reloads and you see the Python output rendered inline — the current date and time, generated fresh on every build. The code lives next to the prose it supports. What you just learned: code blocks tagged with `{python}` or `{bash}` run at build time. This is how Zorto keeps documentation, data-driven pages, and CLI references always up to date. ## Build for production When you are ready to publish, stop the preview server (Ctrl+C) and run: ```bash zorto build ``` Zorto writes the complete site to `public/`. This directory contains plain HTML, CSS, and static assets -- you can host it anywhere (Netlify, Vercel, Cloudflare Pages, GitHub Pages, or your own server). <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">public/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">atom.xml</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">llms.txt</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">llms-full.txt</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">sitemap.xml</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">style.css</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">about/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">posts/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">hello/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">learning-zorto/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">tags/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">hello/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">getting-started/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">tutorial/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">        ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">index.html</span></div> </div> <p class="cv-caption">Plain HTML files you can host anywhere — no server runtime needed.</p> </div> Notice that Zorto generated `sitemap.xml`, `llms.txt`, and `llms-full.txt` automatically — no configuration needed. The `llms.txt` files make your content accessible to AI systems. What you just learned: `zorto build` produces a fully static site. `zorto preview` is for development; `zorto build` is for production. ## What you built In this tutorial you: 1. Scaffolded a new site with `zorto init` 2. Ran the live-reloading preview server 3. Edited the homepage and watched it update instantly 4. Created a blog post and saw it appear in the listing 5. Added tags and explored the auto-generated taxonomy pages 6. Verified the Atom feed 7. Added a standalone page with an executable code block 8. Built the site for production with auto-generated `llms.txt` That covers the core workflow. Head to [next steps](/docs/getting-started/first-site/) to explore themes, deployment, and more advanced features. ## Optimize for SEO and discoverability Configure your Zorto site for search engine visibility and AI agent discovery. Three layers: 1. **Content** — `title` and `description` in every page's frontmatter (you write this) 2. **Meta tags** — Open Graph, Twitter Cards, canonical URLs (built-in themes handle this) 3. **Discovery** — sitemap.xml, robots.txt, llms.txt, search data > [!TIP] > If you use a built-in theme (`zorto`, `dkdc`, `default`, `ember`, `forest`, `ocean`, `rose`, `slate`, etc.), Open Graph tags, canonical URLs, and Twitter Card tags are already included. The sections below show how to set them up manually if you use custom templates. ## Set title and description on every page Search engines use `title` and `description` directly in results. ```markdown +++ title = "Getting started with Zorto" description = "Install Zorto and build your first static site in under five minutes." date = "2025-01-15" +++ ``` Keep titles under 60 characters and descriptions under 160 characters. ## Add Open Graph meta tags Add [Open Graph](/docs/concepts/glossary/#open-graph) tags in your `templates/base.html` `<head>`: The built-in themes handle OG tags using `page.permalink` and `section.permalink` with conditional logic. Here's the pattern for custom templates: <pre><code>{%- if page %} {%- set og_url = page.permalink %} {%- set og_title = page.title %} {%- elif section %} {%- set og_url = section.permalink %} {%- set og_title = section.title %} {%- endif %} <meta property="og:title" content="{{ og_title }}" /> <meta property="og:url" content="{{ og_url }}" /> <meta property="og:type" content="article" /> <meta name="twitter:card" content="summary_large_image" /></code></pre> For pages with a description, add the `og:description` tag: <pre><code>{%- if page.description %} <meta property="og:description" content="{{ page.description }}" /> {%- endif %}</code></pre> ## Canonical URLs Zorto generates absolute [canonical URLs](/docs/concepts/glossary/#canonical-urls) using `base_url` from `config.toml`. Set it to your production domain: ```toml base_url = "https://example.com" ``` Add a canonical link in your base template (using the same `og_url` variable from the Open Graph section above): <pre><code><link rel="canonical" href="{{ og_url }}" /></code></pre> ## Sitemap Zorto generates `sitemap.xml` automatically. It is enabled by default — no configuration needed. The sitemap includes all non-draft pages and sections with their permalinks. Disable it if needed: ```toml generate_sitemap = false ``` Submit your sitemap URL to [Google Search Console](https://search.google.com/search-console) and [Bing Webmaster Tools](https://www.bing.com/webmasters) for faster indexing. ## robots.txt Create `static/robots.txt` to control crawler access and point to your sitemap: ``` User-agent: * Allow: / Sitemap: https://example.com/sitemap.xml ``` Replace `https://example.com` with your `base_url`. This is a plain file in `static/`, not a template — update the URL manually if your `base_url` changes. ## llms.txt Zorto generates [`llms.txt`](/docs/concepts/glossary/#llms-txt) and `llms-full.txt` by default. These files help AI agents understand your site's content and structure. No configuration required. Two files are generated: | File | Contents | Use case | |------|----------|----------| | `/llms.txt` | Structured index with links to every page, organized by section | Agent reads one URL to understand the site | | `/llms-full.txt` | Full raw markdown content of every page in a single file | Agent reads all content without crawling | The `llms.txt` file includes the site title as an H1, the description as a blockquote, then each section as an H2 with its pages listed as links. Pages with descriptions include them inline. Disable if needed: ```toml generate_llms_txt = false ``` When `generate_md_files = true` is also set, `llms.txt` links point to the `.md` versions of each page instead of the HTML versions. ## Built-in search Zorto search is DuckDB-backed. Ship a public `.ddb` file with a `search_pages` table, then point the built-in theme at it: ```toml [extra] search_database_url = "/data/site.ddb" search_database_file = "site.ddb" search_database_schema = "site" ``` The search data supports: - Case-insensitive matching - Ranked results (title matches score higher than body matches) - Prefix matching for autocomplete-style queries Search is disabled by default because the database file adds size to the output. Enable it for documentation sites, blogs, or any site where visitors need to find content quickly. ## Add custom headers for caching Your [static hosting provider](/docs/concepts/glossary/#static-hosting) serves files through a [CDN](/docs/concepts/glossary/#cdn) with default caching. For custom cache-control policies, create a `static/_headers` file (Netlify) or configure headers in your provider's dashboard. See the [deploy guide](/docs/how-to/deploy/) for platform-specific details. ## Related guides - [Deploy your site](/docs/how-to/deploy/) — hosting setup and headers - [Set up a custom domain](/docs/how-to/custom-domain/) — HTTPS and `base_url` - [Add a sitemap](/docs/how-to/add-sitemap/) — sitemap.xml submission details - [AI-native](/docs/concepts/ai-native/) — llms.txt and the consumption model - [Build optimization](/docs/how-to/build-optimization/) — disabling generators for faster builds ## Content directories Pull content from outside the `content/` directory using `[[content_dirs]]` in `config.toml`. This is ideal for documentation that lives alongside source code, or content shared between multiple sites. ## Configuration Add one or more `[[content_dirs]]` entries to your `config.toml`: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" ``` ### Fields | 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"` produces `/docs/...`) | | `template` | string | `"page.html"` | Template for generated pages | | `section_template` | string | `"section.html"` | Template for generated sections | | `sort_by` | string | *none* | Sort pages: `"date"` or `"title"` | | `rewrite_links` | bool | `false` | Rewrite relative `.md` links to clean URL paths | | `exclude` | array of strings | `[]` | Files to skip (relative to the external directory) | ## How it works Zorto walks the external directory and converts its files: - **`README.md`** files become sections (equivalent to `_index.md` in `content/`) - **Other `.md` files** become pages - The `url_prefix` is prepended to all generated URL paths <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">docs/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">README.md</span><span class="cv-tree__tag cv-tree__tag--section">section -> /docs/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">getting-started/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">README.md</span><span class="cv-tree__tag cv-tree__tag--section">section -> /docs/getting-started/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">installation.md</span><span class="cv-tree__tag cv-tree__tag--page">page -> /docs/getting-started/installation/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">quick-start.md</span><span class="cv-tree__tag cv-tree__tag--page">page -> /docs/getting-started/quick-start/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">reference/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">README.md</span><span class="cv-tree__tag cv-tree__tag--section">section -> /docs/reference/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">    ├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--page">cli.md</span><span class="cv-tree__tag cv-tree__tag--page">page -> /docs/reference/cli/</span></div> </div> <p class="cv-caption">External directory structure maps to site URLs.</p> </div> ### Title extraction Since external markdown files typically lack TOML frontmatter, Zorto extracts: - **Title** from the first `# Heading` line (or derived from the filename if absent) - **Description** from the first paragraph of prose The title heading is stripped from the rendered body to avoid duplication. ## Link rewriting When `rewrite_links = true`, Zorto rewrites relative `.md` links in the external content to clean URL paths that work on the built site: | Original link | Rewritten to | |---------------|-------------| | `[Install](/docs/reference/installation/)` | `[Install](/docs/getting-started/installation/)` | | `[CLI](/docs/reference/cli/)` | `[CLI](/docs/reference/cli/)` | | `[GitHub](https://github.com)` | unchanged | This allows the same markdown files to work as documentation on both GitHub (where `.md` links are clickable) and the built site (where clean URLs are used). ## Excluding files Use `exclude` to skip specific files. This is useful when you have manual content in `content/` that should take precedence: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" exclude = ["reference/cli.md", "internal/draft.md"] ``` Excluded files are expected to exist as manually authored content in the `content/` directory. ## Custom templates Assign dedicated templates for external content: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" template = "docs.html" section_template = "docs-section.html" sort_by = "title" ``` The `template` field is applied to all pages loaded from this directory. The `section_template` is applied to all sections (directories with `README.md`). ## Real-world example The [zorto.dev](https://zorto.dev) website uses `content_dirs` to pull in its own documentation: ```toml # website/config.toml [[content_dirs]] path = "../docs" url_prefix = "docs" template = "docs.html" section_template = "docs-section.html" sort_by = "title" rewrite_links = true ``` The `docs/` directory lives at the repository root alongside the source code. The website in `website/` pulls it in as content under `/docs/`. This means: - Documentation files are readable on GitHub with working relative links - The same files render as pages on zorto.dev with clean URLs - No content duplication between the repo and the website ## Multiple content directories You can define multiple `[[content_dirs]]` entries: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" rewrite_links = true [[content_dirs]] path = "../api-docs" url_prefix = "api" template = "api-doc.html" sort_by = "title" ``` Each directory is independent with its own URL prefix, templates, and settings. ## Further reading - [Organize content](/docs/how-to/organize-content/) — sections, nested sections, and content structure - [Configuration reference](/docs/reference/config/) — complete `config.toml` reference - [Content model](/docs/concepts/content-model/) — sections and pages ## Presentations Zorto builds slide decks from markdown: one file per slide, ordered by frontmatter, rendered by a presentation template. The template can be native HTML/CSS/JS, reveal.js, or a custom deck runtime. This follows Zorto's core principle: one markdown file per page. ## The model A presentation is a section with `render_pages = false`. Each markdown file in the section becomes a slide. The section's `_index.md` controls presentation-level settings, and the slides are assembled into a single HTML file using a custom template. <div class="cv-visual cv-visual--center"> <div class="cv-tree"> <div class="cv-tree__line"><span class="cv-tree__icon">📂</span><span class="cv-tree__name cv-tree__name--section">content/presentations/intro/</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name cv-tree__name--section">_index.md</span><span class="cv-tree__tag cv-tree__tag--url">template=presentation.html, sort_by=weight</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">title.md</span><span class="cv-tree__tag cv-tree__tag--url">weight=10, layout=center</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">overview.md</span><span class="cv-tree__tag cv-tree__tag--url">weight=20</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">features.md</span><span class="cv-tree__tag cv-tree__tag--url">weight=30, background_color=#16213e</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">demo.md</span><span class="cv-tree__tag cv-tree__tag--url">weight=40</span></div> <div class="cv-tree__line"><span class="cv-tree__prefix">├── </span><span class="cv-tree__icon">📝</span><span class="cv-tree__name">closing.md</span><span class="cv-tree__tag cv-tree__tag--url">weight=50, layout=center</span></div> </div> <p class="cv-caption">Each slide is an independent markdown file. AI agents can create, edit, or reorder slides individually.</p> </div> ## Why one file per slide Traditional presentation tools (including Quarto) put all slides in a single file separated by headings or horizontal rules. Zorto takes a different approach: - **AI-friendly**: an agent can create, modify, or reorder a single slide without parsing the entire presentation - **Frontmatter per slide**: each slide declares layout, background, and any template-specific settings - **Weight-based ordering**: the `weight` field controls slide order; increment by 10 to leave room for insertions - **Git-friendly**: diffs show exactly which slides changed ## Key features **Layouts**: predefined layouts via `[extra] layout`: `center`, `image-left`, `image-right`, `image-full`, `title`. Or use the default flow layout. **Backgrounds**: set `background_color`, `background_image`, `background_size`, and `background_opacity` in `[extra]` to control per-slide backgrounds. **Template control**: section and slide frontmatter are available to the template, so a site can define its own deck controls, visual treatment, and keyboard behavior. **Progressive reveal**: use the `fragment` shortcode to reveal content incrementally on click. **Speaker notes**: use the `speaker_notes` shortcode to keep notes beside the slide source. Whether notes render in a speaker view depends on the deck template. **Multi-column layouts**: use the `columns` shortcode to split content side-by-side. **Positioned images**: use the `slide_image` shortcode for absolute-positioned images anywhere on a slide. ## The `render_pages` field When a section sets `render_pages = false`, its child pages are rendered to HTML (so their content is available in `section.pages`) but do not produce individual HTML output files. They are also excluded from the sitemap, feed, search data, and llms.txt. This is what makes presentations work. ## The `weight` field Pages can have an optional `weight` field in frontmatter. When a section uses `sort_by = "weight"`, pages are sorted ascending by weight. Pages without a weight sort last; ties are broken by filename. This is useful beyond presentations — any section that needs explicit ordering. ## Further reading - [How to create a presentation](/docs/how-to/create-presentation/): step-by-step guide - [Shortcodes](/docs/concepts/shortcodes/): all built-in shortcodes including presentation shortcodes - [Content model](/docs/concepts/content-model/): sections, pages, and frontmatter ## Optimize builds Speed up builds for large sites and control what Zorto generates. ## Skip executable code blocks with `--no-exec` Executable code blocks run during every build by default. For sites with many code blocks, this can be slow. Skip execution with the `--no-exec` flag: ```bash zorto --no-exec build zorto --no-exec preview ``` `--no-exec` is a global flag (before the subcommand). Code blocks render as static syntax-highlighted code without execution output. This is useful for: - Fast iteration on templates and styles - Building on CI when code execution is not needed - Working with untrusted content ## Work with drafts Pages with `draft = true` in frontmatter are excluded from production builds: ```toml +++ title = "Work in progress" draft = true +++ ``` Include drafts during development with the `--drafts` flag: ```bash zorto build --drafts zorto preview --drafts ``` Drafts are excluded from sitemaps, feeds, `llms.txt`, and search data even when included in a preview build. Use drafts for content that is not ready for publication — remove the `draft = true` line when the page is ready. ## Compile all theme stylesheets By default, Zorto compiles only the active theme's SCSS. Enable `compile_all_themes` to compile CSS for every built-in theme: ```toml # config.toml compile_all_themes = true ``` This generates `style-{name}.css` for each theme (e.g., `style-zorto.css`, `style-ember.css`) alongside the main `style.css`. Useful for: - Theme preview or switcher pages - Letting users choose a theme client-side For most sites, leave this disabled — it adds build time without benefit. ## Cache executable code block results If your site uses executable code blocks extensively, enable caching to avoid re-running unchanged blocks: ```toml # config.toml [cache] enable = true ``` When enabled, Zorto caches code block output and reuses it if the code has not changed. This significantly speeds up rebuilds for sites with many executable code blocks. ## Large site considerations ### Content organization For sites with hundreds of pages, structure content into sections. Zorto processes content in parallel where possible, so deeply nested structures do not create bottlenecks. ### SCSS compilation Zorto compiles SCSS to CSS on every build. Keep stylesheets modular — import only what you need. Avoid importing large external CSS frameworks directly into your SCSS; place them in `static/` instead and load them via the `extra_head` block. ### External content directories When using `content_dirs` to pull in external content, be aware that each directory is walked and parsed on every build. For very large external directories, use the `exclude` field to skip files that should not be processed: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" exclude = ["internal.md", "draft-notes.md"] ``` ### Sitemap and feed generation Sitemap and feed generation scale linearly with the number of pages. For very large sites (thousands of pages), you can disable them if not needed: ```toml generate_sitemap = false generate_feed = false generate_llms_txt = false ``` ### Search data DuckDB-backed search ships a `.ddb` file containing searchable page content. For large sites, keep the `search_pages` table lean and avoid indexing private or generated content that visitors do not need. ## Build output summary After a successful build, Zorto reports timing and output statistics. Use this to identify slow builds: ```bash zorto build # Built 142 pages in 0.8s ``` If builds are slow, try: 1. `--no-exec` to skip code execution (usually the biggest time cost) 2. `[cache] enable = true` if you must keep execution 3. Disable unused generators (`generate_feed`, `generate_sitemap`, etc.) 4. Use `exclude` in `content_dirs` to skip unnecessary files ## Related guides - [CLI reference](/docs/reference/cli/) — all flags and subcommands - [Configuration reference](/docs/reference/config/) — complete config.toml options - [Executable code blocks](/docs/concepts/executable-code/) — how code execution works - [Deploy your site](/docs/how-to/deploy/) — production build and hosting ## Advanced templating Go beyond basic templates with macros, block inheritance, loops, pagination, and dynamic data from `config.extra`. ## Block inheritance Every Zorto template system starts with a base layout. Child templates extend it and override specific blocks. **base.html** defines the skeleton: ```html <html> <head> <title>{% block title %}{{ config.title }}{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
``` **page.html** overrides only what it needs: ```html {% extends "base.html" %} {% block title %}{{ page.title }} | {{ config.title }}{% endblock %} {% block content %}

{{ page.title }}

{{ page.content | safe }}
{% endblock %} ``` Blocks not overridden in the child template use the parent's default content. You can nest extends multiple levels deep (e.g. `page.html` extends `base.html`, `post.html` extends `page.html`). ### Calling the parent block Use `super()` to include the parent block's content alongside your additions:
{% block footer %}
  {{ super() }}
  <p>Extra footer content</p>
{% endblock %}
## Macros Macros are reusable template fragments with parameters. Define them in any template file and import them where needed. ### Define a macro
{% macro card(title, url, description="") %}
<a href="{{ url }}" class="card">
  <h3>{{ title }}</h3>
  {% if description %}
    <p>{{ description }}</p>
  {% endif %}
</a>
{% endmacro %}
### Use a macro in the same file
{{ self::card(title="Home", url="/") }}
{{ self::card(title="About", url="/about/", description="Learn more") }}
### Import macros from another file Create `templates/macros.html` with your macro definitions, then import:
{% import "macros.html" as macros %}

{{ macros::card(title="Home", url="/") }}
## For loops Iterate over arrays like `section.pages`, taxonomy terms, or any array value. ### Basic loop
{% for page in section.pages %}
  <article>
    <h2><a href="{{ page.permalink }}">{{ page.title }}</a></h2>
    {% if page.date %}
      <time>{{ page.date | date(format="%B %d, %Y") }}</time>
    {% endif %}
  </article>
{% endfor %}
### Loop variables Tera provides these variables inside `for` loops: | Variable | Description | |----------|-------------| | `loop.index` | Current iteration (1-based) | | `loop.index0` | Current iteration (0-based) | | `loop.first` | `true` on first iteration | | `loop.last` | `true` on last iteration |
{% for page in section.pages %}
  <div class="{% if loop.first %}featured{% endif %}">
    {{ page.title }}
  </div>
{% endfor %}
### Limiting results Use the `slice` filter to show only a subset:
{% for page in section.pages | slice(end=3) %}
  <!-- Only the first 3 pages -->
{% endfor %}
## Pagination Sections with `paginate_by` set in their frontmatter provide a `paginator` object in the template context. ### Section frontmatter ```toml +++ title = "Blog" sort_by = "date" paginate_by = 10 +++ ``` ### Paginator fields | Field | Type | Description | |-------|------|-------------| | `paginator.pages` | array | Pages for the current pagination page | | `paginator.current_index` | int | Current page number (1-based) | | `paginator.number_pagers` | int | Total number of pagination pages | | `paginator.previous` | string or null | URL of the previous page | | `paginator.next` | string or null | URL of the next page | | `paginator.first` | string | URL of the first page | | `paginator.last` | string | URL of the last page | ### Pagination template
{% for page in paginator.pages %}
  <article>
    <h2><a href="{{ page.permalink }}">{{ page.title }}</a></h2>
  </article>
{% endfor %}

<nav class="pagination">
  {% if paginator.previous %}
    <a href="{{ paginator.previous }}">Previous</a>
  {% endif %}

  <span>Page {{ paginator.current_index }} of {{ paginator.number_pagers }}</span>

  {% if paginator.next %}
    <a href="{{ paginator.next }}">Next</a>
  {% endif %}
</nav>
## Accessing config.extra The `config` object is available in every template. Custom values from the `[extra]` section of `config.toml` are accessible as `config.extra`. ### Define custom values ```toml # config.toml [extra] author = "Cody" year = 2026 social = { github = "dkdc-io", twitter = "dkdc_io" } menu_items = [ { name = "Home", url = "/" }, { name = "Blog", url = "/posts/" }, ] ``` ### Use in templates
<footer>
  <p>&copy; {{ config.extra.year }} {{ config.extra.author }}</p>

  {% if config.extra.social %}
    <a href="https://github.com/{{ config.extra.social.github }}">GitHub</a>
  {% endif %}
</footer>

<nav>
  {% for item in config.extra.menu_items %}
    <a href="{{ item.url }}">{{ item.name }}</a>
  {% endfor %}
</nav>
### Other config fields Beyond `extra`, these top-level config fields are also available:
{{ config.base_url }}       <!-- "https://example.com" -->
{{ config.title }}          <!-- "My Site" -->
{{ config.description }}    <!-- "Site description" -->
## Conditional taxonomy rendering Render taxonomy values only when they exist on a page:
{% if page.taxonomies.tags %}
<div class="tags">
  {% for tag in page.taxonomies.tags %}
    <a href="{{ get_taxonomy_url(kind="tags", name=tag) }}" class="tag">
      {{ tag }}
    </a>
  {% endfor %}
</div>
{% endif %}

{% if page.taxonomies.categories %}
<div class="categories">
  {% for cat in page.taxonomies.categories %}
    <a href="{{ get_taxonomy_url(kind="categories", name=cat) }}">
      {{ cat }}
    </a>
  {% endfor %}
</div>
{% endif %}
## Conditional content with tests Use Tera's `is` keyword with custom tests:
{% if page.path is starting_with("/docs") %}
  <!-- Show docs sidebar -->
{% endif %}

{% if page.draft %}
  <div class="draft-banner">This is a draft</div>
{% endif %}
## Loading sections dynamically Use `get_section` to pull content from other sections into any template:
{% set recent_posts = get_section(path="posts/_index.md") %}

<h2>Latest posts</h2>
{% for page in recent_posts.pages | slice(end=3) %}
  <a href="{{ page.permalink }}">{{ page.title }}</a>
{% endfor %}
This is commonly used on the homepage (`index.html`) to show recent posts from a section. ## Further reading - [Templates concept](/docs/concepts/templates/) — template hierarchy and context variables - [Template functions and filters](/docs/reference/template-functions/) — complete function and filter reference - [Taxonomies in depth](/docs/reference/taxonomies/) — taxonomy template rendering ## Taxonomies in depth Taxonomies let you classify content with terms like tags, categories, or any custom grouping. Zorto auto-generates listing pages for each taxonomy and its terms. ## Defining taxonomies Taxonomies are declared in `config.toml` with `[[taxonomies]]`: ```toml [[taxonomies]] name = "tags" [[taxonomies]] name = "categories" ``` Each entry requires a `name` field. The name determines the URL path (`/tags/`, `/categories/`) and the frontmatter field name.

Note

If you omit `[[taxonomies]]` entirely, Zorto creates a default `tags` taxonomy. Once you define any `[[taxonomies]]` entry, only the ones you list are active.
## Assigning terms In page frontmatter, add a top-level array field matching the taxonomy name: ```toml +++ title = "Building a web app with Rust" date = "2026-03-15" tags = ["rust", "web", "tutorial"] categories = ["engineering"] +++ ``` Any top-level frontmatter key whose value is an array of strings is treated as a taxonomy assignment. The key must match a taxonomy `name` defined in `config.toml`. ## Auto-generated pages For each taxonomy, Zorto generates two types of pages: ### List page A page at `//` showing all terms. For `tags`, this renders at `/tags/`. **Template:** `/list.html` (e.g. `tags/list.html`) **Context variables:** | Variable | Type | Description | |----------|------|-------------| | `terms` | array of objects | All terms in this taxonomy, sorted alphabetically | | `terms[].name` | string | Original term name (e.g. `"Rust"`) | | `terms[].slug` | string | URL-safe slug (e.g. `"rust"`) | | `terms[].permalink` | string | Full URL to the term page | | `terms[].pages` | array | Pages with this term, sorted by date (newest first) | | `config` | object | Site configuration | ### Term page A page at `///` showing pages with that term. For the tag `"rust"`, this renders at `/tags/rust/`. **Template:** `/single.html` (e.g. `tags/single.html`) **Context variables:** | Variable | Type | Description | |----------|------|-------------| | `term` | object | The current taxonomy term | | `term.name` | string | Original term name | | `term.slug` | string | URL-safe slug | | `term.permalink` | string | Full URL to this term page | | `term.pages` | array | Pages with this term, sorted by date (newest first) | | `config` | object | Site configuration | ## Template examples ### Taxonomy list template Create `templates/tags/list.html` to render the tags index page:
{% extends "base.html" %}

{% block content %}
<h1>Tags</h1>

<ul>
{% for term in terms %}
  <li>
    <a href="{{ term.permalink }}">{{ term.name }}</a>
    ({{ term.pages | length }} post{{ term.pages | length | pluralize }})
  </li>
{% endfor %}
</ul>
{% endblock %}
### Taxonomy single template Create `templates/tags/single.html` to render a page for each tag:
{% extends "base.html" %}

{% block content %}
<h1>Posts tagged "{{ term.name }}"</h1>

{% for page in term.pages %}
<article>
  <h2><a href="{{ page.permalink }}">{{ page.title }}</a></h2>
  {% if page.date %}
    <time>{{ page.date | date(format="%B %d, %Y") }}</time>
  {% endif %}
  {% if page.summary %}
    {{ page.summary | safe }}
  {% endif %}
</article>
{% endfor %}
{% endblock %}
## Linking to taxonomy terms from pages Use `get_taxonomy_url` in any template to generate a link to a term page:
{% if page.taxonomies.tags %}
<div class="tags">
  {% for tag in page.taxonomies.tags %}
    <a href="{{ get_taxonomy_url(kind="tags", name=tag) }}">
      {{ tag }}
    </a>
  {% endfor %}
</div>
{% endif %}
## Template directory structure Taxonomy templates live in a subdirectory named after the taxonomy:
📂templates/
├── 📝base.html
├── 📝page.html
├── 📝section.html
├── 📂tags/
    ├── 📝list.htmlrenders /tags/
    ├── 📝single.htmlrenders /tags/rust/, etc.
├── 📂categories/
    ├── 📝list.htmlrenders /categories/
    ├── 📝single.htmlrenders /categories/engineering/, etc.

Each taxonomy gets its own template directory.

Warning

Taxonomy pages are only rendered if the corresponding template exists. If there is no `tags/list.html`, the `/tags/` page will not be generated.
## Multiple taxonomies You can define as many taxonomies as needed: ```toml [[taxonomies]] name = "tags" [[taxonomies]] name = "categories" [[taxonomies]] name = "authors" ``` Each taxonomy is completely independent. A page can have terms in any or all of them: ```toml +++ title = "Zorto 1.0 release" tags = ["release", "rust"] categories = ["announcements"] authors = ["cody"] +++ ``` ## Term slugification Term names are slugified for URLs: spaces become hyphens, special characters are removed, and the result is lowercased. For example: | Term name | Slug | URL | |-----------|------|-----| | `"Rust"` | `rust` | `/tags/rust/` | | `"My Tag"` | `my-tag` | `/tags/my-tag/` | | `"C++"` | `c` | `/tags/c/` | Pages within each term are sorted by date in reverse chronological order (newest first). Terms in the list page are sorted alphabetically by name. ## Further reading - [Frontmatter reference](/docs/reference/frontmatter/) — how to assign taxonomy values in frontmatter - [Template functions and filters](/docs/reference/template-functions/) — `get_taxonomy_url` function reference - [Templates concept](/docs/concepts/templates/) — template hierarchy and context variables ## Search Zorto search is DuckDB-backed. A site ships a public `.ddb` file, the browser loads DuckDB-Wasm on demand, and the search modal queries a `search_pages` table locally. ## How it works
Buildyour pipeline writes site.ddb
Shipsite.ddb is copied as a static asset
Loadbrowser fetches site.ddb + DuckDB-Wasm
QueryCtrl+K runs SQL against search_pages
Displayranked results render in the modal

Search is a small data app: static files, browser SQL, no search server.

### Data artifact The current zorto.dev pattern is a checked-in `static/data/site.ddb` generated by a self-contained `uv` script. The database includes a `search_pages` table: | Column | Source | |--------|--------| | `title` | Page or section title | | `url` | Public URL | | `description` | Frontmatter `description` field | | `content` | Markdown content stripped to searchable text | | `title_lower` | Precomputed lowercase title | | `description_lower` | Precomputed lowercase description | | `content_lower` | Precomputed lowercase content | ### Browser runtime The built-in theme shows the search button when `config.extra.search_database_url` is set. On first use, it imports DuckDB-Wasm, fetches the configured `.ddb`, attaches it read-only, and queries `search_pages`. ### Ranking Results are scored by where the match occurs: | Match type | Points | |------------|--------| | Exact title match | 100 | | Title starts with query | 80 | | Title contains query | 60 | | Description contains query | 20 | | Content contains query | 10 | Scores are additive. Results are ordered by score, then title, and limited to the top 10. ## The search UI The search modal opens with **Ctrl+K** or **Cmd+K**. It includes: - A one-character minimum query with debounce - Keyboard navigation: arrow keys to move, Enter to open, Escape to close - Result snippets from description or content with highlighted matches - Mobile support via a search button in the header ## Further reading - [Add search to your site](/docs/how-to/add-search/): configure the DuckDB-backed search modal - [Data apps](/docs/concepts/data-apps/): the broader `.ddb` application model ## Build a docs site Create a documentation site with organized sections, sidebar navigation, and optional external content sources using `content_dirs`.
📂my-docs/
├── 📝config.tomlsite config
├── 📂content/
    ├── 📝_index.mdsection: homepage
    ├── 📂guide/
        ├── 📝_index.mdsection: getting started
        ├── 📝installation.mdpage
        ├── 📝configuration.mdpage
    ├── 📂reference/
        ├── 📝_index.mdsection: API reference
        ├── 📝cli.mdpage
        ├── 📝config.mdpage
├── 📂templates/optional overrides
├── 📂static/images, fonts

What your docs site will look like after this guide.

## Initialize the project Scaffold a new docs site with the `docs` template: ```bash zorto init --template docs my-docs cd my-docs ``` This creates a project with a `guide/` section and example pages sorted by title. Start the dev server to see it: ```bash zorto preview --open ``` ## Add sections Each section is a directory with an `_index.md` file. Add a reference section: ```bash mkdir -p content/reference ``` Create `content/reference/_index.md`: ```toml +++ title = "Reference" sort_by = "title" +++ ``` Then add pages inside it. Create `content/reference/cli.md`: ```toml +++ title = "CLI reference" +++ ``` ```markdown ## Commands | Command | Description | |---------|-------------| | `build` | Build the site to `public/` | | `preview` | Start dev server with live reload | | `check` | Validate links and config | | `clean` | Remove build output | ``` Repeat for each page in the section. Zorto generates URLs from the file structure: `content/reference/cli.md` becomes `/reference/cli/`. ## Nest sections Sections can nest to any depth. Create subsections by adding directories with their own `_index.md`: ```bash mkdir -p content/guide/advanced ``` Create `content/guide/advanced/_index.md`: ```toml +++ title = "Advanced" sort_by = "title" +++ ``` Pages inside `content/guide/advanced/` appear under `/guide/advanced/`. Access subsections in templates via `section.subsections`.
📂content/guide/
├── 📝_index.mdsection: /guide/
├── 📝installation.mdpage: /guide/installation/
├── 📂advanced/
    ├── 📝_index.mdsection: /guide/advanced/
    ├── 📝caching.mdpage: /guide/advanced/caching/

Nested sections create a hierarchical URL structure.

## Sort pages Control page order with `sort_by` in the section's `_index.md`: - `sort_by = "title"` — alphabetical by title (good for reference docs) - `sort_by = "date"` — newest first (good for changelogs) For manual ordering, prefix filenames with numbers: `01-installation.md`, `02-configuration.md`. Zorto includes the full filename in the title slug, so the numbers appear in URLs too. To keep clean URLs, set a custom `slug` in each page's frontmatter: ```toml +++ title = "Installation" slug = "installation" +++ ``` ## Use custom templates Assign a custom template to your docs sections for a different layout than the rest of your site: ```toml # content/guide/_index.md +++ title = "Guide" sort_by = "title" template = "docs-section.html" +++ ``` Individual pages can also use custom templates: ```toml # content/reference/cli.md +++ title = "CLI reference" template = "docs-page.html" +++ ``` Create the corresponding template files in your `templates/` directory. They extend `base.html` like any other template. ## Pull in external docs with content_dirs If your documentation lives outside the site directory (for example, alongside source code in a library repo), use `content_dirs` to pull it in without copying files: ```toml # config.toml [[content_dirs]] path = "../docs" url_prefix = "docs" template = "docs.html" section_template = "docs-section.html" sort_by = "title" rewrite_links = true ``` | Field | Description | |-------|-------------| | `path` | Relative path to the external directory | | `url_prefix` | URL prefix for all generated pages (e.g. `"docs"` produces `/docs/...`) | | `template` | Template for pages from this directory | | `section_template` | Template for sections from this directory | | `sort_by` | Sort order: `"title"` or `"date"` | | `rewrite_links` | Rewrite relative `.md` links to clean URL paths | With this config, a file at `../docs/guide/installation.md` becomes available at `/docs/guide/installation/`. Zorto treats the external directory as if it were inside `content/` — sections, pages, frontmatter, and links all work normally. ### Exclude files If some external files overlap with manually written content, exclude them: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" exclude = ["reference/cli.md"] ``` The excluded file is skipped during content loading. You can then provide your own version at `content/docs/reference/cli.md`. ### Multiple content directories You can define multiple `content_dirs` entries to pull from several sources: ```toml [[content_dirs]] path = "../docs" url_prefix = "docs" sort_by = "title" rewrite_links = true [[content_dirs]] path = "../api-docs" url_prefix = "api" sort_by = "title" rewrite_links = true ``` Each entry is independent — different URL prefixes, templates, and sort orders. ## Add navigation Configure top-level navigation in `config.toml` using `menu_items` under `[extra]`: ```toml [extra] menu_items = [ { name = "Guide", url = "/guide/" }, { name = "Reference", url = "/reference/" }, ] ``` If using `content_dirs` with a URL prefix, adjust the URLs accordingly: ```toml [extra] menu_items = [ { name = "Guide", url = "/docs/guide/" }, { name = "Reference", url = "/docs/reference/" }, ] ``` Built-in themes render `menu_items` as a navigation bar. For sidebar navigation within a section, theme templates use `section.subsections` and `section.pages` to build the sidebar automatically. ## Enable anchor links Add clickable anchor links to headings so readers can link to specific sections: ```toml # config.toml [markdown] insert_anchor_links = "right" ``` Options: `"right"` or `"none"` (default). ## Build and deploy Build the site for production: ```bash zorto build ``` The output goes to `public/`. Deploy it to any static hosting provider — see [Deploy your site](/docs/how-to/deploy/) for platform-specific instructions. > [!TIP] > Run `zorto check` before deploying to catch broken internal links and configuration issues. ## Related guides - [Content model](/docs/concepts/content-model/) — sections, pages, and frontmatter in depth - [Configuration reference](/docs/reference/config/) — full `content_dirs` and config field reference - [Organize content](/docs/how-to/organize-content/) — section nesting and external content directories - [Customize navigation and footer](/docs/how-to/customize-nav-footer/) — menus, logo, social links ## Frontmatter reference Complete specification for TOML frontmatter in Zorto content files. Frontmatter is enclosed in `+++` delimiters at the top of every `.md` file. ```toml +++ title = "My page" date = "2026-01-15" +++ ``` If no `+++` block is present, Zorto uses default values for all fields. ## Page frontmatter Used in regular `.md` files (anything that is not `_index.md`). | Field | Type | Default | Description | |-------|------|---------|-------------| | `title` | string | `""` | Page title, used in templates and feeds | | `date` | string or datetime | *none* | Publication date (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`). Pages without dates sort last in date-ordered sections | | `author` | string | *none* | Author name | | `description` | string | *none* | Short summary for SEO, feeds, and `llms.txt` | | `draft` | bool | `false` | If `true`, excluded from production builds | | `slug` | string | filename | Override the URL slug. By default, derived from the filename (e.g. `my-post.md` becomes `my-post`) | | `template` | string | `"page.html"` | Custom template for this page | | `weight` | int | *none* | Sort weight for ordering within a section (lower values first). Used with `sort_by = "weight"` | | `aliases` | array of strings | `[]` | Additional URL paths that redirect to this page | | `[extra]` | table | `{}` | Arbitrary key-value data, accessible in templates as `page.extra` | | taxonomy fields | array of strings | `[]` | Top-level arrays are interpreted as taxonomy values (e.g. `tags = ["rust"]`) | ### Computed fields These fields are not set in frontmatter but are available in templates: | Field | Type | Description | |-------|------|-------------| | `page.path` | string | URL path relative to site root (e.g. `"/posts/hello/"`) | | `page.permalink` | string | Full URL including `base_url` | | `page.content` | string | Rendered HTML content | | `page.summary` | string or null | HTML content before `` marker | | `page.raw_content` | string | Raw markdown after frontmatter extraction | | `page.taxonomies` | object | Taxonomy values keyed by name (e.g. `{"tags": ["rust", "web"]}`) | | `page.word_count` | int | Approximate word count | | `page.reading_time` | int | Estimated reading time in minutes (word_count / 200, minimum 1) | | `page.relative_path` | string | Source file path relative to content directory | ## Section frontmatter Used in `_index.md` files that define sections (directories). | Field | Type | Default | Description | |-------|------|---------|-------------| | `title` | string | `""` | Section title | | `description` | string | *none* | Section description | | `sort_by` | string | *none* | Sort pages: `"date"` (reverse chronological), `"title"` (alphabetical), or `"weight"` (ascending by weight, filename tiebreak) | | `paginate_by` | int | *none* | Pages per pagination page. Omit or set to `0` to disable pagination | | `render_pages` | bool | `true` | When `false`, child pages are not rendered as individual HTML files. Their content is still available in `section.pages` for use in templates. Used for [presentations](/docs/concepts/presentations/) | | `template` | string | `"section.html"` | Custom template for this section | | `[extra]` | table | `{}` | Arbitrary key-value data, accessible in templates as `section.extra` | ### Computed fields | Field | Type | Description | |-------|------|-------------| | `section.path` | string | URL path relative to site root (e.g. `"/posts/"`) | | `section.permalink` | string | Full URL including `base_url` | | `section.content` | string | Rendered HTML from the `_index.md` body | | `section.raw_content` | string | Raw markdown after frontmatter extraction | | `section.pages` | array | Pages belonging to this section, sorted per `sort_by` | | `section.relative_path` | string | Source file path relative to content directory | ## Taxonomy values Taxonomy values are defined as top-level arrays in page frontmatter. Any top-level key whose value is an array of strings is treated as a taxonomy assignment: ```toml +++ title = "Building a web app" tags = ["rust", "web", "tutorial"] categories = ["engineering"] +++ ``` The taxonomy names must match those defined in `config.toml` under `[[taxonomies]]`. See [taxonomies in depth](/docs/reference/taxonomies/) for the full workflow. ## Slug derivation The URL slug determines the page's URL path. Zorto derives it in this order: 1. **Explicit `slug` field** in frontmatter, if set 2. **Directory name** for co-located content (`posts/my-post/index.md` uses slug `my-post`) 3. **Filename** for regular files (`my-post.md` uses slug `my-post`) Slugs are always lowercased and URL-safe (spaces become hyphens, special characters are removed). ## Co-located content Pages can use directory-based organization for co-located assets:
📂content/posts/
├── 📂my-post/
    ├── 📝index.mdpage -> /posts/my-post/
    ├── 📝photo.jpg
    ├── 📝diagram.svg

Co-located content uses index.md inside a named directory.

The `index.md` file becomes the page content, and sibling files are copied as assets. Reference them with relative paths in your markdown. ## Date formats The `date` field accepts: - **Date string**: `"2026-01-15"` - **Datetime string**: `"2026-01-15T10:30:00"` - **TOML datetime**: `2026-01-15T10:30:00` (unquoted) All formats are normalized to a string for template rendering. ## Examples ### Minimal page ```toml +++ title = "About" +++ ``` ### Full page ```toml +++ title = "Building a static site generator" date = "2026-03-15" author = "Cody" description = "How and why I built Zorto." draft = false slug = "building-zorto" template = "post.html" aliases = ["/blog/old-url/"] tags = ["rust", "ssg"] categories = ["engineering"] [extra] featured = true hero_image = "/images/zorto-hero.png" +++ ``` ### Section with pagination ```toml +++ title = "Blog" description = "Posts about engineering and design." sort_by = "date" paginate_by = 10 template = "blog.html" [extra] show_sidebar = true +++ ``` ## Further reading - [Content model](/docs/concepts/content-model/) — sections, pages, and internal links - [Configuration reference](/docs/reference/config/) — site-level settings - [Templates](/docs/concepts/templates/) — how frontmatter fields are used in templates - [Taxonomies in depth](/docs/reference/taxonomies/) — defining and assigning taxonomy terms ## Blog, events, and more Zorto does not have a special "blog" feature. Instead, its section system naturally handles any date-ordered content: blog posts, changelogs, event listings, release notes, newsletters. If it has a date and belongs in a list, it is a section. ## The pattern A section is a directory with an `_index.md` file. Every markdown file in that directory becomes a page in the section. The section's frontmatter controls sorting and pagination, while each page's frontmatter provides its metadata.
📂content/posts/
├── 📝_index.mdsection: sort_by=date, paginate_by=10
├── 📝first-post.mdpage with date, title, tags
├── 📝second-post.mdpage
├── 📂announcing-v2/
    ├── 📝index.mdpage with co-located assets
    ├── 📝screenshot.png

A blog is just a section. No special blog feature — just the content model.

This is the same content model used everywhere in Zorto — there is no blog-specific configuration or special directory name. A section called `posts/`, `blog/`, `news/`, or `changelog/` all work identically. ## Consequence: one model for everything Because blogs are just sections, everything you learn about the content model applies directly. Sorting, pagination, taxonomies, co-located assets, internal links, custom templates — they all work the same way whether you are building a blog, a docs site, or both. This also means you can have multiple blog-like sections on one site. A `/posts/` for articles and a `/changelog/` for release notes, each with their own sorting and pagination, coexist naturally. ## Taxonomies tie sections together Tags and categories work across sections. Define taxonomies in your config, assign terms in page frontmatter, and Zorto generates listing pages for each term automatically. A tag page at `/tags/rust/` lists every page tagged "rust" regardless of which section it lives in. ## Summaries Use `` in a post to mark where the summary ends. Everything above it appears on listing pages; the full content appears on the post's own page. ## Feeds When `generate_feed = true` in your config, Zorto generates an Atom feed. Pages need a `date` in their frontmatter to appear in the feed. Pages without dates are silently excluded. ## Further reading - [Content model](/docs/concepts/content-model/) — sections and pages in depth - [Configuration](/docs/concepts/configuration/) — how taxonomies and feed generation are configured - [How to add a blog](/docs/how-to/add-blog/) — step-by-step setup