krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

2f14a2076788dd8a1d9e2c4ee571df3b19198dcf

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-08-11T21:47:34Z

Rename to orgo

org-ssg described the project and named nothing. `orgo` is four letters, sits in
the same sound-space as hugo and zola without borrowing from either, and is free
on crates.io — which almost nothing short is.

The rename reaches further than the crate: the config file is `orgo.toml` and
the cache is `.orgo-cache.json`. Both are better done now, while the only site
using them is one I can rename in the same afternoon.
 .github/workflows/ci.yml                       |   4 +-
 .github/workflows/release.yml                  |   6 +-
 CHANGELOG.md                                   |   8 +-
 Cargo.lock                                     |   2 +-
 Cargo.toml                                     |  15 ++--
 README.md                                      |  36 ++++-----
 RELEASING.md                                   |   7 +-
 docs/guide/01-cli.org                          |  22 +++---
 docs/guide/02-configuration.org                |  12 +--
 docs/guide/03-collections.org                  |   2 +-
 docs/guide/04-templates.org                    |   2 +-
 docs/guide/05-org-support.org                  |  16 ++--
 docs/guide/06-authoring.org                    |   8 +-
 docs/guide/07-incremental.org                  |  14 ++--
 docs/guide/08-workflow.org                     |  12 +--
 docs/guide/09-auditing.org                     |  12 +--
 docs/guide/10-deploying.org                    |  20 ++---
 docs/guide/11-versioning.org                   |  12 +--
 docs/index.org                                 |  12 +--
 docs/install.org                               |  26 +++----
 docs/{org-ssg.toml => orgo.toml}               |   6 +-
 docs/quickstart.org                            |  38 ++++-----
 docs/style.css                                 |   2 +-
 docs/templates/base.html                       |   2 +-
 fixtures/site/about.org                        |   2 +-
 src/config.rs                                  |  20 ++---
 src/incremental.rs                             |   4 +-
 src/lib.rs                                     |   2 +-
 src/main.rs                                    |  38 ++++-----
 src/render.rs                                  |   4 +-
 src/serve.rs                                   |   2 +-
 src/site.rs                                    |   4 +-
 src/template.rs                                |  10 +--
 src/watch.rs                                   |   2 +-
 syntaxes/Org.sublime-syntax                    |   2 +-
 syntaxes/TOML.sublime-syntax                   |   2 +-
 tests/config.rs                                | 102 ++++++++++++-------------
 tests/constructs.rs                            |  12 +--
 tests/harness.rs                               |   4 +-
 tests/incremental.rs                           |  10 +--
 tests/oracle.el                                |   6 +-
 tests/oracle.rs                                |  24 +++---
 tests/pipeline.rs                              |   8 +-
 tests/serve.rs                                 |  22 +++---
 tests/site.rs                                  |  16 ++--
 tests/snapshots/oracle__oracle_blocks.snap     |   2 +-
 tests/snapshots/oracle__oracle_core.snap       |   2 +-
 tests/snapshots/oracle__oracle_elements.snap   |   2 +-
 tests/snapshots/oracle__oracle_footnote.snap   |   2 +-
 tests/snapshots/oracle__oracle_headings.snap   |   2 +-
 tests/snapshots/oracle__oracle_images.snap     |   2 +-
 tests/snapshots/oracle__oracle_lists.snap      |   2 +-
 tests/snapshots/oracle__oracle_minimal.snap    |   2 +-
 tests/snapshots/oracle__oracle_table.snap      |   2 +-
 tests/snapshots/oracle__oracle_timestamps.snap |   2 +-
 tests/snapshots/site__site_guide_html.snap     |   4 +-
 tests/snapshots/site__site_index_html.snap     |   4 +-
 tests/watch.rs                                 |  14 ++--
 58 files changed, 317 insertions(+), 317 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a6e3e7a..3992bfa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,6 +1,6 @@
 name: CI
 
-# Build, test and lint on both platforms org-ssg is used from, plus a compiler-floor job.
+# Build, test and lint on both platforms orgo is used from, plus a compiler-floor job.
 #
 # WHAT THIS CATCHES THAT LOCAL WORK DOES NOT:
 #
@@ -96,7 +96,7 @@ jobs:
       # Build only. The tests pull in dev-dependencies whose own floors move
       # independently, and chasing those would make this job about someone else's MSRV.
       #
-      # The floor is set by dependencies rather than by org-ssg — its own code compiles on
+      # The floor is set by dependencies rather than by orgo — its own code compiles on
       # 1.82 — which is precisely why it is checked here instead of reasoned about: a
       # dependency raising its floor is invisible until someone on an older compiler
       # tries to build.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 71c006b..27c1e46 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,7 +2,7 @@ name: Release
 
 # Build binaries for a tag and attach them to a GitHub release.
 #
-# WHY BINARIES AT ALL, given `cargo install org-ssg` exists: installing from source needs
+# WHY BINARIES AT ALL, given `cargo install orgo` exists: installing from source needs
 # a Rust toolchain and about a minute of compiling syntect. Someone evaluating a site
 # generator should be able to download one file and point it at their notes.
 #
@@ -74,9 +74,9 @@ jobs:
       - name: Package
         shell: bash
         run: |
-          staging="org-ssg-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
+          staging="orgo-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}"
           mkdir "$staging"
-          cp "target/${{ matrix.target }}/release/org-ssg" "$staging/"
+          cp "target/${{ matrix.target }}/release/orgo" "$staging/"
           cp README.md LICENSE CHANGELOG.md "$staging/"
           tar czf "$staging.tar.gz" "$staging"
           shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f9ef86..289bb49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@ Two conventions worth knowing before reading:
 
 - **A cache-format bump is not a change you need to act on.** The incremental cache is
   versioned and discards itself; a bump means the next build re-renders everything once.
-- **Output changes are called out.** org-ssg aims at what Emacs exports from the same
+- **Output changes are called out.** orgo aims at what Emacs exports from the same
   file, so an entry that says "now renders X" means your pages will change. That is the
   product, not a regression — but it belongs in a changelog rather than a diff you find
   later.
@@ -47,7 +47,7 @@ Release engineering, so that a version number is worth reading.
   built with `--strict`. Emacs is installed on both, so the oracle suite runs for real
   instead of skipping.
 - **A checked MSRV**, 1.88 — which is how it came to be 1.88 rather than the 1.82
-  org-ssg's own code needs. The floor comes from dependencies, and nobody finds that out
+  orgo's own code needs. The floor comes from dependencies, and nobody finds that out
   by reasoning about it.
 - **Release binaries** for macOS (arm64, x86_64) and Linux (gnu, musl), built on tag into
   a draft release. The tag is checked against `Cargo.toml` before anything is built.
@@ -117,7 +117,7 @@ publishes from the same sources. **All of these change output.**
 ## 0.12.0
 
 - `serve`: a development server with live reload, bound to loopback.
-- A documentation site under `docs/`, built by org-ssg itself.
+- A documentation site under `docs/`, built by orgo itself.
 
 ## 0.11.0
 
@@ -153,7 +153,7 @@ publishes from the same sources. **All of these change output.**
 ## 0.5.0
 
 - Parse diagnostics carry `file:line`, and pages render in parallel.
-- The corpus audit (`org-ssg audit`) and the `emacs --batch` oracle.
+- The corpus audit (`orgo audit`) and the `emacs --batch` oracle.
 - `#+SLUG:` decides a page's output filename — found by auditing a real corpus, where it
   affected 169 of 182 URLs.
 
diff --git a/Cargo.lock b/Cargo.lock
index 5c1f0df..955d1a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -674,7 +674,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "org-ssg"
+name = "orgo"
 version = "0.19.1"
 dependencies = [
  "anyhow",
diff --git a/Cargo.toml b/Cargo.toml
index 9a5c39e..d243106 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,17 +1,16 @@
 [package]
-name = "org-ssg"
+name = "orgo"
 version = "0.19.1"
 edition = "2021"
 description = "Org-mode static site generator that renders the org element tree straight to HTML"
-license = "MIT"
+license = "0BSD"
 readme = "README.md"
 keywords = ["org-mode", "static-site-generator", "emacs", "html", "blog"]
 categories = ["command-line-utilities", "text-processing"]
-# Set before the first `cargo publish`: crates.io shows it on the crate page, and a
-# missing link is the first thing anyone evaluating a generator looks for.
-# repository = "https://git.krz.sh/cmc/org-ssg.git/"
+repository = "https://github.com/ccleberg/orgo"
+homepage = "https://ccleberg.github.io/orgo/"
 
-# The compiler floor, checked in CI rather than assumed. org-ssg's own code needs 1.82
+# The compiler floor, checked in CI rather than assumed. orgo's own code needs 1.82
 # (`Option::is_none_or`); the floor is 1.88 because dependencies in Cargo.lock declare it
 # — `plist` and `time`, both by way of syntect. Raising this is a minor-version change,
 # never a patch.
@@ -23,11 +22,11 @@ rust-version = "1.88"
 exclude = ["docs/", "target/", "/.github/"]
 
 [lib]
-name = "org_ssg"
+name = "orgo"
 path = "src/lib.rs"
 
 [[bin]]
-name = "org-ssg"
+name = "orgo"
 path = "src/main.rs"
 
 [dependencies]
diff --git a/README.md b/README.md
index 6a45805..f903b36 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# org-ssg
+# orgo
 
 An org-mode static site generator, in Rust. Org is treated as the *source language*,
 not an inconvenient input to be normalized into markdown. The org element tree —
@@ -14,7 +14,7 @@ it imposes on the data model — pure, hashable, dependency-tracked units — is
 deliverable, even while the corpus is small enough that a full rebuild is instant.
 
 **Full documentation is in [`docs/`](docs/)** — a site written in org and built by
-org-ssg itself. Build and read it with:
+orgo itself. Build and read it with:
 
 ```bash
 cargo run -- serve docs -o docs/_site
@@ -33,23 +33,23 @@ Or skip the scaffolding entirely — point it at any directory of `.org` files:
 cargo run -- build ~/notes -o _site
 ```
 
-**Zero configuration is a supported path, not a demo.** With no `org-ssg.toml`, no
-templates and no org-ssg-specific markup in your files, you get a complete site: pages,
+**Zero configuration is a supported path, not a demo.** With no `orgo.toml`, no
+templates and no orgo-specific markup in your files, you get a complete site: pages,
 navigation, syntax-highlighted code and the stylesheet to colour it. Configuration
 changes what you get; it is never what makes it work.
 
 Discovery skips what should not be published — dot-directories such as `.git`, the config
 file, the templates directory, and the output directory when it sits inside the source, so
-`org-ssg build . -o _site` does the obvious thing.
+`orgo build . -o _site` does the obvious thing.
 
 ## Configuration
 
-Everything is optional. `org-ssg init` writes a fully commented `org-ssg.toml`; every
+Everything is optional. `orgo init` writes a fully commented `orgo.toml`; every
 value below is the default.
 
 ```toml
 [site]
-title = "org-ssg site"
+title = "orgo site"
 base_url = ""          # absolute URL, no trailing slash; needed for feeds/canonical links
 description = ""
 language = "en"
@@ -224,7 +224,7 @@ group.
 
 **A feed is a listing page with an XML template**, not a separate feature — templates are
 loaded by full filename and any extension, so `output = "feed.xml"` with
-`template = "feed.xml"` is all it takes. `org-ssg init` writes a working RSS template.
+`template = "feed.xml"` is all it takes. `orgo init` writes a working RSS template.
 
 A feed is read away from the site that served it, so relative links in one are simply
 broken. Set `site.base_url` and use the `absolute` filter:
@@ -319,7 +319,7 @@ is the only inherently global stage — it is where the link dependency graph is
 
 | Stage | Module | Notes |
 |---|---|---|
-| config | `src/config.rs` | `org-ssg.toml`: site metadata, nav mode, templates, theme. A hash input. |
+| config | `src/config.rs` | `orgo.toml`: site metadata, nav mode, templates, theme. A hash input. |
 | PARSE | `src/parser.rs` | Hand-written recursive descent: line lexer → element builder → inline tokenizer. |
 | audit | `src/audit.rs` | Phase 0 corpus audit: construct frequencies against the IN/OUT line. |
 | model | `src/model.rs` | The org element tree — Elements (block) vs Objects (inline). |
@@ -424,7 +424,7 @@ feature):**
   The config component folds in a **site-structure hash** (every page's `(path, title)`),
   because the shared nav bar is global chrome — a title change or a page add/remove alters
   the nav on every page and so must re-render them all (otherwise byte-equivalence breaks).
-- **Persisted cache manifest** (`<out>/.org-ssg-cache.json`, JSON), carrying per-page
+- **Persisted cache manifest** (`<out>/.orgo-cache.json`, JSON), carrying per-page
   records, the config/template hashes, and the serialized dependency graph, tagged with
   `CACHE_FORMAT_VERSION`. A version mismatch, a missing file, or a corrupt file all fall
   back to a clean full rebuild — the cache is an optimization, never a correctness
@@ -523,7 +523,7 @@ it over the original and touches the directory, which is one edit and several ev
 Two rules decide what counts as a change, and they are not the same rules the build uses
 to find content:
 
-- **A build input is a change.** Editing `org-ssg.toml` or a template rebuilds, even
+- **A build input is a change.** Editing `orgo.toml` or a template rebuilds, even
   though discovery skips both as non-content. The question is "would this change the
   site?", not "is this a page?".
 - **Our own output is not.** `watch . -o _site` puts the output inside the source, so a
@@ -544,7 +544,7 @@ Emacs does.
 The audit runs against any corpus — point it at your own notes before trusting this tool
 with them. The numbers below come from a 179-file site published today by weblorg, a
 wrapper around org's own HTML exporter, which makes it both a realistic workload and a
-directly comparable incumbent. With collections configured, org-ssg now reproduces
+directly comparable incumbent. With collections configured, orgo now reproduces
 **all 182 of that site's URLs**.
 
 ```
@@ -560,7 +560,7 @@ whole out-of-scope tail is 8 uses: four `#+TBLFM:` in a post *about* org-mode, t
 
 **`#+SLUG:` was a hole big enough to sink the project.** 178 of 179 files set it, and the
 published URL comes from it, not from the filename: `2018-11-28-aes-encryption.org` is
-served at `blog/aes-encryption.html`. org-ssg derived output paths from source filenames,
+served at `blog/aes-encryption.html`. orgo derived output paths from source filenames,
 so **169 of 179 pages would have been published at the wrong URL** — every inbound link and
 every search result, broken, by a tool that reported a clean build. Output paths now come
 from `#+SLUG:` when present ([`util::output_path`](src/util.rs)); slugs are sanitized so an
@@ -588,10 +588,10 @@ report gets reviewed and shows up as a diff, where a permanently red test gets i
 Three invariants are asserted outright, and all three hold — heading structure, list
 nesting, and source-block text match Emacs exactly.
 
-**No bugs in org-ssg.** Every remaining divergence is a deliberate choice to emit better
+**No bugs in orgo.** Every remaining divergence is a deliberate choice to emit better
 HTML than org does:
 
-| | org-ssg | Emacs | why |
+| | orgo | Emacs | why |
 |---|---|---|---|
 | emphasis | `<em>`/`<strong>` | `<i>`/`<b>` | semantic, not presentational |
 | captioned image | `<figure>`/`<figcaption>` | `<p>` + `"Figure 1: …"` | real figure semantics |
@@ -695,7 +695,7 @@ a major version — is what you actually build a site against:
 
 | Stable | Detail |
 |---|---|
-| `org-ssg.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. |
+| `orgo.toml` keys | Names, types and meaning. New keys are minor releases; removing one is major. |
 | Template context | `page`, `site`, `nav`, `root`, `pages`, `group`, `groups`, `paginator`, `stylesheet`, and the `absolute` / `rfc822` / `truncate` filters. |
 | CLI | Command names, flags, and exit codes. |
 | URLs | How a source path becomes an output path, including `#+SLUG:`. A generator that moves your URLs breaks every link anyone has to you. |
@@ -704,14 +704,14 @@ Explicitly **not stable**, so that the above can be:
 
 - **The incremental cache.** Versioned, discarded on mismatch, never a correctness
   dependency. It changes whenever it needs to, in any release.
-- **Rendered HTML details.** org-ssg tracks what Emacs exports from the same file, and
+- **Rendered HTML details.** orgo tracks what Emacs exports from the same file, and
   closing a gap changes markup. Changes that affect output are called out in
   [CHANGELOG.md](CHANGELOG.md) — the class names the documentation names (`post-list`,
   `figure-number`, `section-number-N`, `footnote-ref`) are the ones to write CSS against.
 - **The Rust API.** The crate is published so the binary can be installed with
   `cargo install`; the library exists to serve it, and its types move as the tool does.
 
-The **MSRV is 1.88**, checked in CI on every change. org-ssg's own code compiles on
+The **MSRV is 1.88**, checked in CI on every change. orgo's own code compiles on
 1.82; the floor comes from dependencies. Raising it is a minor version, never a patch.
 
 ## Dependencies
diff --git a/RELEASING.md b/RELEASING.md
index b819ed1..23684d1 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -7,14 +7,15 @@ reports `0.17.0` is the kind of mistake nobody notices for months.
 
 ## Before the first publish
 
-`repository` in `Cargo.toml` is commented out, because a wrong URL on a crates.io page is
-worse than none. Set it, then:
-
 ```bash
 cargo login          # a crates.io token, once per machine
 cargo publish --dry-run
 ```
 
+`repository` and `homepage` in `Cargo.toml` point at GitHub and at the documentation site
+on Pages. If git.krz.sh becomes the primary remote, `repository` should follow it —
+crates.io shows that link on the crate page, and it should lead somewhere you read.
+
 ## Every release
 
 1. **Write the changelog entry first.** [CHANGELOG.md](CHANGELOG.md) names behaviour, not
diff --git a/docs/guide/01-cli.org b/docs/guide/01-cli.org
index c8e9ab3..f85d8d0 100644
--- a/docs/guide/01-cli.org
+++ b/docs/guide/01-cli.org
@@ -7,7 +7,7 @@
 =build=, =serve= and =watch= all take the same pair:
 
 #+BEGIN_SRC sh
-org-ssg <command> <SOURCE> -o <OUTPUT>
+orgo <command> <SOURCE> -o <OUTPUT>
 #+END_SRC
 
 *SOURCE is the URL root*, not "the project". =SOURCE/blog/post.org= is published at
@@ -22,7 +22,7 @@ consumes its own output.
 * build
 
 #+BEGIN_SRC sh
-org-ssg build <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
+orgo build <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
 #+END_SRC
 
 If =INPUT= is a directory, it is walked and built into a linked site at =OUTPUT=. If it
@@ -35,7 +35,7 @@ though with no other documents to resolve against, internal links keep a best-ef
 | =--no-cache= | Ignore the incremental cache and re-render every page. |
 | =--strict= | Broken internal links and parse diagnostics become a non-zero exit. |
 | =--drafts= | Include pages marked =#+DRAFT:=. |
-| =--config FILE= | Use this config instead of =org-ssg.toml= in the source directory. |
+| =--config FILE= | Use this config instead of =orgo.toml= in the source directory. |
 
 The summary line reports what happened:
 
@@ -52,13 +52,13 @@ Without it, a broken link is a warning and the build succeeds. With it, the buil
 and names every problem. Use it wherever a bad build should not ship:
 
 #+BEGIN_SRC sh
-org-ssg build content -o _site --strict
+orgo build content -o _site --strict
 #+END_SRC
 
 * serve
 
 #+BEGIN_SRC sh
-org-ssg serve <INPUT> -o <OUTPUT> [-p PORT] [--host HOST] [--drafts] [--config FILE]
+orgo serve <INPUT> -o <OUTPUT> [-p PORT] [--host HOST] [--drafts] [--config FILE]
 #+END_SRC
 
 Builds, watches, serves, and reloads the browser when a rebuild lands. This is the
@@ -74,7 +74,7 @@ command to use while writing.
 laptop, so reaching the local network is something you ask for:
 
 #+BEGIN_SRC sh
-org-ssg serve content -o _site --host 0.0.0.0
+orgo serve content -o _site --host 0.0.0.0
 #+END_SRC
 
 The live-reload script is injected into responses and never written to disk, so what you
@@ -83,7 +83,7 @@ deploy stays clean. Details in [[file:../guide/08-workflow.org][Watching and ser
 * watch
 
 #+BEGIN_SRC sh
-org-ssg watch <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
+orgo watch <INPUT> -o <OUTPUT> [--no-cache] [--strict] [--drafts] [--config FILE]
 #+END_SRC
 
 Rebuilds on filesystem events with no server — for when something else is already serving
@@ -92,10 +92,10 @@ the output, or you just want the build to keep up as you write.
 * audit
 
 #+BEGIN_SRC sh
-org-ssg audit <INPUT>
+orgo audit <INPUT>
 #+END_SRC
 
-Reports which org constructs a corpus uses and how they land against what org-ssg
+Reports which org constructs a corpus uses and how they land against what orgo
 supports, plus a census of every keyword, block type, drawer and link scheme seen. Point
 it at your notes before trusting a tool with them. See [[file:../guide/09-auditing.org][Auditing a corpus]].
 
@@ -105,7 +105,7 @@ private notes is safe to share.
 * init
 
 #+BEGIN_SRC sh
-org-ssg init [DIRECTORY]
+orgo init [DIRECTORY]
 #+END_SRC
 
 Scaffolds a working site: a fully commented config, an editable copy of the built-in
@@ -118,7 +118,7 @@ that already has content — it fills in what is missing and leaves the rest alo
 * clean
 
 #+BEGIN_SRC sh
-org-ssg clean <OUTPUT>
+orgo clean <OUTPUT>
 #+END_SRC
 
 Removes the output directory, including the incremental cache manifest inside it. You
diff --git a/docs/guide/02-configuration.org b/docs/guide/02-configuration.org
index 4a254de..f8ea22d 100644
--- a/docs/guide/02-configuration.org
+++ b/docs/guide/02-configuration.org
@@ -1,8 +1,8 @@
 #+TITLE: Configuration
-#+DESCRIPTION: Every setting in org-ssg.toml, what it changes, and what it costs.
+#+DESCRIPTION: Every setting in orgo.toml, what it changes, and what it costs.
 #+LEDE: All of it optional. A missing config is a valid config.
 
-org-ssg looks for =org-ssg.toml= in the source directory. Pass a different path with
+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.
 
@@ -14,7 +14,7 @@ lose an afternoon.
 
 #+BEGIN_SRC toml
 [site]
-title = "org-ssg site"
+title = "orgo site"
 base_url = ""
 description = ""
 language = "en"
@@ -47,7 +47,7 @@ Plus any number of =[[collections]]= blocks, documented in [[file:03-collections
 
 | Key | Default | Meaning |
 |-----+---------+---------|
-| =title= | ="org-ssg site"= | Site name. Available as ={{ site.title }}=. |
+| =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. |
@@ -181,7 +181,7 @@ An unknown name is an error listing the valid ones.
 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.
 
-org-ssg bundles TOML and Org on top of syntect's built-in languages. Anything else
+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.
@@ -258,6 +258,6 @@ 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 =org-ssg.toml=
+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.
diff --git a/docs/guide/03-collections.org b/docs/guide/03-collections.org
index d45d6bb..948ae9f 100644
--- a/docs/guide/03-collections.org
+++ b/docs/guide/03-collections.org
@@ -230,7 +230,7 @@ title = "Feed"
 #+END_SRC
 
 This needs =site.base_url=, because a feed with relative links is invalid everywhere it
-is read. =org-ssg init= writes this template and leaves the collection commented out
+is read. =orgo init= writes this template and leaves the collection commented out
 until there is a base URL to make absolute links from.
 
 * Every setting
diff --git a/docs/guide/04-templates.org b/docs/guide/04-templates.org
index 9d43d33..941a8d3 100644
--- a/docs/guide/04-templates.org
+++ b/docs/guide/04-templates.org
@@ -122,7 +122,7 @@ Empty on generated pages, which build their content from =pages= or =groups= ins
 | =keywords= | *Every* =#+KEYWORD:=, by lowercased name. |
 
 =page.keywords= is the escape hatch: =#+CUSTOM_THING: x= is
-={{ page.keywords.custom_thing }}=, so your own metadata works without org-ssg knowing it
+={{ page.keywords.custom_thing }}=, so your own metadata works without orgo knowing it
 exists.
 
 ** site
diff --git a/docs/guide/05-org-support.org b/docs/guide/05-org-support.org
index b1ea7b9..dde4d6e 100644
--- a/docs/guide/05-org-support.org
+++ b/docs/guide/05-org-support.org
@@ -2,7 +2,7 @@
 #+DESCRIPTION: Exactly which org syntax is handled, which is not, and how the rest degrades.
 #+LEDE: A deliberate subset, with the boundary enforced by tests rather than by hope.
 
-org-ssg parses a defined slice of org. The boundary is not aspirational: every supported
+orgo parses a defined slice of org. The boundary is not aspirational: every supported
 construct has a golden-file test, and every excluded one has a test asserting how it
 degrades. That is what stops the parser drifting toward all-of-org.
 
@@ -37,7 +37,7 @@ machine-readable =datetime=.
 
 *** Text conversions
 
-Org rewrites some prose on export, and so does org-ssg:
+Org rewrites some prose on export, and so does orgo:
 
 | Written | Published |
 |---------+-----------|
@@ -94,7 +94,7 @@ An =html= export block passes through verbatim; every other backend is dropped,
 emitting LaTeX into an HTML page is worse than emitting nothing.
 
 *Any other name is a special block*: =#+BEGIN_NOTE= becomes =<div class="note">= holding
-*parsed org*, which is what makes the convention usable without org-ssg knowing the word
+*parsed org*, which is what makes the convention usable without orgo knowing the word
 "note". A =COMMENT= block is not published.
 
 *** Which languages highlight
@@ -108,7 +108,7 @@ Recognised, among others: =bash= / =sh=, =c=, =c++=, =css=, =clojure=, =diff=, =
 =makefile=, =markdown=, =matlab=, =objective-c=, =ocaml=, =perl=, =php=, =python=, =r=,
 =ruby=, =rust=, =scala=, =sql=, =tcl=, =xml=, =yaml=.
 
-org-ssg adds two syntect does not ship: *TOML* and *Org*. Both are what this project's
+orgo adds two syntect does not ship: *TOML* and *Org*. Both are what this project's
 own documentation needed on its first page — every config example is TOML, and a tool for
 org users gets written about in org — so they are compiled into the binary and work with
 no setup.
@@ -121,7 +121,7 @@ to parse is reported and skipped rather than failing the build.
 *** The comma escape
 
 A line inside a block that would otherwise look like document structure is written with a
-leading comma — =,* heading=, =,#+KEYWORD:= — and org-ssg removes exactly one comma on
+leading comma — =,* heading=, =,#+KEYWORD:= — and orgo removes exactly one comma on
 output, as Emacs does. Every org example in this documentation relies on it.
 
 The escape is not optional politeness: an unescaped =*= at column zero *ends the block*,
@@ -169,7 +169,7 @@ broken.
 | =#+CAPTION:=, =#+ATTR_HTML:= | Attach to the image *directly* below them — a blank line in between attaches to nothing, as in org. A captioned image is numbered =Figure N:=. |
 
 Every other =#+KEYWORD:= is available to templates as
-={{ page.keywords.that_keyword }}=, so metadata org-ssg has never heard of still reaches
+={{ page.keywords.that_keyword }}=, so metadata orgo has never heard of still reaches
 your layout.
 
 * Not supported, and what happens instead
@@ -219,7 +219,7 @@ source-block text are asserted to match exactly.
 
 The rest differs deliberately:
 
-| | org-ssg | Emacs |
+| | orgo | Emacs |
 |-+---------+-------|
 | emphasis | =<em>= / =<strong>= | =<i>= / =<b>= |
 | captioned image | =<figure>= / =<figcaption>= | =<p>= + "Figure 1: …" |
@@ -229,6 +229,6 @@ The rest differs deliberately:
 | code | =<pre><code>= | =<pre>= |
 
 One genuine semantic difference: org treats a single blank line between a =1.= list and a
-following =-= list as *one* list, keeping the first item's bullet type. org-ssg starts a
+following =-= list as *one* list, keeping the first item's bullet type. orgo starts a
 second list. That was kept on measurement — the pattern occurred zero times across a
 179-file reference corpus — rather than on taste.
diff --git a/docs/guide/06-authoring.org b/docs/guide/06-authoring.org
index b446096..c85e57a 100644
--- a/docs/guide/06-authoring.org
+++ b/docs/guide/06-authoring.org
@@ -39,7 +39,7 @@ That is deliberate: it is what that link would be on the published site, and bet
 now than by a reader.
 
 #+BEGIN_SRC sh
-org-ssg serve content -o _site --drafts
+orgo serve content -o _site --drafts
 #+END_SRC
 
 The keyword is read forgivingly. =t=, =yes=, =1= and a bare =#+DRAFT:= all mean draft,
@@ -125,7 +125,7 @@ Every =#+KEYWORD:= reaches templates under its lowercased name:
 {% if page.keywords.subtitle %}<p class="subtitle">{{ page.keywords.subtitle }}</p>{% endif %}
 #+END_SRC
 
-Nothing needs to be registered, and org-ssg needs no release to support a keyword you
+Nothing needs to be registered, and orgo needs no release to support a keyword you
 invented.
 
 * Assets
@@ -138,9 +138,9 @@ Four things are *never* published:
 
 - Dot-entries such as =.git= and =.env=. A source directory is often a repository, and
   publishing its history next to the homepage is a real way to leak a project.
-- =org-ssg.toml=, which is a build input.
+- =orgo.toml=, which is a build input.
 - The templates directory, likewise.
-- The output directory, when it lives inside the source — so =org-ssg build . -o _site=
+- The output directory, when it lives inside the source — so =orgo build . -o _site=
   does the obvious thing rather than copying its own output back into itself.
 
 Note that excluding dot-entries also means =.well-known/= cannot be published.
diff --git a/docs/guide/07-incremental.org b/docs/guide/07-incremental.org
index 9e3e7f3..7938ae8 100644
--- a/docs/guide/07-incremental.org
+++ b/docs/guide/07-incremental.org
@@ -2,7 +2,7 @@
 #+DESCRIPTION: How the cache decides what to re-render, and why that shape is the architecture.
 #+LEDE: Editing one post rebuilds four pages, whatever the size of the site.
 
-Incremental rebuilding is not an optimisation bolted on to org-ssg; it is the constraint
+Incremental rebuilding is not an optimisation bolted on to orgo; it is the constraint
 the data model was built around. Parsing is a pure function of one file's bytes, link
 resolution reports the edges it used, and rendering is a pure function of a resolved
 document. Those properties are what make caching sound — and they are also what make the
@@ -14,13 +14,13 @@ invalidates what explains the behaviour you will see.
 * What you observe
 
 #+BEGIN_EXAMPLE
-$ org-ssg build content -o _site
+$ orgo build content -o _site
 built 182 page(s) (182 rendered, 0 cached) ...
 
-$ org-ssg build content -o _site
+$ orgo build content -o _site
 built 182 page(s) (0 rendered, 182 cached) ...
 
-$ vim content/blog/post.org && org-ssg build content -o _site
+$ vim content/blog/post.org && orgo build content -o _site
 built 182 page(s) (4 rendered, 178 cached) ...
 #+END_EXAMPLE
 
@@ -35,7 +35,7 @@ Every page has a key composed from four hashes:
 |-----------+--------------|
 | content | The source file's bytes change. |
 | resolved links | A link's target moves, is renamed, or disappears. |
-| config | =org-ssg.toml= changes, or the shared chrome does. |
+| config | =orgo.toml= changes, or the shared chrome does. |
 | templates | *This page's* layout changes, or something that layout extends or includes. |
 
 If a page's key matches the cached one and its output file still exists, the file on disk
@@ -54,7 +54,7 @@ A template whose include is computed at render time — ={% include chooser %}=
 followed, so it is treated as depending on every template. Over-invalidating costs time;
 under-invalidating publishes a stale page.
 
-The cache lives in =<output>/.org-ssg-cache.json= and is tagged with a format version. A
+The cache lives in =<output>/.orgo-cache.json= and is tagged with a format version. A
 version mismatch, a missing file or a corrupt file all fall back to a full rebuild — the
 cache is an optimisation, never a correctness dependency. There is a test for each of
 those three fallbacks.
@@ -124,5 +124,5 @@ correctly on their own. It exists to answer "is the cache lying to me?" — and
 is, that is a bug worth reporting, with the two builds' output to compare.
 
 #+BEGIN_SRC sh
-org-ssg build content -o _site --no-cache
+orgo build content -o _site --no-cache
 #+END_SRC
diff --git a/docs/guide/08-workflow.org b/docs/guide/08-workflow.org
index 7c9e717..79e02f6 100644
--- a/docs/guide/08-workflow.org
+++ b/docs/guide/08-workflow.org
@@ -5,7 +5,7 @@
 * serve
 
 #+BEGIN_SRC sh
-org-ssg serve content -o _site
+orgo serve content -o _site
 #+END_SRC
 
 Builds, watches, serves at [[http://127.0.0.1:3000][127.0.0.1:3000]], and reloads the
@@ -43,7 +43,7 @@ the build just failed tells you nothing — the error is already on your termina
 * watch
 
 #+BEGIN_SRC sh
-org-ssg watch content -o _site
+orgo watch content -o _site
 #+END_SRC
 
 The same rebuilding without the server, for when something else already serves the output.
@@ -54,7 +54,7 @@ Rebuilds are driven by OS filesystem events, so nothing happens while nothing ha
 The rule for what triggers one is deliberately *not* the rule the build uses to find
 content — the question is "would this change the site?", not "is this a page?".
 
-*Triggers a rebuild:* any =.org= file, any asset, =org-ssg.toml=, and anything in the
+*Triggers a rebuild:* any =.org= file, any asset, =orgo.toml=, and anything in the
 templates directory. The last two are skipped by the build when looking for content, but
 both change the output.
 
@@ -86,7 +86,7 @@ blog/post.org changed: 2 rendered, 180 cached
 
 * Where native watching is unavailable
 
-Some network and container filesystems have no event API. org-ssg falls back to polling
+Some network and container filesystems have no event API. orgo falls back to polling
 every two seconds and says so, rather than failing:
 
 #+BEGIN_EXAMPLE
@@ -106,10 +106,10 @@ note: native file watching unavailable (...); polling every 2s
 
 #+BEGIN_SRC sh
 # One terminal, left running.
-org-ssg serve content -o _site --drafts
+orgo serve content -o _site --drafts
 
 # Write. The browser keeps up.
 
 # Before publishing, check what a real build says.
-org-ssg build content -o _site --strict
+orgo build content -o _site --strict
 #+END_SRC
diff --git a/docs/guide/09-auditing.org b/docs/guide/09-auditing.org
index c8c7e05..0c870d1 100644
--- a/docs/guide/09-auditing.org
+++ b/docs/guide/09-auditing.org
@@ -3,7 +3,7 @@
 #+LEDE: Construct frequencies, unknown-name census, and no document text in the output.
 
 #+BEGIN_SRC sh
-org-ssg audit ~/notes
+orgo audit ~/notes
 #+END_SRC
 
 The audit answers two questions about a body of org files:
@@ -11,7 +11,7 @@ The audit answers two questions about a body of org files:
 1. *Coverage.* Of the constructs this corpus uses, which are supported? A construct that
    is common here and unsupported is a problem with the tool's scope, not with your
    writing.
-2. *Blind spots.* Which names appear that org-ssg has no opinion about at all? These are
+2. *Blind spots.* Which names appear that orgo has no opinion about at all? These are
    the dangerous ones — not "known unsupported" but unknown.
 
 * Reading the output
@@ -40,7 +40,7 @@ KEYWORDS
 - =IN= is supported; =OUT= is excluded by design and degrades as described in
   [[file:05-org-support.org][Org support]].
 - The *coverage* line is the number to look at first.
-- =???= marks a name org-ssg does not recognise at all — in this example =#+SLUG:=, from
+- =???= marks a name orgo does not recognise at all — in this example =#+SLUG:=, from
   a run made before it was supported.
 
 Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
@@ -80,13 +80,13 @@ green run; it simply measures one thing less.
 
 #+BEGIN_SRC sh
 # What is in there?
-org-ssg audit ~/notes
+orgo audit ~/notes
 
 # Build it and see what the builder itself complains about.
-org-ssg build ~/notes -o /tmp/preview --strict
+orgo build ~/notes -o /tmp/preview --strict
 
 # Look at the result.
-org-ssg serve ~/notes -o /tmp/preview
+orgo serve ~/notes -o /tmp/preview
 #+END_SRC
 
 =--strict= surfaces broken internal links and malformed constructs as failures rather
diff --git a/docs/guide/10-deploying.org b/docs/guide/10-deploying.org
index f668865..3fad552 100644
--- a/docs/guide/10-deploying.org
+++ b/docs/guide/10-deploying.org
@@ -5,7 +5,7 @@
 * The production build
 
 #+BEGIN_SRC sh
-org-ssg build content -o _site --strict
+orgo build content -o _site --strict
 #+END_SRC
 
 Two differences from the build you run while writing:
@@ -34,12 +34,12 @@ No trailing slash.
 
 * One thing to exclude
 
-The build writes =.org-ssg-cache.json= into the output directory. It is a dot-file, so
+The build writes =.orgo-cache.json= into the output directory. It is a dot-file, so
 most static hosts ignore it, but it is not part of the site — exclude it if your host
 uploads everything:
 
 #+BEGIN_SRC sh
-rsync -a --delete --exclude '.org-ssg-cache.json' _site/ user@host:/var/www/site/
+rsync -a --delete --exclude '.orgo-cache.json' _site/ user@host:/var/www/site/
 #+END_SRC
 
 Keeping the cache *between* deploys, where the CI runner can see it, is what makes CI
@@ -57,7 +57,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: dtolnay/rust-toolchain@stable
       - run: cargo install --path .
-      - run: org-ssg build content -o _site --strict
+      - run: orgo build content -o _site --strict
       - uses: actions/upload-artifact@v4
         with:
           name: site
@@ -69,7 +69,7 @@ failed build.
 
 ** Caching between runs
 
-Cache =_site/.org-ssg-cache.json= *and* =_site= together, or not at all. The manifest
+Cache =_site/.orgo-cache.json= *and* =_site= together, or not at all. The manifest
 describes files it expects to find; a cache without its outputs simply triggers a full
 rebuild, which is correct but pointless.
 
@@ -78,16 +78,16 @@ builds is rarely worth the configuration.
 
 * Static hosts
 
-Nothing here is org-ssg-specific; a built site is ordinary static files.
+Nothing here is orgo-specific; a built site is ordinary static files.
 
 - *Netlify, Vercel, Cloudflare Pages*: publish directory =_site=, build command
-  =cargo install --path . && org-ssg build content -o _site --strict=.
+  =cargo install --path . && orgo build content -o _site --strict=.
 - *GitHub Pages*: upload =_site= as the Pages artifact.
 - *Any web server*: copy =_site= to the document root.
 
 ** URLs end in .html
 
-org-ssg writes =blog/post.html= and links to it that way, so the site works with no
+orgo writes =blog/post.html= and links to it that way, so the site works with no
 server configuration at all — including opening it from a filesystem path.
 
 If you prefer extensionless URLs, that is a server-side rewrite, and you should also set
@@ -96,8 +96,8 @@ If you prefer extensionless URLs, that is a server-side rewrite, and you should
 * Checking a build before shipping
 
 #+BEGIN_SRC sh
-org-ssg build content -o _site --strict
-org-ssg serve content -o _site
+orgo build content -o _site --strict
+orgo serve content -o _site
 #+END_SRC
 
 Serving the production build locally is the last check worth doing: it catches a missing
diff --git a/docs/guide/11-versioning.org b/docs/guide/11-versioning.org
index f03a1b7..682c57d 100644
--- a/docs/guide/11-versioning.org
+++ b/docs/guide/11-versioning.org
@@ -11,7 +11,7 @@ Changing any of this incompatibly requires a major version.
 
 | Stable | What that covers |
 |--------+------------------|
-| =org-ssg.toml= keys | Their names, types and meaning. |
+| =orgo.toml= keys | Their names, types and meaning. |
 | Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. |
 | The CLI | Command names, flags and exit codes. |
 | URLs | How a source path becomes an output path, =#+SLUG:= included. |
@@ -28,13 +28,13 @@ Three things move freely, so the list above can hold still.
 
 ** The incremental cache
 
-=<output>/.org-ssg-cache.json= is versioned and discards itself on a mismatch. A cache
+=<output>/.orgo-cache.json= is versioned and discards itself on a mismatch. A cache
 format bump means one full rebuild, and nothing else. It is never a correctness
 dependency: a missing, stale or corrupt cache produces exactly the same site, more slowly.
 
 ** Rendered HTML details
 
-org-ssg aims at what Emacs exports from the same file, and closing a gap changes markup.
+orgo aims at what Emacs exports from the same file, and closing a gap changes markup.
 That is the product working rather than a regression — but it is called out in the
 changelog every time, because your stylesheet is downstream of it.
 
@@ -50,7 +50,7 @@ exists to serve the binary, and its types move as the tool does.
 * The compiler floor
 
 The MSRV is *1.88*, checked in CI on every change rather than assumed — which is how it
-came to be 1.88 rather than the 1.82 org-ssg's own code needs. The floor is set by
+came to be 1.88 rather than the 1.82 orgo's own code needs. The floor is set by
 dependencies, and a dependency raising its own is invisible until someone on an older
 compiler tries to build.
 
@@ -59,8 +59,8 @@ Raising it is a minor version, never a patch.
 * Upgrading
 
 #+BEGIN_SRC sh
-cargo install org-ssg          # or download a release binary
-org-ssg build content -o _site --no-cache --strict
+cargo install orgo          # or download a release binary
+orgo build content -o _site --no-cache --strict
 #+END_SRC
 
 =--no-cache= makes the first build after an upgrade a full one, so you are comparing the
diff --git a/docs/index.org b/docs/index.org
index 4807762..7b7c01a 100644
--- a/docs/index.org
+++ b/docs/index.org
@@ -1,9 +1,9 @@
-#+TITLE: org-ssg
+#+TITLE: orgo
 #+DESCRIPTION: An org-mode static site generator in Rust, where the org element tree is the document model.
 #+LEDE: Org is the source language, not an inconvenient input to be normalised into markdown.
 #+OPTIONS: toc:nil
 
-org-ssg turns a directory of =.org= files into a static website. It treats org as the
+orgo turns a directory of =.org= files into a static website. It treats org as the
 *source language*: the org element tree — headings, drawers, blocks, links with their
 org-specific semantics — /is/ the document model, and that tree is rendered straight to
 HTML. There is no markdown-shaped intermediate representation, because the point is to
@@ -26,10 +26,10 @@ Open [[http://127.0.0.1:3000][127.0.0.1:3000]], edit any =.org= file, and the br
 
 Point it at a directory of org files and you get a complete site: pages, navigation,
 syntax-highlighted code, and the stylesheet that colours it. Nothing about your files has
-to change, and no =org-ssg.toml= is required.
+to change, and no =orgo.toml= is required.
 
 #+BEGIN_SRC sh
-org-ssg build ~/notes -o _site
+orgo build ~/notes -o _site
 #+END_SRC
 
 Configuration changes what you get. It is never what makes it work.
@@ -54,7 +54,7 @@ listed in [[file:guide/05-org-support.org][Org support]].
 ** It tells you what your corpus actually uses
 
 #+BEGIN_SRC sh
-org-ssg audit ~/notes
+orgo audit ~/notes
 #+END_SRC
 
 The audit reports which org constructs appear in a corpus, how often, and whether each is
@@ -75,6 +75,6 @@ notes stays safe to paste into an issue.
 
 * Status
 
-This documentation site is itself an org-ssg site — the sources are in =docs/= and it is
+This documentation site is itself an orgo site — the sources are in =docs/= and it is
 built with the command in [[file:quickstart.org][Quick start]]. If a feature is described here, it is being used
 to render the page describing it.
diff --git a/docs/install.org b/docs/install.org
index fc7f0ce..8cbdec6 100644
--- a/docs/install.org
+++ b/docs/install.org
@@ -1,5 +1,5 @@
 #+TITLE: Install
-#+DESCRIPTION: Build org-ssg from source, put it on your PATH, and check that it works.
+#+DESCRIPTION: Build orgo from source, put it on your PATH, and check that it works.
 #+LEDE: One Rust toolchain, one command, no runtime dependencies.
 
 * Requirements
@@ -13,15 +13,15 @@
 * From source
 
 #+BEGIN_SRC sh
-git clone <repository-url> org-ssg
-cd org-ssg
+git clone <repository-url> orgo
+cd orgo
 cargo build --release
 #+END_SRC
 
-The binary lands at =target/release/org-ssg=. Copy it somewhere on your =PATH=:
+The binary lands at =target/release/orgo=. Copy it somewhere on your =PATH=:
 
 #+BEGIN_SRC sh
-cp target/release/org-ssg ~/.local/bin/
+cp target/release/orgo ~/.local/bin/
 #+END_SRC
 
 Or let cargo do it, which puts it in =~/.cargo/bin=:
@@ -33,7 +33,7 @@ cargo install --path .
 * Running without installing
 
 Every command in this documentation works through cargo if you would rather not install
-anything. Replace =org-ssg= with =cargo run --= and add =--release= for a fast build:
+anything. Replace =orgo= with =cargo run --= and add =--release= for a fast build:
 
 #+BEGIN_SRC sh
 cargo run --release -- build my-site -o _site
@@ -45,9 +45,9 @@ syntax highlighting dominates and is not optimised in a debug profile.
 * Check that it works
 
 #+BEGIN_SRC sh
-org-ssg --version
-org-ssg init /tmp/org-ssg-check
-org-ssg build /tmp/org-ssg-check -o /tmp/org-ssg-check/_site
+orgo --version
+orgo init /tmp/orgo-check
+orgo build /tmp/orgo-check -o /tmp/orgo-check/_site
 #+END_SRC
 
 You should see a line reporting the pages built:
@@ -56,10 +56,10 @@ You should see a line reporting the pages built:
 built 5 page(s) (5 rendered, 0 cached), copied 0 asset(s) ... (0 unresolved link(s), 0 diagnostic(s))
 #+END_EXAMPLE
 
-Open =/tmp/org-ssg-check/_site/index.html= in a browser, or serve it properly:
+Open =/tmp/orgo-check/_site/index.html= in a browser, or serve it properly:
 
 #+BEGIN_SRC sh
-org-ssg serve /tmp/org-ssg-check -o /tmp/org-ssg-check/_site
+orgo serve /tmp/orgo-check -o /tmp/orgo-check/_site
 #+END_SRC
 
 * Running the test suite
@@ -82,13 +82,13 @@ still gets a green test run — it simply measures one thing less.
 
 * Upgrading
 
-org-ssg stores an incremental cache in =<output>/.org-ssg-cache.json=, tagged with a
+orgo stores an incremental cache in =<output>/.orgo-cache.json=, tagged with a
 format version. A newer binary that changes how output is produced bumps that version,
 and a version it does not recognise is discarded in favour of a full rebuild. You never
 need to clear the cache by hand after an upgrade — but if you want to:
 
 #+BEGIN_SRC sh
-org-ssg clean _site
+orgo clean _site
 #+END_SRC
 
 * Next
diff --git a/docs/org-ssg.toml b/docs/orgo.toml
similarity index 89%
rename from docs/org-ssg.toml
rename to docs/orgo.toml
index b3db419..dbf4ad4 100644
--- a/docs/org-ssg.toml
+++ b/docs/orgo.toml
@@ -1,10 +1,10 @@
-# Configuration for the org-ssg documentation site.
+# Configuration for the orgo documentation site.
 #
-# This site is built by org-ssg itself, so this file doubles as a worked example: every
+# This site is built by orgo itself, so this file doubles as a worked example: every
 # setting here is one the docs describe, used the way the docs recommend.
 
 [site]
-title = "org-ssg"
+title = "orgo"
 description = "An org-mode static site generator, in Rust."
 language = "en"
 # Left empty so the docs build with relative URLs and open from the filesystem. Set it to
diff --git a/docs/quickstart.org b/docs/quickstart.org
index 64ecc6a..979076f 100644
--- a/docs/quickstart.org
+++ b/docs/quickstart.org
@@ -5,8 +5,8 @@
 * Two commands
 
 #+BEGIN_SRC sh
-org-ssg init my-site
-org-ssg serve my-site -o _site
+orgo init my-site
+orgo serve my-site -o _site
 #+END_SRC
 
 Open [[http://127.0.0.1:3000][127.0.0.1:3000]]. Edit =my-site/index.org= in your editor, save, and the page reloads
@@ -19,7 +19,7 @@ already has content is safe and additive.
 
 #+BEGIN_EXAMPLE
 my-site/
-  org-ssg.toml            every setting, at its default, commented
+  orgo.toml            every setting, at its default, commented
   index.org               the home page
   blog/first-post.org     a post, to show the collection working
   templates/
@@ -35,7 +35,7 @@ You do not need =init=, a config file, or templates. Every command takes the sam
 paths:
 
 #+BEGIN_SRC sh
-org-ssg serve <SOURCE> -o <OUTPUT>
+orgo serve <SOURCE> -o <OUTPUT>
 #+END_SRC
 
 ** SOURCE is the URL root
@@ -49,14 +49,14 @@ repository around it:
 
 #+BEGIN_SRC sh
 cd ~/my-site
-org-ssg serve content -o _site        # → /blog/post.html
+orgo serve content -o _site        # → /blog/post.html
 #+END_SRC
 
 Pointing one level too high still builds, which is what makes it worth saying out loud.
 It just builds the wrong site:
 
 #+BEGIN_SRC sh
-org-ssg serve . -o _site              # → /content/blog/post.html
+orgo serve . -o _site              # → /content/blog/post.html
 #+END_SRC
 
 Every URL gains a =/content/= prefix, and every non-org file in the repository —
@@ -67,24 +67,24 @@ If you see either symptom, you picked the directory above the one you meant.
 
 | Your files | Command |
 |------------+---------|
-| =~/notes/*.org= | =org-ssg serve ~/notes -o /tmp/notes-site= |
-| =my-site/content/**/*.org= | =cd my-site && org-ssg serve content -o _site= |
-| =my-site/*.org= at the top level | =cd my-site && org-ssg serve . -o _site= |
+| =~/notes/*.org= | =orgo serve ~/notes -o /tmp/notes-site= |
+| =my-site/content/**/*.org= | =cd my-site && orgo serve content -o _site= |
+| =my-site/*.org= at the top level | =cd my-site && orgo serve . -o _site= |
 | Org files scattered in a code repo | Do not. Copy or symlink the ones you publish into one directory. |
 
 ** OUTPUT can live inside the source
 
-=org-ssg serve . -o _site= is fine: the output directory is recognised and skipped, so
+=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/org-ssg.toml= and =SOURCE/templates/= — and
+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:
 
 #+BEGIN_SRC sh
-org-ssg serve content -o _site --config config/org-ssg.toml
+orgo serve content -o _site --config config/orgo.toml
 #+END_SRC
 
 ** A worked example
@@ -95,13 +95,13 @@ 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.
-org-ssg audit content
+orgo audit content
 
 # Build it somewhere disposable and look.
-org-ssg serve content -o /tmp/preview
+orgo serve content -o /tmp/preview
 
 # Happy with it? Build for real, failing on broken links.
-org-ssg build content -o _site --strict
+orgo build content -o _site --strict
 #+END_SRC
 
 The audit reports which org constructs appear, how often, and whether each is supported —
@@ -159,7 +159,7 @@ The page is not written, and does not appear in listings or navigation. Preview
 you work with =--drafts=:
 
 #+BEGIN_SRC sh
-org-ssg serve my-site -o _site --drafts
+orgo serve my-site -o _site --drafts
 #+END_SRC
 
 * Change the design
@@ -193,7 +193,7 @@ still looking at it. The full list of variables is in [[file:guide/04-templates.
 
 * Add a blog index
 
-Listing pages have no source file; they are declared in =org-ssg.toml=:
+Listing pages have no source file; they are declared in =orgo.toml=:
 
 #+BEGIN_SRC toml
 [[collections]]
@@ -212,7 +212,7 @@ settings. See [[file:guide/03-collections.org][Collections]].
 * Build for real
 
 #+BEGIN_SRC sh
-org-ssg build my-site -o _site --strict
+orgo build my-site -o _site --strict
 #+END_SRC
 
 =--strict= turns broken internal links and parse diagnostics into a non-zero exit, which
@@ -222,5 +222,5 @@ is what you want in CI. Deployment is just copying =_site= somewhere; see
 * Next
 
 - [[file:guide/01-cli.org][Command reference]] — every command and flag.
-- [[file:guide/02-configuration.org][Configuration]] — every setting in =org-ssg.toml=.
+- [[file:guide/02-configuration.org][Configuration]] — every setting in =orgo.toml=.
 - [[file:guide/05-org-support.org][Org support]] — exactly which org syntax is handled.
diff --git a/docs/style.css b/docs/style.css
index 27b9f13..8192c0d 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -74,7 +74,7 @@ code {
 }
 
 /* Code blocks are dark in both colour schemes, matching the syntect theme named in
-   org-ssg.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page
+   orgo.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page
    commits to the theme's palette instead of leaving highlighted code unreadable in one
    of the two. */
 pre {
diff --git a/docs/templates/base.html b/docs/templates/base.html
index 9faafc3..48e1af7 100644
--- a/docs/templates/base.html
+++ b/docs/templates/base.html
@@ -45,7 +45,7 @@
 {%- endif %}
 {% block content %}{{ body | safe }}{% endblock %}</main>
 <footer class="site">
-Built with org-ssg &mdash; these docs are an org-ssg site.
+Built with orgo &mdash; these docs are an orgo site.
 </footer>
 </body>
 </html>
diff --git a/fixtures/site/about.org b/fixtures/site/about.org
index a0239c8..2ced0f0 100644
--- a/fixtures/site/about.org
+++ b/fixtures/site/about.org
@@ -1,3 +1,3 @@
 #+TITLE: About
 
-This site is built by org-ssg. Back to [[file:index.org][home]].
+This site is built by orgo. Back to [[file:index.org][home]].
diff --git a/src/config.rs b/src/config.rs
index e826737..af4dedb 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-//! User-facing build configuration (`org-ssg.toml`).
+//! User-facing build configuration (`orgo.toml`).
 //!
 //! Everything here was once a constant in the source: the page layout, the nav rule, the
 //! highlighting theme. That made the generator produce exactly one kind of site — a
@@ -7,10 +7,10 @@
 //! Two properties matter beyond the settings themselves:
 //!
 //! 1. **Absent config is a valid config.** Every field has a default, so a directory of
-//!    `.org` files with no `org-ssg.toml` still builds. Configuration is how you change
+//!    `.org` files with no `orgo.toml` still builds. Configuration is how you change
 //!    the output, never how you make it work at all.
 //! 2. **Config is a hash input** (spec §4.1). [`Config`] serializes deterministically and
-//!    its hash is folded into every page's render key, so editing `org-ssg.toml` re-renders
+//!    its hash is folded 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.
 
 use anyhow::{Context, Result};
@@ -18,7 +18,7 @@ use camino::{Utf8Path, Utf8PathBuf};
 use serde::{Deserialize, Serialize};
 
 /// The config file's name, looked for in the source directory.
-pub const CONFIG_FILE: &str = "org-ssg.toml";
+pub const CONFIG_FILE: &str = "orgo.toml";
 
 /// Resolved build configuration. Serialized into the config hash, so field order and
 /// defaults are part of the cache contract.
@@ -319,7 +319,7 @@ pub struct Site {
 impl Default for Site {
     fn default() -> Self {
         Site {
-            title: "org-ssg site".to_string(),
+            title: "orgo site".to_string(),
             base_url: String::new(),
             description: String::new(),
             language: "en".to_string(),
@@ -405,7 +405,7 @@ impl Default for Highlight {
 }
 
 impl Config {
-    /// Load `org-ssg.toml` from `dir`, or return defaults if there is none.
+    /// Load `orgo.toml` from `dir`, or return defaults if there is none.
     ///
     /// A *missing* config is normal and silent. A *malformed* one is an error: someone
     /// who wrote a config meant it, and silently building the default site would hide
@@ -533,13 +533,13 @@ impl Config {
     }
 }
 
-/// The starter config written by `org-ssg init`, and the documentation of record for
+/// The starter config written by `orgo init`, and the documentation of record for
 /// what is configurable. Every value shown is the default, so deleting any line is safe.
-pub const STARTER_CONFIG: &str = r#"# org-ssg configuration. Every setting here is optional and shown at its default,
+pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default,
 # so you can delete any line you do not need — or the whole file.
 
 [site]
-title = "org-ssg site"
+title = "orgo site"
 # Absolute base URL, no trailing slash. Needed for feeds and canonical links, which
 # cannot be relative — set it and uncomment the [[collections]] feed block below.
 base_url = ""
@@ -574,7 +574,7 @@ expose_page_list = false
 # A syntect theme name: InspiredGitHub, Solarized (dark), base16-ocean.dark,
 # base16-eighties.dark, base16-mocha.dark, base16-ocean.light.
 theme = "InspiredGitHub"
-# Extra .sublime-syntax files for languages neither syntect nor org-ssg bundles.
+# Extra .sublime-syntax files for languages neither syntect nor orgo bundles.
 syntaxes_dir = "syntaxes"
 
 [build]
diff --git a/src/incremental.rs b/src/incremental.rs
index 1ae9d0d..8960ec3 100644
--- a/src/incremental.rs
+++ b/src/incremental.rs
@@ -35,7 +35,7 @@ pub const CACHE_FORMAT_VERSION: u32 = 7;
 pub type Hash = ContentHash;
 
 /// The resolved global build config is [`crate::config::Config`]; its hash is a
-/// component of every page's render key (spec §4.1), so editing `org-ssg.toml`
+/// component of every page's render key (spec §4.1), so editing `orgo.toml`
 /// invalidates the pages it affects.
 pub use crate::config::Config as BuildConfig;
 
@@ -199,7 +199,7 @@ pub struct Manifest {
 /// The cache-manifest file lives inside the output directory (spec §4.5: an on-disk
 /// cache dir). `clean` removes the output directory, taking the cache with it.
 pub fn manifest_path(out: &Utf8Path) -> Utf8PathBuf {
-    out.join(".org-ssg-cache.json")
+    out.join(".orgo-cache.json")
 }
 
 /// Load the manifest, returning `None` on ANY of: missing file, read/parse error, or a
diff --git a/src/lib.rs b/src/lib.rs
index 4dc73cd..ebf3210 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-//! org-ssg — an org-mode static site generator.
+//! orgo — an org-mode static site generator.
 //!
 //! Org is the source language, not an input to be normalized into markdown. The org
 //! element tree ([`model`]) *is* the document model; we render it straight to HTML.
diff --git a/src/main.rs b/src/main.rs
index 7f49f9d..32bb19a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,15 +6,15 @@ use anyhow::{Context, Result};
 use camino::{Utf8Path, Utf8PathBuf};
 use clap::{Parser, Subcommand};
 
-use org_ssg::parser::parse;
-use org_ssg::config::{self, Config};
-use org_ssg::render::{self, render, Html, SyntectHighlighter};
-use org_ssg::resolve::ResolvedDoc;
-use org_ssg::site::{build_site, BuildOptions, SYNTAX_STYLESHEET};
-use org_ssg::template::{PageContext, RenderContext, SiteContext, Templater};
+use orgo::parser::parse;
+use orgo::config::{self, Config};
+use orgo::render::{self, render, Html, SyntectHighlighter};
+use orgo::resolve::ResolvedDoc;
+use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET};
+use orgo::template::{PageContext, RenderContext, SiteContext, Templater};
 
 #[derive(Parser)]
-#[command(name = "org-ssg", version, about = "Org-mode static site generator")]
+#[command(name = "orgo", version, about = "Org-mode static site generator")]
 struct Cli {
     #[command(subcommand)]
     command: Command,
@@ -36,7 +36,7 @@ enum Command {
         /// Treat broken links and parse diagnostics as errors (spec §4.3.4).
         #[arg(long)]
         strict: bool,
-        /// Config file to use, overriding `org-ssg.toml` in the source directory.
+        /// Config file to use, overriding `orgo.toml` in the source directory.
         #[arg(long, value_name = "FILE")]
         config: Option<Utf8PathBuf>,
         /// Include pages marked `#+DRAFT:`.
@@ -57,7 +57,7 @@ enum Command {
         /// Treat broken links and parse diagnostics as errors.
         #[arg(long)]
         strict: bool,
-        /// Config file to use, overriding `org-ssg.toml` in the source directory.
+        /// Config file to use, overriding `orgo.toml` in the source directory.
         #[arg(long, value_name = "FILE")]
         config: Option<Utf8PathBuf>,
         /// Include pages marked `#+DRAFT:`. Handy while writing one.
@@ -81,7 +81,7 @@ enum Command {
         /// Include pages marked `#+DRAFT:`.
         #[arg(long)]
         drafts: bool,
-        /// Config file to use, overriding `org-ssg.toml` in the source directory.
+        /// Config file to use, overriding `orgo.toml` in the source directory.
         #[arg(long, value_name = "FILE")]
         config: Option<Utf8PathBuf>,
     },
@@ -151,7 +151,7 @@ fn main() -> Result<()> {
             strict,
             config,
             drafts,
-        } => org_ssg::watch::run(
+        } => orgo::watch::run(
             &input,
             &output,
             &BuildOptions {
@@ -162,8 +162,8 @@ fn main() -> Result<()> {
             },
         ),
         Command::Audit { input } => {
-            let result = org_ssg::audit::audit(&input)?;
-            print!("{}", org_ssg::audit::report(&result));
+            let result = orgo::audit::audit(&input)?;
+            print!("{}", orgo::audit::report(&result));
             Ok(())
         }
         Command::Serve {
@@ -173,7 +173,7 @@ fn main() -> Result<()> {
             host,
             drafts,
             config,
-        } => org_ssg::serve::run(
+        } => orgo::serve::run(
             &input,
             &output,
             &BuildOptions {
@@ -201,8 +201,8 @@ fn main() -> Result<()> {
 /// Scaffold a working site. Writes only files that do not already exist, so running it
 /// in a directory that has content is safe and additive rather than destructive.
 fn init(dir: &Utf8Path) -> Result<()> {
-    use org_ssg::config::{CONFIG_FILE, STARTER_CONFIG};
-    use org_ssg::template::{
+    use orgo::config::{CONFIG_FILE, STARTER_CONFIG};
+    use orgo::template::{
         starter_template, STARTER_FEED_TEMPLATE, STARTER_LIST_TEMPLATE, STARTER_TAGS_TEMPLATE,
     };
 
@@ -234,7 +234,7 @@ fn init(dir: &Utf8Path) -> Result<()> {
         "#+FILETAGS: :example:\n",
         "\n",
         "Posts in this directory are collected into /blog/ by the [[collections]] block\n",
-        "in org-ssg.toml, newest first.\n",
+        "in orgo.toml, newest first.\n",
     );
 
     let files: [(Utf8PathBuf, &str); 7] = [
@@ -260,7 +260,7 @@ fn init(dir: &Utf8Path) -> Result<()> {
     for path in &created {
         println!("created {path}");
     }
-    println!("\nNext: org-ssg build {dir} -o _site");
+    println!("\nNext: orgo build {dir} -o _site");
     Ok(())
 }
 
@@ -321,7 +321,7 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> {
         word_count: 0,
         reading_time: 0,
         keywords: Default::default(),
-        toc: org_ssg::util::table_of_contents(&resolved.document.root),
+        toc: orgo::util::table_of_contents(&resolved.document.root),
     };
     let mut ctx = RenderContext::new(&site, &page_ctx, &[], SYNTAX_STYLESHEET, "");
     ctx.body = &fragment;
diff --git a/src/render.rs b/src/render.rs
index 65b697c..4496862 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -45,7 +45,7 @@ const CLASS_STYLE: ClassStyle = ClassStyle::Spaced;
 
 /// Syntax definitions syntect does not bundle, compiled into the binary.
 ///
-/// Both are gaps this project hits on its own first page: every `org-ssg.toml` example is
+/// Both are gaps this project hits on its own first page: every `orgo.toml` example is
 /// TOML, and a tool for org users is going to be written about in org. Embedding them
 /// rather than shipping files means they work with no setup, which is the same promise
 /// the rest of the zero-config path makes.
@@ -928,7 +928,7 @@ fn strip_special_column(table: &crate::model::Table) -> crate::model::Table {
 
 /// Split text into alternating prose and LaTeX spans, `(text, is_latex)`.
 ///
-/// org-ssg does not typeset LaTeX — it passes it through for MathJax or a reader's eyes —
+/// orgo does not typeset LaTeX — it passes it through for MathJax or a reader's eyes —
 /// but it must know where a fragment *is*, because the export-time text conversions would
 /// otherwise rewrite the mathematics: `x^2` inside `$…$` is not a superscript to be
 /// marked up, and `--` inside one is a minus sign twice.
diff --git a/src/serve.rs b/src/serve.rs
index 228c8b7..d335ef8 100644
--- a/src/serve.rs
+++ b/src/serve.rs
@@ -30,7 +30,7 @@ use crate::site::{build_site, BuildOptions};
 
 /// Where the browser subscribes for reload events. Namespaced so it cannot collide with
 /// a real page.
-pub const RELOAD_PATH: &str = "/__org-ssg/reload";
+pub const RELOAD_PATH: &str = "/__orgo/reload";
 
 /// How long a poll waits before answering "nothing yet". Long enough that an idle tab is
 /// nearly silent, short enough to stay under any proxy or browser idle timeout.
diff --git a/src/site.rs b/src/site.rs
index 84aced3..61093f6 100644
--- a/src/site.rs
+++ b/src/site.rs
@@ -62,7 +62,7 @@ pub struct BuildOptions {
     pub no_cache: bool,
     /// Treat broken internal links as a build error rather than a warning (spec §4.3.4).
     pub strict: bool,
-    /// Explicit config file, overriding `org-ssg.toml` in the source directory.
+    /// Explicit config file, overriding `orgo.toml` in the source directory.
     pub config_path: Option<Utf8PathBuf>,
     /// Include pages marked `#+DRAFT:`, overriding `build.drafts` when set.
     pub drafts: bool,
@@ -1368,7 +1368,7 @@ fn collect_assets(
 /// directory (build input, not content) and the output directory when it lives inside
 /// the source.
 ///
-/// The output case is not a corner case — `org-ssg build . -o _site` is the obvious
+/// The output case is not a corner case — `orgo build . -o _site` is the obvious
 /// thing to type, and without this the build copies its own output back into itself,
 /// growing `_site/_site/_site/…` on every run.
 fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec<Utf8PathBuf> {
diff --git a/src/template.rs b/src/template.rs
index 541b33e..dc12960 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -375,7 +375,7 @@ impl<'a> RenderContext<'a> {
     }
 }
 
-/// The starter tag-index template written by `org-ssg init`: shows how `groups` is
+/// The starter tag-index template written by `orgo init`: shows how `groups` is
 /// iterated, and how a group page is linked.
 pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html>
 <html lang="{{ site.language }}">
@@ -410,7 +410,7 @@ pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html>
 </html>
 "#;
 
-/// The starter listing template written by `org-ssg init`: a blog index, showing how a
+/// The starter listing template written by `orgo init`: a blog index, showing how a
 /// collection's `pages` are iterated.
 pub const STARTER_LIST_TEMPLATE: &str = r#"<!DOCTYPE html>
 <html lang="{{ site.language }}">
@@ -486,7 +486,7 @@ fn add_filters(env: &mut Environment<'static>, base_url: &str) {
                 return Err(minijinja::Error::new(
                     minijinja::ErrorKind::InvalidOperation,
                     "the `absolute` filter needs site.base_url, which is empty; \
-                     set it in org-ssg.toml (e.g. base_url = \"https://example.com\")",
+                     set it in orgo.toml (e.g. base_url = \"https://example.com\")",
                 ));
             }
             if path.starts_with("http://") || path.starts_with("https://") {
@@ -538,7 +538,7 @@ fn add_filters(env: &mut Environment<'static>, base_url: &str) {
     });
 }
 
-/// The starter RSS feed written by `org-ssg init`. A listing page with an XML template:
+/// The starter RSS feed written by `orgo init`. A listing page with an XML template:
 /// no feed-specific machinery, just `absolute` and `rfc822` doing what syndication needs.
 ///
 /// Emitted commented-out guidance rather than a broken feed when `site.base_url` is
@@ -667,7 +667,7 @@ fn render_error_detail(error: minijinja::Error) -> String {
     out
 }
 
-/// The starter layout written by `org-ssg init`: the built-in template, on disk, ready
+/// The starter layout written by `orgo init`: the built-in template, on disk, ready
 /// to edit.
 pub fn starter_template() -> &'static str {
     BASE_TEMPLATE
diff --git a/src/watch.rs b/src/watch.rs
index 63dd56f..48fed29 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -6,7 +6,7 @@
 //!
 //! Two things matter more than the watching itself:
 //!
-//! 1. **Not watching our own output.** `org-ssg watch . -o _site` puts the output inside
+//! 1. **Not watching our own output.** `orgo watch . -o _site` puts the output inside
 //!    the source. Rebuilding writes files, writing files raises events, and events
 //!    trigger a rebuild — a loop that never stops and never idles. [`ChangeFilter`] is
 //!    what prevents it, and it is a pure function precisely so it can be tested without
diff --git a/syntaxes/Org.sublime-syntax b/syntaxes/Org.sublime-syntax
index 38ea843..ef48df7 100644
--- a/syntaxes/Org.sublime-syntax
+++ b/syntaxes/Org.sublime-syntax
@@ -5,7 +5,7 @@
 # needs it.
 #
 # This highlights org as *source text you are reading about*, which is a different job
-# from parsing it: org-ssg's own parser (src/parser.rs) is what turns org into a
+# from parsing it: orgo's own parser (src/parser.rs) is what turns org into a
 # document. Where the two could disagree, this one stays conservative — a highlighter
 # that colours something wrongly is a cosmetic bug, and one that swallows a line is not.
 name: Org
diff --git a/syntaxes/TOML.sublime-syntax b/syntaxes/TOML.sublime-syntax
index 424c9cf..bc093e5 100644
--- a/syntaxes/TOML.sublime-syntax
+++ b/syntaxes/TOML.sublime-syntax
@@ -1,7 +1,7 @@
 %YAML 1.2
 ---
 # TOML, for syntect. Not one of the definitions syntect bundles, and the first thing a
-# config-heavy site needs — every org-ssg.toml example in the documentation is one.
+# config-heavy site needs — every orgo.toml example in the documentation is one.
 #
 # Scope names are the standard TextMate ones, so any syntect theme colours this without
 # knowing it exists.
diff --git a/tests/config.rs b/tests/config.rs
index b190932..054b1ba 100644
--- a/tests/config.rs
+++ b/tests/config.rs
@@ -2,7 +2,7 @@
 //! generator for one site or for anyone's.
 //!
 //! The theme running through these tests is that **the zero-config path has to work**.
-//! A directory of `.org` files with no `org-ssg.toml`, no templates and no knowledge of
+//! A directory of `.org` files with no `orgo.toml`, no templates and no knowledge of
 //! this tool must build into a real site; configuration is how you change the output,
 //! never how you make it work at all.
 
@@ -10,15 +10,15 @@ use std::sync::atomic::{AtomicU32, Ordering};
 
 use camino::Utf8PathBuf;
 
-use org_ssg::config::{Config, NavMode};
-use org_ssg::site::{build_site, BuildOptions};
+use orgo::config::{Config, NavMode};
+use orgo::site::{build_site, BuildOptions};
 
 fn tmpdir(tag: &str) -> Utf8PathBuf {
     static N: AtomicU32 = AtomicU32::new(0);
     let n = N.fetch_add(1, Ordering::Relaxed);
     let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
         .expect("utf-8 temp dir")
-        .join(format!("org-ssg-cfg-{}-{tag}-{n}", std::process::id()));
+        .join(format!("orgo-cfg-{}-{tag}-{n}", std::process::id()));
     let _ = std::fs::remove_dir_all(&base);
     std::fs::create_dir_all(&base).unwrap();
     base
@@ -36,7 +36,7 @@ fn write_site(src: &Utf8PathBuf) {
     .unwrap();
 }
 
-fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> org_ssg::site::SiteReport {
+fn build(src: &Utf8PathBuf, out: &Utf8PathBuf) -> orgo::site::SiteReport {
     build_site(src, out, &BuildOptions::default()).expect("build")
 }
 
@@ -81,9 +81,9 @@ fn a_malformed_config_is_an_error_but_a_missing_one_is_not() {
 
     assert_eq!(Config::load(&src).unwrap(), Config::default());
 
-    std::fs::write(src.join("org-ssg.toml"), "[site\ntitle = broken").unwrap();
+    std::fs::write(src.join("orgo.toml"), "[site\ntitle = broken").unwrap();
     let err = Config::load(&src).expect_err("malformed config must fail");
-    assert!(format!("{err:#}").contains("org-ssg.toml"), "names the file: {err:#}");
+    assert!(format!("{err:#}").contains("orgo.toml"), "names the file: {err:#}");
 }
 
 /// A misspelled key is a silent no-op in most config formats, which is exactly how
@@ -93,7 +93,7 @@ fn an_unknown_config_key_is_rejected() {
     let root = tmpdir("unknownkey");
     let src = root.join("src");
     std::fs::create_dir_all(&src).unwrap();
-    std::fs::write(src.join("org-ssg.toml"), "[site]\ntittle = \"typo\"\n").unwrap();
+    std::fs::write(src.join("orgo.toml"), "[site]\ntittle = \"typo\"\n").unwrap();
 
     let err = Config::load(&src).expect_err("unknown key must fail");
     assert!(
@@ -126,7 +126,7 @@ fn nav_modes_select_different_pages() {
         std::fs::create_dir_all(&src).unwrap();
         write_site(&src);
         std::fs::write(
-            src.join("org-ssg.toml"),
+            src.join("orgo.toml"),
             format!("[nav]\nmode = \"{mode}\"\n"),
         )
         .unwrap();
@@ -155,7 +155,7 @@ fn explicit_nav_uses_the_configured_order() {
     std::fs::create_dir_all(&src).unwrap();
     write_site(&src);
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[nav]\nmode = \"explicit\"\npages = [\"blog/post.org\", \"index.org\"]\n",
     )
     .unwrap();
@@ -178,7 +178,7 @@ fn explicit_nav_rejects_a_page_that_does_not_exist() {
     std::fs::create_dir_all(&src).unwrap();
     write_site(&src);
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[nav]\nmode = \"explicit\"\npages = [\"nope.org\"]\n",
     )
     .unwrap();
@@ -304,7 +304,7 @@ fn the_page_list_is_opt_in_and_widens_invalidation() {
     std::fs::create_dir_all(src.join("templates")).unwrap();
     write_site(&src);
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[templates]\nexpose_page_list = true\n",
     )
     .unwrap();
@@ -352,7 +352,7 @@ fn heading_offset_shifts_content_headings_below_the_page_title() {
         "a level-1 org heading renders as <h2> by default"
     );
 
-    std::fs::write(src.join("org-ssg.toml"), "[html]\nheading_offset = 0\n").unwrap();
+    std::fs::write(src.join("orgo.toml"), "[html]\nheading_offset = 0\n").unwrap();
     let out2 = root.join("out2");
     build(&src, &out2);
     assert!(
@@ -369,7 +369,7 @@ fn an_unknown_highlight_theme_is_rejected_with_the_available_ones() {
     let src = root.join("src");
     std::fs::create_dir_all(&src).unwrap();
     write_site(&src);
-    std::fs::write(src.join("org-ssg.toml"), "[highlight]\ntheme = \"nope\"\n").unwrap();
+    std::fs::write(src.join("orgo.toml"), "[highlight]\ntheme = \"nope\"\n").unwrap();
 
     let err = build_site(&src, &root.join("out"), &BuildOptions::default())
         .expect_err("unknown theme must fail");
@@ -385,7 +385,7 @@ fn an_unknown_highlight_theme_is_rejected_with_the_available_ones() {
 // Discovery
 // ---------------------------------------------------------------------------
 
-/// `org-ssg build . -o _site` is the obvious thing to type. Without excluding the output
+/// `orgo build . -o _site` is the obvious thing to type. Without excluding the output
 /// directory, the build copies its own output back into itself, growing `_site/_site/…`
 /// on every run.
 #[test]
@@ -421,7 +421,7 @@ fn dot_directories_and_build_inputs_are_never_published() {
     write_site(&src);
     std::fs::write(src.join(".git/config"), "[remote]\nurl = private\n").unwrap();
     std::fs::write(src.join(".env"), "SECRET=hunter2\n").unwrap();
-    std::fs::write(src.join("org-ssg.toml"), "[site]\ntitle = \"T\"\n").unwrap();
+    std::fs::write(src.join("orgo.toml"), "[site]\ntitle = \"T\"\n").unwrap();
     std::fs::write(src.join("templates/base.html"), "<html>{{ body | safe }}</html>").unwrap();
     std::fs::write(src.join("style.css"), "body{}\n").unwrap();
     let out = root.join("out");
@@ -430,7 +430,7 @@ fn dot_directories_and_build_inputs_are_never_published() {
     assert!(!out.join(".git").exists(), ".git must never be published");
     assert!(!out.join(".env").exists(), "dotfiles must never be published");
     assert!(
-        !out.join("org-ssg.toml").exists(),
+        !out.join("orgo.toml").exists(),
         "the config is a build input, not content"
     );
     assert!(
@@ -472,7 +472,7 @@ fn write_blog(src: &Utf8PathBuf, extra_config: &str) {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         format!(
             "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
              template = \"list.html\"\ntitle = \"Blog\"\n{extra_config}"
@@ -758,12 +758,12 @@ fn a_feed_is_just_a_listing_page_with_an_xml_template() {
          <pubDate>{{ p.date_iso }}</pubDate></item>{% endfor %}</channel></rss>",
     )
     .unwrap();
-    let mut config = std::fs::read_to_string(src.join("org-ssg.toml")).unwrap();
+    let mut config = std::fs::read_to_string(src.join("orgo.toml")).unwrap();
     config.push_str(
         "\n[[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
          template = \"feed.xml\"\ntitle = \"Feed\"\n",
     );
-    std::fs::write(src.join("org-ssg.toml"), config).unwrap();
+    std::fs::write(src.join("orgo.toml"), config).unwrap();
     let out = root.join("out");
     build(&src, &out);
 
@@ -833,16 +833,16 @@ fn colliding_collection_outputs_are_rejected() {
     std::fs::create_dir_all(&src).unwrap();
     write_blog(&src, "");
 
-    let mut config = std::fs::read_to_string(src.join("org-ssg.toml")).unwrap();
+    let mut config = std::fs::read_to_string(src.join("orgo.toml")).unwrap();
     config.push_str("\n[[collections]]\nsource = \"\"\noutput = \"blog/index.html\"\n");
-    std::fs::write(src.join("org-ssg.toml"), &config).unwrap();
+    std::fs::write(src.join("orgo.toml"), &config).unwrap();
     let err = build_site(&src, &root.join("out"), &BuildOptions::default())
         .expect_err("two collections writing one file must fail");
     assert!(format!("{err:#}").contains("blog/index.html"), "{err:#}");
 
     // And a listing that would overwrite a real page.
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\noutput = \"index.html\"\ntemplate = \"list.html\"\n",
     )
     .unwrap();
@@ -859,7 +859,7 @@ fn a_missing_collection_template_names_the_ones_that_exist() {
     std::fs::create_dir_all(&src).unwrap();
     write_blog(&src, "");
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\ntemplate = \"nope.html\"\n",
     )
     .unwrap();
@@ -912,7 +912,7 @@ fn write_tagged_blog(src: &Utf8PathBuf, extra: &str) {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         format!(
             "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
              output = \"tags/{{tag}}.html\"\ntemplate = \"tag.html\"\ntitle = \"Tagged: {{tag}}\"\n\
@@ -1044,7 +1044,7 @@ fn a_collection_can_group_by_any_keyword() {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\ngroup_by = \"category\"\n\
          output = \"cat/{tag}.html\"\ntemplate = \"tag.html\"\ntitle = \"{tag}\"\n",
     )
@@ -1081,7 +1081,7 @@ fn grouping_without_a_placeholder_is_rejected() {
     std::fs::create_dir_all(&src).unwrap();
     write_tagged_blog(&src, "");
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
          output = \"tags/all.html\"\ntemplate = \"tag.html\"\n",
     )
@@ -1149,7 +1149,7 @@ fn write_paginated_blog(src: &Utf8PathBuf, count: usize, extra: &str) {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         format!(
             "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
              template = \"list.html\"\ntitle = \"Blog\"\n{extra}"
@@ -1275,7 +1275,7 @@ fn groups_paginate_independently() {
         .unwrap();
     }
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
          output = \"tags/{tag}.html\"\ntemplate = \"list.html\"\ntitle = \"{tag}\"\n\
          paginate = 2\npaginate_output = \"tags/{tag}/page/{n}.html\"\n",
@@ -1317,7 +1317,7 @@ fn pagination_placeholders_are_validated() {
 
     // Grouped without {tag} in the page pattern.
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\ngroup_by = \"tags\"\n\
          output = \"tags/{tag}.html\"\ntemplate = \"list.html\"\n\
          paginate = 2\npaginate_output = \"tags/page/{n}.html\"\n",
@@ -1380,7 +1380,7 @@ fn write_feed_site(src: &Utf8PathBuf, base_url: &str) {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         format!(
             "[site]\nbase_url = \"{base_url}\"\n\n\
              [[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
@@ -1442,7 +1442,7 @@ fn absolute_without_a_base_url_is_an_error_that_says_what_to_set() {
         .expect_err("absolute with no base_url must fail");
     let message = format!("{err:#}");
     assert!(message.contains("base_url"), "names the setting: {message}");
-    assert!(message.contains("org-ssg.toml"), "names where to set it: {message}");
+    assert!(message.contains("orgo.toml"), "names where to set it: {message}");
     assert!(message.contains("feed.xml"), "names the template: {message}");
 }
 
@@ -1484,7 +1484,7 @@ fn the_default_layout_emits_a_canonical_link_only_with_a_base_url() {
         std::fs::create_dir_all(&src).unwrap();
         write_site(&src);
         std::fs::write(
-            src.join("org-ssg.toml"),
+            src.join("orgo.toml"),
             format!("[site]\nbase_url = \"{base}\"\n"),
         )
         .unwrap();
@@ -1509,7 +1509,7 @@ fn changing_base_url_re_renders_the_site() {
     std::fs::create_dir_all(&src).unwrap();
     write_site(&src);
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[site]\nbase_url = \"https://example.com\"\n",
     )
     .unwrap();
@@ -1518,7 +1518,7 @@ fn changing_base_url_re_renders_the_site() {
     assert!(build(&src, &out).rendered.is_empty(), "unchanged rebuild renders nothing");
 
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[site]\nbase_url = \"https://moved.example\"\n",
     )
     .unwrap();
@@ -1556,7 +1556,7 @@ fn write_excerpt_site(src: &Utf8PathBuf, extra_config: &str) {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         format!(
             "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
              template = \"list.html\"\ntitle = \"Blog\"\n{extra_config}"
@@ -1731,9 +1731,9 @@ fn a_link_to_a_draft_is_reported_as_broken() {
 /// "no" has to mean no.
 #[test]
 fn draft_truthiness_is_forgiving_but_respects_an_explicit_negative() {
-    use org_ssg::model::Keywords;
+    use orgo::model::Keywords;
     let draft = |value: &str| {
-        org_ssg::util::is_draft(&Keywords {
+        orgo::util::is_draft(&Keywords {
             entries: vec![("DRAFT".to_string(), value.to_string())],
         })
     };
@@ -1744,7 +1744,7 @@ fn draft_truthiness_is_forgiving_but_respects_an_explicit_negative() {
         assert!(!draft(no), "{no:?} should mean published");
     }
     assert!(
-        !org_ssg::util::is_draft(&Keywords::default()),
+        !orgo::util::is_draft(&Keywords::default()),
         "no keyword at all means published"
     );
 }
@@ -1773,7 +1773,7 @@ fn write_toc_site(src: &Utf8PathBuf, options: &str, config: &str) {
          <nav>{{ walk(page.toc) }}</nav>{{ body | safe }}</body></html>",
     )
     .unwrap();
-    std::fs::write(src.join("org-ssg.toml"), config).unwrap();
+    std::fs::write(src.join("orgo.toml"), config).unwrap();
 }
 
 /// A table of contents is a tree, and reconstructing one from a flat list of levels
@@ -1889,7 +1889,7 @@ fn section_numbering_resets_at_each_level() {
          * One\n** A\n** B\n* Two\n** C\n*** Deep\n* Three\n",
     )
     .unwrap();
-    std::fs::write(src.join("org-ssg.toml"), "").unwrap();
+    std::fs::write(src.join("orgo.toml"), "").unwrap();
     let out = root.join("out");
     build(&src, &out);
 
@@ -1914,8 +1914,8 @@ fn section_numbering_resets_at_each_level() {
 /// `#+OPTIONS:` is a space-separated list of switches, and org spells "off" several ways.
 #[test]
 fn export_options_parse_as_org_writes_them() {
-    use org_ssg::model::Keywords;
-    use org_ssg::util::option_enabled;
+    use orgo::model::Keywords;
+    use orgo::util::option_enabled;
     let keywords = |v: &str| Keywords {
         entries: vec![("OPTIONS".to_string(), v.to_string())],
     };
@@ -1955,7 +1955,7 @@ fn write_two_layouts(src: &Utf8PathBuf, config: &str) {
          <p>Reply by email</p></body></html>",
     )
     .unwrap();
-    std::fs::write(src.join("org-ssg.toml"), config).unwrap();
+    std::fs::write(src.join("orgo.toml"), config).unwrap();
 }
 
 /// A section's layout is a property of the section: one rule covers every page under it,
@@ -2124,7 +2124,7 @@ fn adding_a_pages_rule_rerenders_the_pages_it_covers() {
     build(&src, &out);
 
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[pages]]\nmatch = \"blog\"\ntemplate = \"post.html\"\n",
     )
     .unwrap();
@@ -2142,7 +2142,7 @@ fn adding_a_pages_rule_rerenders_the_pages_it_covers() {
 #[test]
 fn a_pages_rule_without_a_template_is_rejected() {
     let mut config = Config::default();
-    config.pages.push(org_ssg::config::PageRule {
+    config.pages.push(orgo::config::PageRule {
         pattern: Utf8PathBuf::from("blog"),
         template: String::new(),
     });
@@ -2211,7 +2211,7 @@ fn same_day_entries_sort_by_time_of_day() {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\noutput = \"blog/index.html\"\n\
          template = \"list.html\"\ntitle = \"Blog\"\nsort = \"date\"\norder = \"desc\"\n",
     )
@@ -2249,7 +2249,7 @@ fn an_asset_root_publishes_to_the_site_root() {
     std::fs::write(root.join("theme/static/robots.txt"), "User-agent: *\n").unwrap();
     std::fs::write(root.join("theme/static/img/logo.svg"), "<svg/>").unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[build]\nassets = [\"../theme/static\"]\n",
     )
     .unwrap();
@@ -2280,7 +2280,7 @@ fn two_assets_claiming_one_url_is_an_error() {
     std::fs::create_dir_all(root.join("static")).unwrap();
     std::fs::write(root.join("static/style.css"), "body{color:red}").unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[build]\nassets = [\"../static\"]\n",
     )
     .unwrap();
@@ -2301,7 +2301,7 @@ fn a_missing_asset_root_is_an_error() {
     std::fs::create_dir_all(&src).unwrap();
     write_site(&src);
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[build]\nassets = [\"../nope\"]\n",
     )
     .unwrap();
@@ -2330,7 +2330,7 @@ fn a_collection_can_carry_its_entries_rendered_bodies() {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[collections]]\nsource = \"blog\"\noutput = \"feed.xml\"\n\
          template = \"feed.xml\"\ntitle = \"Feed\"\ninclude_content = true\n",
     )
diff --git a/tests/constructs.rs b/tests/constructs.rs
index fa1df2f..838fa7f 100644
--- a/tests/constructs.rs
+++ b/tests/constructs.rs
@@ -13,10 +13,10 @@
 
 use camino::Utf8PathBuf;
 
-use org_ssg::model::Document;
-use org_ssg::parser::parse;
-use org_ssg::render::{render, Html, SyntectHighlighter};
-use org_ssg::resolve::ResolvedDoc;
+use orgo::model::Document;
+use orgo::parser::parse;
+use orgo::render::{render, Html, SyntectHighlighter};
+use orgo::resolve::ResolvedDoc;
 
 fn parse_fixture(name: &str) -> Document {
     let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -160,7 +160,7 @@ fn highlighting_emits_classes_not_inline_styles() {
         "highlighting must not emit inline styles:\n{html}"
     );
     assert!(
-        org_ssg::render::syntax_css("InspiredGitHub")
+        orgo::render::syntax_css("InspiredGitHub")
             .expect("a built-in theme")
             .contains(".storage"),
         "the generated stylesheet must define the emitted classes"
@@ -559,7 +559,7 @@ fn special_strings_leave_code_alone() {
     );
 }
 
-/// `#+OPTIONS: -:nil` is how a document opts out, and org-ssg honours org's own switch
+/// `#+OPTIONS: -:nil` is how a document opts out, and orgo honours org's own switch
 /// rather than inventing one.
 #[test]
 fn a_document_can_turn_special_strings_off() {
diff --git a/tests/harness.rs b/tests/harness.rs
index 47510b2..edd66b6 100644
--- a/tests/harness.rs
+++ b/tests/harness.rs
@@ -5,8 +5,8 @@
 //! - an `insta` JSON snapshot of a hand-built element-tree value, standing in for the
 //!   element-tree snapshots the parser will produce from Phase 1 onward.
 
-use org_ssg::model::{Link, LinkTarget, Object};
-use org_ssg::parser::content_hash;
+use orgo::model::{Link, LinkTarget, Object};
+use orgo::parser::content_hash;
 
 #[test]
 fn content_hash_is_deterministic_blake3() {
diff --git a/tests/incremental.rs b/tests/incremental.rs
index 40a7cfb..68bcddf 100644
--- a/tests/incremental.rs
+++ b/tests/incremental.rs
@@ -16,8 +16,8 @@ use std::sync::atomic::{AtomicU32, Ordering};
 
 use camino::Utf8PathBuf;
 
-use org_ssg::incremental::{manifest_path, Manifest, CACHE_FORMAT_VERSION};
-use org_ssg::site::{build_site, BuildOptions};
+use orgo::incremental::{manifest_path, Manifest, CACHE_FORMAT_VERSION};
+use orgo::site::{build_site, BuildOptions};
 
 /// A fresh, empty temp directory unique to this process + call.
 fn tmpdir(tag: &str) -> Utf8PathBuf {
@@ -25,7 +25,7 @@ fn tmpdir(tag: &str) -> Utf8PathBuf {
     let n = N.fetch_add(1, Ordering::Relaxed);
     let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
         .expect("utf-8 temp dir")
-        .join(format!("org-ssg-it-{}-{tag}-{n}", std::process::id()));
+        .join(format!("orgo-it-{}-{tag}-{n}", std::process::id()));
     if base.exists() {
         std::fs::remove_dir_all(&base).unwrap();
     }
@@ -47,7 +47,7 @@ fn output_files(out: &Utf8PathBuf) -> BTreeMap<String, Vec<u8>> {
             continue;
         }
         let path = Utf8PathBuf::from_path_buf(entry.path().to_owned()).unwrap();
-        if path.file_name() == Some(".org-ssg-cache.json") {
+        if path.file_name() == Some(".orgo-cache.json") {
             continue;
         }
         let rel = path.strip_prefix(out).unwrap().to_string();
@@ -476,7 +476,7 @@ fn editing_one_template_rebuilds_only_the_pages_that_use_it() {
     )
     .unwrap();
     std::fs::write(
-        src.join("org-ssg.toml"),
+        src.join("orgo.toml"),
         "[[pages]]\nmatch = \"blog\"\ntemplate = \"post.html\"\n",
     )
     .unwrap();
diff --git a/tests/oracle.el b/tests/oracle.el
index 2a3b0ce..a3897c9 100644
--- a/tests/oracle.el
+++ b/tests/oracle.el
@@ -1,4 +1,4 @@
-;;; oracle.el --- ground-truth HTML export for the org-ssg differential tests  -*- lexical-binding: t -*-
+;;; oracle.el --- ground-truth HTML export for the orgo differential tests  -*- lexical-binding: t -*-
 
 ;; Exports the org file named by $ORG_ORACLE_INPUT to HTML on stdout, using org's own
 ;; exporter — the same one weblorg wraps to publish the corpus this project targets.
@@ -16,7 +16,7 @@
 ;; learn what stock org does — normalizing that away would be marking our own homework.
 (setq org-export-with-toc nil              ; we emit no table of contents
       org-export-with-section-numbers nil  ; we do not number headings
-      ;; org-html-toplevel-hlevel is left at its default of 2. org-ssg's own default
+      ;; org-html-toplevel-hlevel is left at its default of 2. orgo's own default
       ;; heading_offset is 1, which produces the same <h2>, so both sides now agree
       ;; without the oracle being told to.
       org-html-htmlize-output-type nil     ; plain <pre>, not htmlize spans: we highlight
@@ -24,7 +24,7 @@
                                            ; the meaningful part
       org-html-head-include-default-style nil
       org-html-head-include-scripts nil
-      ;; Fixtures link to ids that live in org-ssg's own symbol table, not in an
+      ;; Fixtures link to ids that live in orgo's own symbol table, not in an
       ;; `org-id' database. Without this, org aborts the whole export on the first one.
       org-export-with-broken-links t
       make-backup-files nil)
diff --git a/tests/oracle.rs b/tests/oracle.rs
index 209282e..ba8a5fc 100644
--- a/tests/oracle.rs
+++ b/tests/oracle.rs
@@ -1,6 +1,6 @@
 //! The `emacs --batch` ground-truth oracle (spec §5, Phase 0).
 //!
-//! Every other test in this suite checks org-ssg against org-ssg: a snapshot says our
+//! Every other test in this suite checks orgo against orgo: a snapshot says our
 //! output has not *changed*, never that it is *right*. Those two questions are different,
 //! and only one of them matters to someone whose site is currently published by Emacs.
 //! This file answers the second by exporting the same fixture with org's own HTML
@@ -26,9 +26,9 @@ use std::process::Command;
 
 use camino::Utf8PathBuf;
 
-use org_ssg::parser::parse;
-use org_ssg::render::{render, Html, SyntectHighlighter};
-use org_ssg::resolve::ResolvedDoc;
+use orgo::parser::parse;
+use orgo::render::{render, Html, SyntectHighlighter};
+use orgo::resolve::ResolvedDoc;
 
 fn manifest_dir() -> Utf8PathBuf {
     Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -62,7 +62,7 @@ fn org_export(fixture: &str) -> String {
     String::from_utf8(output.stdout).expect("emacs emits UTF-8")
 }
 
-/// Render a fixture with org-ssg.
+/// Render a fixture with orgo.
 fn our_export(fixture: &str) -> String {
     let path = manifest_dir().join("fixtures").join(fixture);
     let source = std::fs::read_to_string(&path).expect("read fixture");
@@ -252,12 +252,12 @@ fn decode_entities(s: &str) -> String {
 // Divergence report
 // ---------------------------------------------------------------------------
 
-/// One divergence org-ssg makes on purpose, so the report can separate "we chose this"
+/// One divergence orgo makes on purpose, so the report can separate "we chose this"
 /// from "we got this wrong".
 ///
 /// Without this split the agreement percentage is noise: the timestamps fixture sat at
 /// 40% while being entirely correct, because org writes `<2024-01-15 Mon>` as text and
-/// org-ssg writes a `<time datetime>` element. A number that cannot fall when a real
+/// orgo writes a `<time datetime>` element. A number that cannot fall when a real
 /// defect appears is not measuring anything.
 struct Deliberate {
     name: &'static str,
@@ -335,7 +335,7 @@ const DELIBERATE: &[Deliberate] = &[
         },
         in_notes_only: false,
     },
-    // Org emits a `<colgroup>` of empty `<col>`s to carry column alignment; org-ssg
+    // Org emits a `<colgroup>` of empty `<col>`s to carry column alignment; orgo
     // leaves alignment to the stylesheet.
     Deliberate {
         name: "no-colgroup",
@@ -511,7 +511,7 @@ fn align(ours: &[String], theirs: &[String]) -> Vec<Op> {
 }
 
 /// A unified diff of the two skeletons, with hunks that are deliberate collapsed to a
-/// named line. `-` is org-ssg, `+` is Emacs.
+/// named line. `-` is orgo, `+` is Emacs.
 ///
 /// The number that matters is the last one: *unexplained* lines. Agreement can be low
 /// while unexplained is zero, and that is a passing state.
@@ -566,7 +566,7 @@ fn divergence(ours: &[String], theirs: &[String]) -> String {
         "agreement: {agreed}/{total} skeleton lines ({pct:.1}%)\n\
          deliberate: {deliberate} line(s){}\n\
          unexplained: {unexplained} line(s)\n\
-         (- org-ssg, + emacs, ~ a difference we mean to have)\n\n{body}",
+         (- orgo, + emacs, ~ a difference we mean to have)\n\n{body}",
         if rules.is_empty() {
             String::new()
         } else {
@@ -575,7 +575,7 @@ fn divergence(ours: &[String], theirs: &[String]) -> String {
     )
 }
 
-/// Snapshot the divergence between org-ssg and Emacs for one fixture.
+/// Snapshot the divergence between orgo and Emacs for one fixture.
 fn compare(fixture: &str) -> Option<String> {
     if !emacs_available() {
         eprintln!("skipping oracle comparison for {fixture}: no emacs on PATH");
@@ -612,7 +612,7 @@ oracle_test!(oracle_elements, "elements.org");
 ///
 /// The percentages above are context, not a target — the timestamps fixture agrees on
 /// 40% of its lines and is entirely correct, because org writes a date as text where
-/// org-ssg writes `<time datetime>`. What must hold is that nothing diverges for a
+/// orgo writes `<time datetime>`. What must hold is that nothing diverges for a
 /// reason nobody has written down. A new unexplained line means either a defect to fix
 /// or a decision to record in `DELIBERATE`.
 #[test]
diff --git a/tests/pipeline.rs b/tests/pipeline.rs
index 4457e2c..086de84 100644
--- a/tests/pipeline.rs
+++ b/tests/pipeline.rs
@@ -6,10 +6,10 @@
 
 use camino::Utf8PathBuf;
 
-use org_ssg::model::Document;
-use org_ssg::parser::parse;
-use org_ssg::render::{render, Html, SyntectHighlighter};
-use org_ssg::resolve::ResolvedDoc;
+use orgo::model::Document;
+use orgo::parser::parse;
+use orgo::render::{render, Html, SyntectHighlighter};
+use orgo::resolve::ResolvedDoc;
 
 fn parse_fixture(name: &str) -> Document {
     let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
diff --git a/tests/serve.rs b/tests/serve.rs
index 309925f..1462eff 100644
--- a/tests/serve.rs
+++ b/tests/serve.rs
@@ -12,15 +12,15 @@ use std::time::{Duration, Instant};
 
 use camino::{Utf8Path, Utf8PathBuf};
 
-use org_ssg::serve::{inject_reload_script, resolve, since_parameter};
-use org_ssg::site::BuildOptions;
+use orgo::serve::{inject_reload_script, resolve, since_parameter};
+use orgo::site::BuildOptions;
 
 fn tmpdir(tag: &str) -> Utf8PathBuf {
     static N: AtomicU32 = AtomicU32::new(0);
     let n = N.fetch_add(1, Ordering::Relaxed);
     let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
         .expect("utf-8 temp dir")
-        .join(format!("org-ssg-serve-{}-{tag}-{n}", std::process::id()));
+        .join(format!("orgo-serve-{}-{tag}-{n}", std::process::id()));
     let _ = std::fs::remove_dir_all(&base);
     std::fs::create_dir_all(&base).unwrap();
     base
@@ -107,10 +107,10 @@ fn plus_is_not_decoded_as_a_space() {
 
 #[test]
 fn the_poll_parameter_is_read_from_the_query() {
-    assert_eq!(since_parameter("/__org-ssg/reload?since=7"), 7);
-    assert_eq!(since_parameter("/__org-ssg/reload?x=1&since=42"), 42);
-    assert_eq!(since_parameter("/__org-ssg/reload"), 0, "absent means start from zero");
-    assert_eq!(since_parameter("/__org-ssg/reload?since=nope"), 0, "unparseable means zero");
+    assert_eq!(since_parameter("/__orgo/reload?since=7"), 7);
+    assert_eq!(since_parameter("/__orgo/reload?x=1&since=42"), 42);
+    assert_eq!(since_parameter("/__orgo/reload"), 0, "absent means start from zero");
+    assert_eq!(since_parameter("/__orgo/reload?since=nope"), 0, "unparseable means zero");
 }
 
 // ---------------------------------------------------------------------------
@@ -167,7 +167,7 @@ fn start_server(src: &Utf8Path, out: &Utf8Path) -> u16 {
     let port = 20000 + ((std::process::id() % 10000) as u16) + NEXT.fetch_add(1, Ordering::Relaxed) as u16;
     let (s, o) = (src.to_owned(), out.to_owned());
     std::thread::spawn(move || {
-        let _ = org_ssg::serve::run(&s, &o, &BuildOptions::default(), "127.0.0.1", port);
+        let _ = orgo::serve::run(&s, &o, &BuildOptions::default(), "127.0.0.1", port);
     });
     let deadline = Instant::now() + Duration::from_secs(20);
     while Instant::now() < deadline {
@@ -194,16 +194,16 @@ fn serving_a_site_reloads_the_browser_when_a_source_changes() {
     let home = get(port, "/", Duration::from_secs(5)).expect("a response");
     assert!(home.contains("200 OK"), "{home}");
     assert!(home.contains("First version."), "the page is served: {home}");
-    assert!(home.contains("__org-ssg/reload"), "with the reload script: {home}");
+    assert!(home.contains("__orgo/reload"), "with the reload script: {home}");
     assert!(
-        !std::fs::read_to_string(out.join("index.html")).unwrap().contains("__org-ssg"),
+        !std::fs::read_to_string(out.join("index.html")).unwrap().contains("__orgo"),
         "but the file on disk stays clean"
     );
 
     // A poll for a generation we already have must block, not answer immediately.
     let poller = std::thread::spawn(move || {
         let started = Instant::now();
-        let body = get(port, "/__org-ssg/reload?since=0", Duration::from_secs(30));
+        let body = get(port, "/__orgo/reload?since=0", Duration::from_secs(30));
         (started.elapsed(), body)
     });
     std::thread::sleep(Duration::from_millis(400));
diff --git a/tests/site.rs b/tests/site.rs
index 41568f2..2865ac6 100644
--- a/tests/site.rs
+++ b/tests/site.rs
@@ -6,11 +6,11 @@
 
 use camino::Utf8PathBuf;
 
-use org_ssg::index::{SymbolTable, TargetId};
-use org_ssg::parser::parse;
-use org_ssg::render::{render, Html, SyntectHighlighter};
-use org_ssg::resolve::{resolve, ResolvedDoc};
-use org_ssg::site::{render_site, BuiltPage};
+use orgo::index::{SymbolTable, TargetId};
+use orgo::parser::parse;
+use orgo::render::{render, Html, SyntectHighlighter};
+use orgo::resolve::{resolve, ResolvedDoc};
+use orgo::site::{render_site, BuiltPage};
 
 fn fixtures() -> Utf8PathBuf {
     Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
@@ -169,13 +169,13 @@ fn links_resolve_through_the_slug() {
 /// be impossible by construction rather than by convention.
 #[test]
 fn slugs_cannot_escape_the_output_directory() {
-    use org_ssg::model::Keywords;
+    use orgo::model::Keywords;
     let source = Utf8PathBuf::from("blog/post.org");
     let slugged = |value: &str| {
         let keywords = Keywords {
             entries: vec![("SLUG".to_string(), value.to_string())],
         };
-        org_ssg::util::output_path(&source, &keywords).to_string()
+        orgo::util::output_path(&source, &keywords).to_string()
     };
     assert_eq!(slugged("../../etc/passwd"), "blog/etc-passwd.html");
     assert_eq!(slugged("/absolute"), "blog/absolute.html");
@@ -190,7 +190,7 @@ fn slugs_cannot_escape_the_output_directory() {
 /// invisible in the source filenames, so the build refuses rather than picking a winner.
 #[test]
 fn colliding_slugs_are_a_build_error() {
-    let dir = std::env::temp_dir().join(format!("org-ssg-slug-{}", std::process::id()));
+    let dir = std::env::temp_dir().join(format!("orgo-slug-{}", std::process::id()));
     let dir = Utf8PathBuf::from_path_buf(dir).expect("utf-8 temp dir");
     let _ = std::fs::remove_dir_all(&dir);
     std::fs::create_dir_all(&dir).unwrap();
diff --git a/tests/snapshots/oracle__oracle_blocks.snap b/tests/snapshots/oracle__oracle_blocks.snap
index 2079eb9..b685527 100644
--- a/tests/snapshots/oracle__oracle_blocks.snap
+++ b/tests/snapshots/oracle__oracle_blocks.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 67/77 skeleton lines (87.0%)
 deliberate: 11 line(s) — pre-code ×6, semantic-emphasis ×4, verse-trailing-break ×1
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
   "Quote"
diff --git a/tests/snapshots/oracle__oracle_core.snap b/tests/snapshots/oracle__oracle_core.snap
index 545fb09..3c69768 100644
--- a/tests/snapshots/oracle__oracle_core.snap
+++ b/tests/snapshots/oracle__oracle_core.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 51/58 skeleton lines (87.9%)
 deliberate: 7 line(s) — list-per-bullet-type ×2, pre-code ×2, semantic-emphasis ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <p>
   "Intro paragraph with a bare URL"
diff --git a/tests/snapshots/oracle__oracle_elements.snap b/tests/snapshots/oracle__oracle_elements.snap
index 4afb933..3a9ab15 100644
--- a/tests/snapshots/oracle__oracle_elements.snap
+++ b/tests/snapshots/oracle__oracle_elements.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 64/82 skeleton lines (78.0%)
 deliberate: 26 line(s) — footnote-anchor-naming ×1, footnote-section-shape ×2, id-link-resolution ×1, no-colgroup ×1, pre-code ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
   "Code and tables"
diff --git a/tests/snapshots/oracle__oracle_footnote.snap b/tests/snapshots/oracle__oracle_footnote.snap
index ed3a955..ef95bb3 100644
--- a/tests/snapshots/oracle__oracle_footnote.snap
+++ b/tests/snapshots/oracle__oracle_footnote.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 30/53 skeleton lines (56.6%)
 deliberate: 29 line(s) — footnote-anchor-naming ×3, footnote-section-shape ×6, semantic-emphasis ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <p>
   "Text with a reference."
diff --git a/tests/snapshots/oracle__oracle_headings.snap b/tests/snapshots/oracle__oracle_headings.snap
index f6fc356..7466e54 100644
--- a/tests/snapshots/oracle__oracle_headings.snap
+++ b/tests/snapshots/oracle__oracle_headings.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 28/30 skeleton lines (93.3%)
 deliberate: 2 line(s) — semantic-time ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
 ~ semantic-time (1 line(s))
diff --git a/tests/snapshots/oracle__oracle_images.snap b/tests/snapshots/oracle__oracle_images.snap
index 8a7e0f4..22a79e3 100644
--- a/tests/snapshots/oracle__oracle_images.snap
+++ b/tests/snapshots/oracle__oracle_images.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 30/42 skeleton lines (71.4%)
 deliberate: 14 line(s) — figure-element ×8, semantic-emphasis ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
   "Bare image"
diff --git a/tests/snapshots/oracle__oracle_lists.snap b/tests/snapshots/oracle__oracle_lists.snap
index c510b36..ec87623 100644
--- a/tests/snapshots/oracle__oracle_lists.snap
+++ b/tests/snapshots/oracle__oracle_lists.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 109/111 skeleton lines (98.2%)
 deliberate: 2 line(s) — semantic-emphasis ×2
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
   "Nesting"
diff --git a/tests/snapshots/oracle__oracle_minimal.snap b/tests/snapshots/oracle__oracle_minimal.snap
index c20f493..e7722f8 100644
--- a/tests/snapshots/oracle__oracle_minimal.snap
+++ b/tests/snapshots/oracle__oracle_minimal.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 38/42 skeleton lines (90.5%)
 deliberate: 4 line(s) — semantic-emphasis ×4
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <p>
   "A single paragraph of preamble text before any heading."
diff --git a/tests/snapshots/oracle__oracle_table.snap b/tests/snapshots/oracle__oracle_table.snap
index ed793e9..a0f4639 100644
--- a/tests/snapshots/oracle__oracle_table.snap
+++ b/tests/snapshots/oracle__oracle_table.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 30/34 skeleton lines (88.2%)
 deliberate: 4 line(s) — no-colgroup ×1
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <table>
 ~ no-colgroup (4 line(s))
diff --git a/tests/snapshots/oracle__oracle_timestamps.snap b/tests/snapshots/oracle__oracle_timestamps.snap
index 4f873ab..8879768 100644
--- a/tests/snapshots/oracle__oracle_timestamps.snap
+++ b/tests/snapshots/oracle__oracle_timestamps.snap
@@ -5,7 +5,7 @@ expression: report
 agreement: 25/62 skeleton lines (40.3%)
 deliberate: 37 line(s) — semantic-time ×5
 unexplained: 0 line(s)
-(- org-ssg, + emacs, ~ a difference we mean to have)
+(- orgo, + emacs, ~ a difference we mean to have)
 
   <h2>
   "Single"
diff --git a/tests/snapshots/site__site_guide_html.snap b/tests/snapshots/site__site_guide_html.snap
index a517c5e..b53008c 100644
--- a/tests/snapshots/site__site_guide_html.snap
+++ b/tests/snapshots/site__site_guide_html.snap
@@ -7,12 +7,12 @@ expression: "page(&pages, \"guide.org\").html"
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Guide &middot; org-ssg site</title>
+<title>Guide &middot; orgo site</title>
 <link rel="stylesheet" href="syntax.css">
 </head>
 <body>
 <header>
-<a class="site-title" href="index.html">org-ssg site</a>
+<a class="site-title" href="index.html">orgo site</a>
 <nav>
 <a href="about.html">About</a>
 <a href="#">Guide</a>
diff --git a/tests/snapshots/site__site_index_html.snap b/tests/snapshots/site__site_index_html.snap
index fd2459e..ae92e8d 100644
--- a/tests/snapshots/site__site_index_html.snap
+++ b/tests/snapshots/site__site_index_html.snap
@@ -7,12 +7,12 @@ expression: "page(&pages, \"index.org\").html"
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Home &middot; org-ssg site</title>
+<title>Home &middot; orgo site</title>
 <link rel="stylesheet" href="syntax.css">
 </head>
 <body>
 <header>
-<a class="site-title" href="index.html">org-ssg site</a>
+<a class="site-title" href="index.html">orgo site</a>
 <nav>
 <a href="about.html">About</a>
 <a href="guide.html">Guide</a>
diff --git a/tests/watch.rs b/tests/watch.rs
index 6cb66aa..3c904dc 100644
--- a/tests/watch.rs
+++ b/tests/watch.rs
@@ -1,6 +1,6 @@
 //! `watch`: the change filter, and one end-to-end run against real filesystem events.
 //!
-//! The filter carries the weight here. `org-ssg watch . -o _site` puts the output inside
+//! The filter carries the weight here. `orgo watch . -o _site` puts the output inside
 //! the source, so a rebuild writes files, writing files raises events, and events trigger
 //! a rebuild — a loop that never stops. That it is a pure function is what makes the
 //! guarantee testable without waiting on a filesystem.
@@ -10,15 +10,15 @@ use std::time::{Duration, Instant};
 
 use camino::{Utf8Path, Utf8PathBuf};
 
-use org_ssg::site::{build_site, BuildOptions};
-use org_ssg::watch::ChangeFilter;
+use orgo::site::{build_site, BuildOptions};
+use orgo::watch::ChangeFilter;
 
 fn tmpdir(tag: &str) -> Utf8PathBuf {
     static N: AtomicU32 = AtomicU32::new(0);
     let n = N.fetch_add(1, Ordering::Relaxed);
     let base = Utf8PathBuf::from_path_buf(std::env::temp_dir())
         .expect("utf-8 temp dir")
-        .join(format!("org-ssg-watch-{}-{tag}-{n}", std::process::id()));
+        .join(format!("orgo-watch-{}-{tag}-{n}", std::process::id()));
     let _ = std::fs::remove_dir_all(&base);
     std::fs::create_dir_all(&base).unwrap();
     base
@@ -39,7 +39,7 @@ fn changes_under_the_output_directory_are_ignored() {
     let filter = ChangeFilter::new(&src, &out);
     assert!(!filter.is_relevant(Utf8Path::new("_site/index.html")));
     assert!(!filter.is_relevant(Utf8Path::new("_site/blog/post.html")));
-    assert!(!filter.is_relevant(Utf8Path::new("_site/.org-ssg-cache.json")));
+    assert!(!filter.is_relevant(Utf8Path::new("_site/.orgo-cache.json")));
     assert!(filter.is_relevant(Utf8Path::new("index.org")), "real sources still count");
 }
 
@@ -68,7 +68,7 @@ fn build_inputs_trigger_a_rebuild_even_though_discovery_skips_them() {
     std::fs::create_dir_all(&src).unwrap();
     let filter = ChangeFilter::new(&src, &root.join("out"));
 
-    assert!(filter.is_relevant(Utf8Path::new("org-ssg.toml")));
+    assert!(filter.is_relevant(Utf8Path::new("orgo.toml")));
     assert!(filter.is_relevant(Utf8Path::new("templates/base.html")));
     assert!(filter.is_relevant(Utf8Path::new("templates/feed.xml")));
     assert!(filter.is_relevant(Utf8Path::new("style.css")), "assets are copied through");
@@ -149,7 +149,7 @@ fn watching_rebuilds_the_site_when_a_source_file_changes() {
 
     let (src_t, out_t) = (src.clone(), out.clone());
     let handle = std::thread::spawn(move || {
-        let _ = org_ssg::watch::run(&src_t, &out_t, &BuildOptions::default());
+        let _ = orgo::watch::run(&src_t, &out_t, &BuildOptions::default());
     });
 
     // Give the watcher a moment to register before making the change it should see.