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:

+++
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"
+++
FieldTypeDescription
titlestringPage title (required)
datestringPublication date (YYYY-MM-DD)
authorstringAuthor name
descriptionstringSummary for SEO and feeds
draftboolIf true, excluded from builds (default: false)
slugstringOverride the URL slug
templatestringUse a custom template
aliasesarray of stringsRedirect old URLs to this page
sort_bystringSort child pages: "date" (newest first) or "title" (sections only)
paginate_byintNumber of items per page, 0 = no pagination (sections only)
taxonomy fieldsarray of stringsTaxonomy values as top-level arrays (e.g. tags = ["rust", "ssg"])
[extra]tableArbitrary custom data, accessible in templates

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 pathURL pathPermalink (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:

SourceSlug
My First Post.mdmy-first-post
HΓ©llo WΓΆrld.mdhello-world
posts/my-post/index.mdmy-post (from the directory name)

Override the slug in frontmatter to decouple the URL from the filename:

+++
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.

Link to other content files using the @/ prefix:

[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:

[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 <!-- more --> in a page’s body to mark where the summary ends:

This is the summary shown on listing pages.

<!-- more -->

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 <!-- more --> 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:

![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.

Further reading #