Authoring
What to put at the top of a file, and what each keyword buys you.
URLs
By default a source path becomes the matching output path: blog/post.org → blog/post.html.
#+SLUG: overrides the filename, never the directory:
#+TITLE: AES Encryption
#+SLUG: aes-encryption
blog/2018-11-28-aes-encryption.org now publishes at blog/aes-encryption.html. This is how a date-prefixed filename — useful for sorting in a file manager — becomes a clean address.
Slugs are reduced to a single safe path component, so a slug cannot escape the output directory however it was written. Two pages claiming one URL is a build error rather than one silently overwriting the other.
Links follow slugs automatically: [[file:blog/2018-11-28-aes-encryption.org]] resolves to blog/aes-encryption.html.
Drafts
#+DRAFT: t
The page is not written at all, and is absent from listings, tag pages and navigation — not merely unlinked.
It is also out of the symbol table, so a link to a draft is reported as a broken link. That is deliberate: it is what that link would be on the published site, and better found now than by a reader.
The keyword is read forgivingly. t, yes, 1 and a bare #+DRAFT: all mean draft, because writing the keyword at all is the signal. Only an explicit nil, false, no, 0 or off means published — publishing someone's unfinished post because they typed yes instead of t is the wrong way to be strict.
Dates
#+DATE: <2026-02-02 Mon>
#+DATE: [2025-09-05 Fri 10:21:00]
#+DATE: 2024-05-01
All three work. page.date keeps what you wrote, and page.date_iso is the YYYY-MM-DD inside it — the value listings sort on and templates usually print.
A page with no parseable date sorts last in a dated listing, in either direction, so a draft with no date never leads an archive.
Excerpts
page.excerpt is #+DESCRIPTION: when the page sets one, and its first paragraph otherwise:
#+DESCRIPTION: How the borrow checker thinks about lifetimes.
The fallback matters more than the keyword: it means a listing has something to show whether or not the author ever thought about summaries. Use truncate in the template to cut a long paragraph to size.
Reading time
page.word_count and page.reading_time (minutes at 200 wpm, rounded up) count prose only. Source and example blocks are excluded, because a post that is mostly a shell transcript should not read as an hour's work. #+TITLE: is metadata rendered as chrome, so it is not counted either.
Tags
#+FILETAGS: :rust:web:
Available as page.tags, and the input to tag pages — see Collections.
Table of contents
Every page's heading tree is available as page.toc without any markup in the file. Turn it off for one document the way org already does:
#+OPTIONS: toc:nil
Or site-wide with [html] toc = false. Rendering it is the template's business; see Templates.
Section numbers
Off by default, unlike Emacs. Turn them on for one document:
#+OPTIONS: num:t
Or site-wide with [html] section_numbers = true.
Your own metadata
Every #+KEYWORD: reaches templates under its lowercased name:
#+SUBTITLE: A closer look
#+REVIEWED_BY: someone
{% if page.keywords.subtitle %}{{ page.keywords.subtitle }}{% endif %}
Nothing needs to be registered, and orgo needs no release to support a keyword you invented.
Assets
Any non-.org file in the source directory is copied to the output, preserving layout: content/img/diagram.png → _site/img/diagram.png. Reference it from a page with an ordinary relative link, and from a template with {{ root }}img/diagram.png.
Four things are never published:
- Dot-entries such as
.gitand.env. A source directory is often a repository, and publishing its history next to the homepage is a real way to leak a project. orgo.toml, which is a build input.- The templates directory, likewise.
- The output directory, when it lives inside the source — so
orgo build . -o _sitedoes the obvious thing rather than copying its own output back into itself.
Note that excluding dot-entries also means .well-known/ cannot be published.