orgo

Configuration

All of it optional. A missing config is a valid config.

orgo looks for orgo.toml in the source directory. Pass a different path with --config. Every field has a default, so a directory of org files with no config still builds a complete site.

A missing config is normal and silent. A malformed one is an error, and an unknown key is rejected by name — a misspelled setting that silently does nothing is how people lose an afternoon.

The whole file

[site]
title = "orgo site"
base_url = ""
description = ""
language = "en"
theme = ""

[nav]
mode = "top-level"
# pages = ["index.org", "about.org"]

[templates]
dir = "templates"
expose_page_list = false

[highlight]
theme = "InspiredGitHub"
theme_dark = ""
syntaxes_dir = "syntaxes"

[build]
drafts = false
assets = []
sitemap = true

[html]
heading_offset = 1
toc = true
section_numbers = false

Plus any number of [[collections]] blocks, documented in Collections.

[site]

KeyDefaultMeaning
title"orgo site"Site name. Available as {{ site.title }}.
base_url=""Absolute origin, no trailing slash.
description=""Available as {{ site.description }}.
language"en"Goes in <html lang> in the built-in layout.
theme=""A built-in stylesheet, written to the output as theme.css.

theme

Four themes are compiled into the binary. Name one and each build writes it to the output root as theme.css, which the built-in layout and the templates orgo init writes both link.

ThemeShapeFor
"plain"Narrow, system fonts, hairline rules.Readable defaults to build your own CSS on.
"blog"Serif prose, a centred masthead, styled post lists.Dated writing.
"wiki"Wide and dense, contents in the margin, TODO states as badges.Notes, a reference site.
"docs"Narrow, a contents card, quote blocks as notes, #+LEDE:.A guide read in order.

All four follow prefers-color-scheme, so a site gets a dark mode without a toggle, a setting or a line of JavaScript — and all four reflow from a 320px phone up, with tables and code blocks scrolling inside their own box rather than widening the page.

The default is empty: no stylesheet is written and no page links one, so the output is unstyled HTML. That is deliberate — a site that already ships CSS of its own should not find a second stylesheet competing with it, and upgrading orgo should never restyle a site. An unknown name is an error listing the four.

A theme styles the markup orgo already emits — headings, tags, TODO keywords, checkbox lists, footnotes, tables — plus the chrome the built-in layout puts around it. There is no theme-specific HTML, so switching or removing one touches no template.

Every colour is a custom property on :root, named --orgo-*. To adjust rather than replace a theme, ship a stylesheet of your own as an asset, link it after theme.css, and redefine the handful you care about:

:root {
  --orgo-accent: #7a1fa2;
  --orgo-measure: 46rem;
}

Code blocks are the one part a built-in theme leaves light in dark mode: syntax.css is coloured by highlight.theme, and that default is a light theme. Two pieces make a block follow prefers-color-schemehighlight.theme_dark for the tokens, and --orgo-code-bg, --orgo-code-fg and --orgo-code-rule for the surface under them. Set both, or neither and blocks stay light in both schemes. Those three properties colour blocks only — inline ~code~ follows the page's own scheme, so it stays legible whichever highlight theme you use. The documentation site keeps a dark surface in both schemes instead; its style.css is those three lines and nothing else.

When you outgrow a theme, drop theme from the config and write templates/base.html against your own CSS. Nothing else changes.

baseurl

Leave it empty and the site is built entirely with relative URLs, which means it works from a subdirectory, from a filesystem path, and from any origin. That portability is why it is the default.

Set it when you need absolute URLs, which two things require: feeds, because a feed is read away from the site that served it, and canonical links. The absolute template filter turns a site-root-relative path into a full URL, and errors if there is no base URL to build one from — rather than quietly emitting a relative URL that would make the feed invalid everywhere while looking fine.

A trailing slash is rejected, because https://example.com/ plus blog/x.html is https://example.com//blog/x.html.

The navigation shared by every page.

modeIncludes
"top-level" (default)Pages at the site root.
"all"Every page.
"explicit"Only nav.pages, in the order listed.
"none"Nothing.

"all" makes output quadratic. Each of n pages carries n links, so total output grows with the square of the site. On a 1,790-page site that was 284 MB of mostly navigation. It is fine for a handful of pages and a trap beyond that.

"top-level" keeps the nav a map of the site's top level rather than an index of its contents, so nav size does not depend on how much you write.

explicit

[nav]
mode = "explicit"
pages = ["index.org", "about.org", "uses.org"]

Paths are source paths relative to the source root, and the order given is the order rendered — a hand-written nav is a designed sequence, not an alphabetical one. Naming a page that does not exist is an error, because a silently shorter nav is a poor way to learn about a typo.

Section landing pages in the nav

If your sections live in subdirectories, none of them are top-level pages. Put the section's generated index in the nav instead, with nav = true on its collection — that is the page a nav entry should point at anyway.

A generated page has no source file, so name it in pages by its output path:

[nav]
mode = "explicit"
pages = ["blog/index.html", "garden/index.html", "about.org"]

That is the only way to interleave the two: a collection that sets nav = true without being listed is appended after everything you did list, so "about.org" alone would put About first and the sections after it. Listing all of them puts each exactly where you said. Either spelling works for an authored page too — its source path or its output path — though the source path is the one that survives a #+SLUG:.

pages

Which layout a page renders through. Without any of these, every authored page uses base.html.

[[pages]]
match = "blog"
template = "post.html"

match is a source path relative to the source root — a directory, covering every page beneath it however deep, or one .org file. It is matched by path component, so blog covers blog/2026/post.org and does not touch blogroll.org.

A section's layout is a property of the section, which is why this is a rule and not something you write in each file: a blog post carries the same byline and reply footer as every other one, and repeating that in 200 files means maintaining one fact 200 times.

Which rule wins

Most specific, by path depth — blog/notes beats blog, whatever order they appear in. An empty match covers the whole site, which is how you rename the default layout.

A page that differs from its section says so itself, and that wins over any rule:

#+TITLE: Colophon
#+TEMPLATE: wide.html

Naming a template that is not in the templates directory is an error that names the page, the template and what does exist — a layout typo should not be a hunt.

[templates]

KeyDefaultMeaning
dir"templates"Directory of templates, relative to the source root.
expose_page_listfalseGive every template a pages list of all page metadata.

exposepagelist costs incremental precision

With it on, any page can read every page's metadata — so adding one page can change any page's output, and the whole site must re-render on every add, rename or retitle. That is the trade for building an index by hand in a template. Most people want a collection instead, which gets the same result while keeping adding a post a one-page rebuild.

[highlight]

KeyDefaultMeaning
theme"InspiredGitHub"A syntect theme name.
theme_dark""A second theme for readers in dark mode.
syntaxes_dir"syntaxes"Extra .sublime-syntax files.

Any theme syntect ships: InspiredGitHub, Solarized (dark), Solarized (light), base16-ocean.dark, base16-ocean.light, base16-eighties.dark, base16-mocha.dark. An unknown name is an error listing the valid ones.

theme_dark is off by default, and one theme colours every reader. Name a second one and syntax.css carries both, each behind the prefers-color-scheme query it belongs to, so a reader's system picks the colours — one stylesheet, no JavaScript, nothing extra for a layout to link:

[highlight]
theme = "InspiredGitHub"
theme_dark = "base16-ocean.dark"

Only the token colours change with the scheme. The surface a block sits on is the page's, so give your dark mode a dark pre background — a dark theme's colours are chosen for one — with --orgo-code-bg under a built-in theme, or your own CSS.

The two themes are separated rather than stacked because they name different scopes: a light theme's .source.python .keyword outranks a dark theme's .keyword, so appending one to the other would leave light colours on some tokens. The cost of the separation is that a browser too old to know prefers-color-scheme matches neither query and shows code unhighlighted. Leaving theme_dark empty keeps the unconditional rules of before.

Highlighting emits CSS classes, never inline styles, so themes live in a stylesheet. Each build writes syntax.css into the output and every page links it.

orgo bundles TOML and Org on top of syntect's built-in languages. Anything else missing is a file away: put a .sublime-syntax definition in syntaxes_dir and it is loaded. A definition that fails to parse is reported and skipped, because one bad file should not stop a site from building.

[build]

KeyDefaultMeaning
draftsfalseInclude pages marked #+DRAFT:.
assets[]Extra directories copied to the site root.
sitemaptrueWrite sitemap.xml. Needs site.base_url.

--drafts on the command line turns this on for one run. The flag can only turn drafts on; it never turns off a config that asked for them.

sitemap.xml

Every page the build emits, generated ones included — a crawler has no other way to learn that /blog/ exists. lastmod is the page's own #+DATE: where it has one, and absent where it does not: a filesystem timestamp would say the day you cloned the repository.

Nothing is written until site.base_url is set. A sitemap has nowhere to put a relative URL, so a zero-config build produces no sitemap rather than an invalid one. Set a base URL and it appears; set sitemap = false and it does not.

Static files that live elsewhere

A site's static files do not always sit where its writing does. weblorg publishes theme/static/ at /, and a repository migrating from it should not have to move robots.txt next to its blog posts to keep the URL:

[build]
assets = ["../theme/static"]

Paths are relative to the source root and may point outside it. Each directory's contents land at the site root — theme/static/img/logo.svg publishes at /img/logo.svg, not /static/img/logo.svg.

Two files claiming one URL is a build error naming both, rather than a coin flip decided by directory order. A path that is not a directory is an error too, since it is a typo.

Under watch and serve these directories are watched as well, so editing a stylesheet outside the source tree still reloads the page.

[html]

KeyDefaultMeaning
heading_offset1Added to every org heading level.
toctrueMake page.toc available to templates.
section_numbersfalseNumber headings 1., 1.1., …

headingoffset

A level-1 org heading renders as <h2> by default, because the layout supplies the page title as the <h1>. This matches Emacs, whose org-html-toplevel-hlevel is 2 for the same reason.

Set it to 0 if your template renders no title of its own — otherwise the document starts at <h2> with nothing above it.

sectionnumbers differs from Emacs on purpose

org-export-with-section-numbers is on in Emacs, so an org-published site inherits numbered headings whether or not anyone chose them. Most sites do not want them, so the default here is the taste rather than the inheritance. Turning it on emits Emacs' own section-number-N classes.

Per-file overrides

Org's own #+OPTIONS: switches override the site setting for one document:

#+OPTIONS: toc:nil num:t
SwitchOverrides
toc:nil / toc:t[html] toc
num:t / num:nil[html] section_numbers

Off is spelled nil, false, no, 0 or off; anything else is on.

Configuration is a cache input

The resolved config is hashed into every page's render key, so editing orgo.toml re-renders exactly the pages it affects — which for most settings is all of them. You never need --no-cache after a config change.