orgo

Quick start

Five minutes from nothing to a site that reloads as you type.

Two commands

orgo init my-site
orgo serve my-site -o _site

Open 127.0.0.1:3000. Edit my-site/index.org in your editor, save, and the page reloads on its own.

init writes only files that do not already exist, so running it inside a directory that already has content is safe and additive.

What init created

my-site/
  orgo.toml            every setting, commented — all at their defaults but `theme`
  index.org               the home page
  blog/first-post.org     a post, to show the collection working
  templates/
    base.html             the page layout — edit this
    list.html             the blog index
    tags.html             the tag index
    feed.xml              an RSS feed

Adapting it to org files you already have

You do not need init, a config file, or templates. Every command takes the same two paths:

orgo serve <SOURCE> -o <OUTPUT>

SOURCE is the URL root

This is the one thing worth getting right, and it is not "the project directory" — it is the directory whose contents should sit at the top of your site. A file at SOURCE/blog/post.org is published at /blog/post.html.

So if your writing lives in a content/ subdirectory, point at content/, not at the repository around it:

cd ~/my-site
orgo serve content -o _site        # → /blog/post.html

Pointing one level too high still builds, which is what makes it worth saying out loud. It just builds the wrong site:

orgo serve . -o _site              # → /content/blog/post.html

Every URL gains a /content/ prefix, and every non-org file in the repository — README.md, build scripts, licence files — is copied into the output as a site asset. If you see either symptom, you picked the directory above the one you meant.

Common layouts

Your filesCommand
~/notes/*.orgorgo serve ~/notes -o /tmp/notes-site
my-site/content/**/*.orgcd my-site && orgo serve content -o _site
my-site/*.org at the top levelcd my-site && orgo serve . -o _site
Org files scattered in a code repoDo not. Copy or symlink the ones you publish into one directory.

OUTPUT can live inside the source

orgo serve . -o _site is fine: the output directory is recognised and skipped, so the build never copies its own output back into itself. Nothing dot-prefixed is published either, so .git stays out of a site built from a repository root.

Where config and templates go

Both live in the source directory — SOURCE/orgo.toml and SOURCE/templates/ — and neither is published. If you would rather keep the config elsewhere, name it:

orgo serve content -o _site --config config/orgo.toml

A worked example

A repository laid out as content/ (org files), theme/ (unrelated), build.py:

cd ~/my-site

# What is actually in there, before trusting anything with it.
orgo audit content

# Build it somewhere disposable and look.
orgo serve content -o /tmp/preview

# Happy with it? Build for real, failing on broken links.
orgo build content -o _site --strict

The audit reports which org constructs appear, how often, and whether each is supported — names, counts and line numbers only, never your text. See Auditing a corpus.

Nothing in your files has to change. With no config you get a complete site: a built-in layout, navigation across your top-level pages, and syntax highlighting.

Write a page

Any .org file under the source directory becomes a page at the matching path. notes/rust/borrowing.org becomes notes/rust/borrowing.html.

#+TITLE: Borrowing
#+DATE: <2026-02-02 Mon>
#+FILETAGS: :rust:notes:
#+DESCRIPTION: How the borrow checker thinks about lifetimes.

An opening paragraph, which becomes the excerpt in listings when there is no
description.

* A heading

Ordinary org: *bold*, /italic/, ~code~, [[https://orgmode.org][links]], and lists.

#+BEGIN_SRC rust
fn main() {}
#+END_SRC

The keywords are all optional. #+TITLE: names the page, #+DATE: orders it in listings, #+FILETAGS: groups it on tag pages, and #+DESCRIPTION: is its summary.

Control the URL

By default the filename decides the URL. #+SLUG: overrides it, which is how a date-prefixed filename becomes a clean address:

#+TITLE: Borrowing
#+SLUG: borrowing-explained

2026-02-02-borrowing.org now publishes as borrowing-explained.html.

Keep something unfinished out of the build

#+DRAFT: t

The page is not written, and does not appear in listings or navigation. Preview it while you work with --drafts:

orgo serve my-site -o _site --drafts

Change the design

Everything visual lives in templates/base.html. It is an ordinary minijinja (Jinja2) template, and replacing it replaces the whole layout:

<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
  <meta charset="utf-8">
  <title>{{ page.title }} — {{ site.title }}</title>
  <link rel="stylesheet" href="{{ root }}style.css">
</head>
<body>
  <nav>{% for item in nav %}<a href="{{ item.url }}">{{ item.title }}</a>{% endfor %}</nav>
  <h1>{{ page.title }}</h1>
  {{ body | safe }}
</body>
</html>

root is the ../ prefix back to the site root, so the same template works at any depth. Any other file in the source directory — style.css, images, fonts — is copied to the output untouched.

Editing a template rebuilds every page that uses it, so the browser reloads while you are still looking at it. The full list of variables is in Templates.

Add a blog index

Listing pages have no source file; they are declared in orgo.toml:

[[collections]]
source = "blog"
output = "blog/index.html"
template = "list.html"
title = "Blog"
sort = "date"
order = "desc"
nav = true

That is also how you get tag pages, pagination and an RSS feed — same mechanism, more settings. See Collections.

Build for real

orgo build my-site -o _site --strict

--strict turns broken internal links and parse diagnostics into a non-zero exit, which is what you want in CI. Deployment is just copying _site somewhere; see Deploying.

Next