krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
bbe31403db53cf5311232c64e95ce0b98b8713dd
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-08-11T20:48:13Z
.github/workflows/ci.yml | 104 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 113 ++++++++++++++++++++++++++++++ CHANGELOG.md | 156 ++++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 26 ++++++- LICENSE | 21 ++++++ README.md | 30 +++++++- RELEASING.md | 78 +++++++++++++++++++++ docs/guide/10-deploying.org | 7 ++ docs/guide/11-versioning.org | 72 +++++++++++++++++++ 10 files changed, 605 insertions(+), 4 deletions(-) new file mode 100644 @@ -0,0 +1,104 @@ +name: CI + +# Build, test and lint on both platforms org-ssg is used from, plus a compiler-floor job. +# +# WHAT THIS CATCHES THAT LOCAL WORK DOES NOT: +# +# 1. Linux. Development happens on macOS, and the two differ where this project is most +# likely to break: filesystem event paths (the watcher had a real `/var` vs +# `/private/var` bug on macOS), case-insensitive filenames, and path separators. +# 2. A clean checkout. The oracle tests skip when Emacs is absent and the cache is +# gitignored, so a machine that has been building all afternoon is not a fair test of +# what a fresh clone does. +# 3. The MSRV. A stabilised API used without noticing is invisible on a current +# toolchain and is a build failure for anyone on a distribution compiler. +# +# NOT GATED ON `cargo fmt`. The source is formatted by hand — comment tables, aligned +# match arms, and prose wrapped to fit the argument being made — and rustfmt disagrees +# with most of it. Clippy is the lint that catches defects; fmt would only catch taste. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + # A failing build should print the error, not a backtrace-shaped wall. + RUST_BACKTRACE: 1 + +jobs: + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + # Both platforms report, so a macOS-only failure is distinguishable from a real one. + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install Rust + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable + with: + toolchain: stable + components: clippy + + - name: Cache cargo + uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + + # Emacs makes the oracle suite run for real instead of skipping. It is the only + # reason to trust that output still matches org's own exporter, so it is worth the + # install minute. + - name: Install Emacs (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends emacs-nox + + - name: Install Emacs (macOS) + if: runner.os == 'macOS' + run: brew install emacs + + - name: Build + run: cargo build --all-targets --locked + + - name: Test + run: cargo test --locked + + - name: Clippy + run: cargo clippy --all-targets --locked -- -D warnings + + # The documentation site is built by the tool it documents, so a docs page that no + # longer builds is a product defect. `--strict` fails on broken internal links, + # which is the failure mode a docs site actually has. + - name: Build the documentation site + run: cargo run --locked -- build docs -o docs/_site --strict + + msrv: + name: minimum supported Rust (1.88) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Pinned to the version in Cargo.toml's `rust-version`. When that moves, this moves + # with it in the same commit — a floor nobody checks is a floor nobody has. + - name: Install Rust 1.88 + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable + with: + toolchain: "1.88" + + - name: Cache cargo + uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 + + # 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 + # 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. + - name: Build + run: cargo build --locked new file mode 100644 @@ -0,0 +1,113 @@ +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 +# 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. +# +# The tag is the source of truth for the version. The build checks it against Cargo.toml +# rather than trusting them to match, because a release tagged v0.18.0 containing a binary +# that reports 0.17.0 is the kind of thing nobody notices for months. + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Tag to build (for a re-run of a failed release)" + required: true + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + # Apple Silicon and Intel Macs, built natively on their own runners so neither + # is a cross-compile nobody has run. + - { os: macos-latest, target: aarch64-apple-darwin } + - { os: macos-13, target: x86_64-apple-darwin } + # glibc for ordinary distributions, musl for containers and anything older than + # the runner's glibc — a dynamically linked binary is the usual reason a + # download does not run. + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } + - { os: ubuntu-latest, target: x86_64-unknown-linux-musl } + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable + with: + toolchain: stable + targets: ${{ matrix.target }} + + - name: Install musl tools + if: endsWith(matrix.target, '-musl') + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools + + - name: Check the tag against Cargo.toml + shell: bash + run: | + tag="${{ github.event.inputs.tag || github.ref_name }}" + crate=$(cargo metadata --no-deps --format-version 1 \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])') + if [ "$tag" != "v$crate" ]; then + echo "tag $tag does not match Cargo.toml version $crate" >&2 + exit 1 + fi + + - name: Build + run: cargo build --release --locked --target ${{ matrix.target }} + + # A tarball rather than a bare binary: it keeps the executable bit through GitHub's + # download path, and carries the licence with the thing it licenses. + - name: Package + shell: bash + run: | + staging="org-ssg-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}" + mkdir "$staging" + cp "target/${{ matrix.target }}/release/org-ssg" "$staging/" + cp README.md LICENSE CHANGELOG.md "$staging/" + tar czf "$staging.tar.gz" "$staging" + shasum -a 256 "$staging.tar.gz" > "$staging.tar.gz.sha256" + + - name: Upload + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ matrix.target }} + path: | + *.tar.gz + *.tar.gz.sha256 + + release: + name: publish the release + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download every build + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + merge-multiple: true + + # A draft, deliberately. The changelog entry is written by a person, and a release + # that publishes itself before anyone has read it cannot be edited quietly. + - name: Create the draft release + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + with: + draft: true + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + files: | + *.tar.gz + *.tar.gz.sha256 new file mode 100644 @@ -0,0 +1,156 @@ +# Changelog + +What changed and why, newest first. Entries name the *behaviour* that moved, since that is +what a rebuild will show you. + +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 + 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. + +Versions follow the compatibility promise in the README: config keys, template variables, +CLI flags and URLs are the stable surface. + +## 0.18.0 + +Release engineering, so that a version number is worth reading. + +- **A written compatibility promise.** Config keys, template variables, CLI flags and URLs + are the stable surface; the incremental cache, HTML details and the Rust API are not. + In the README, and in the guide under *Versioning and upgrades*. +- **CI** on Linux and macOS: build, test, clippy as an error, and the documentation site + 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 + 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. +- A `LICENSE` file to go with the MIT declaration, crates.io metadata, and a release + profile that produces a 5.0 MB binary rather than 6.5 MB. +- This changelog, and `RELEASING.md`. + +## 0.17.0 + +- **Asset directories outside the source.** `[build] assets = ["../theme/static"]` copies + a directory's contents to the site root. A site's static files do not always live where + its writing does, and copying them next to the writing is how a repository ends up with + two of every stylesheet. `watch` and `serve` watch these directories too. Two files + claiming one URL is a build error naming both. +- **Template hashing is per template.** A page's render key covered every template, so + editing a feed template re-rendered the whole site. It now covers the layout the page + uses plus what that layout extends, includes or imports. On a 196-page site, editing the + feed template renders one page instead of 196. +- Cache format 7. + +## 0.16.0 + +- **Org's entity table.** `\alpha`, `\rarr`, `20\deg` and the other 412 names, generated + from Emacs' own `org-entities`. An unknown name stays literal; `#+OPTIONS: e:nil` turns + the table off. *Output changes* for any page using entities. +- **Table captions.** `#+CAPTION:` above a table becomes a numbered `<caption>`. +- **`#+INCLUDE:` reports itself.** It was inert and silent, which publishes a page with + content missing and nobody told. Now a diagnostic, and `--strict` makes it a failure. +- The Emacs oracle separates deliberate divergence from defects. Every difference from + org's exporter is named and justified, and a test asserts there are no others. + +## 0.15.0 + +Export parity, from a page-by-page diff of a 179-file corpus against the site Emacs +publishes from the same sources. **All of these change output.** + +- Heading levels are relative to a document's shallowest heading, as org exports them. +- Org's text conversions: `--`, `---`, `...`, and `x^2` / `a_{b}`. Never inside verbatim, + code, source blocks or LaTeX. `#+OPTIONS: -:nil`, `^:nil` and `^:{}` all work. +- Captioned figures are numbered `Figure N:`. +- A caption attaches to the element *directly* below it; a blank line between attaches to + nothing. +- Checkboxes render as org writes them, which keeps the `[-]` partly-done state a disabled + `<input>` could not express. `[@4]` sets a list item's number. +- A table's special marker column and its marker rows stay out of the output. +- `#+BEGIN_NOTE` and any other unrecognised name is a special block: a div holding parsed + org rather than a `<pre>` of literal text. Verse keeps its line breaks. +- Emphasis borders forbid whitespace and nothing else, so `="proxied":false=` is verbatim + and `~~/.config/doom/config.el~` is a path that starts with a tilde. +- Listings sort on the time of day when a timestamp carries one. +- Cache format 6. + +## 0.14.0 + +- **Per-page layouts.** `[[pages]]` rules map a source path to a template, and + `#+TEMPLATE:` on a page overrides any rule. A missing template fails the build naming + the page, the template, and what does exist. +- `page.year`, for grouping a listing by year with minijinja's `groupby`. +- An explicit nav can order generated pages among authored ones. `nav.mode = "none"` now + really means none. + +## 0.13.0 + +- Bundled TOML and Org syntax definitions, a `syntaxes_dir` for your own, and org's comma + escape (`,* heading` inside a block). + +## 0.12.0 + +- `serve`: a development server with live reload, bound to loopback. +- A documentation site under `docs/`, built by org-ssg itself. + +## 0.11.0 + +- Table of contents as `page.toc`, section numbers, and org's `#+OPTIONS:` per-file + switches. + +## 0.10.0 + +- Excerpts, word count, reading time, a `truncate` filter, and `#+DRAFT:` pages. + +## 0.9.0 + +- `watch`: rebuilds on OS filesystem events, debounced. + +## 0.8.0 + +- `site.base_url`, the `absolute` and `rfc822` filters, canonical links, and an RSS feed + in the scaffold that validates. + +## 0.7.0 + +- Pagination for large listings, with a `paginator` template context that composes with + grouping. + +## 0.6.0 + +- Grouped collections: one page per tag plus a tag index. +- Generated listing pages (`[[collections]]`), sorted indexes, and feeds via XML + templates. +- A config file, user templates, nav modes, an `init` scaffold, and discovery that will + not publish `.git`. + +## 0.5.0 + +- Parse diagnostics carry `file:line`, and pages render in parallel. +- The corpus audit (`org-ssg 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. + +## 0.4.0 + +- The full v1 construct scope, with the IN/OUT line under test. + +## 0.3.0 + +- The incremental build layer: content, config and template hashing, a dependency graph, + per-page render keys, and a persisted cache manifest. A full build and an incremental + build produce byte-identical output. + +## 0.2.0 + +- Multi-file site builds: a symbol table, internal link resolution, minijinja templates, + tables and footnotes. + +## 0.1.0 + +- Parse and render a single `.org` file to HTML. @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "org-ssg" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "blake3", @@ -1,9 +1,26 @@ [package] name = "org-ssg" -version = "0.17.0" +version = "0.18.0" edition = "2021" description = "Org-mode static site generator that renders the org element tree straight to HTML" license = "MIT" +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/" + +# The compiler floor, checked in CI rather than assumed. org-ssg'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. +rust-version = "1.88" + +# Published crates carry the source, the fixtures the tests need, and nothing else. The +# documentation site is 14 org files plus its build output, which nobody installing a +# binary wants to download. +exclude = ["docs/", "target/", "/.github/"] [lib] name = "org_ssg" @@ -37,3 +54,10 @@ tiny_http = "0.12.0" [dev-dependencies] insta = { version = "1", features = ["json"] } + +# Release binaries are downloaded by people evaluating the tool, so they are built to be +# small and quick to start rather than quick to compile. Thin LTO keeps CI build times +# reasonable; full LTO bought a few percent for minutes per job. +[profile.release] +lto = "thin" +strip = "symbols" new file mode 100644 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Christian Cleberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. @@ -387,7 +387,7 @@ all-of-org. Phase 0 checked this line against a real 179-file corpus and found i | **19** | **Export parity: relative heading levels, special strings, sub/superscript, caption numbering, checkbox and counter markup, table marker columns, special blocks** | **done** | | **20** | **Correctness debt: org's entity table, table captions, a reported `#+INCLUDE:`, and an oracle that separates deliberate divergence from defects** | **done** | | **21** | **Extra asset roots; per-template hashing so one layout edit does not re-render the site** | **done** | -| 22 | Release engineering: CI, MSRV, published binaries, changelog, a written compatibility promise | 1.0 | +| **22** | **Release engineering: CI on both platforms, a checked MSRV, release binaries, a changelog, and a written compatibility promise** | **done** | ### v0.2 in / out @@ -688,6 +688,32 @@ anchored — `:CUSTOM_ID:`/`:ID:` else a slug of its text) and trailing tags; pa plain lists (unordered + ordered) with checkboxes; source blocks; inline markup (`*bold*`, `/italic/`, `_underline_`, `+strike+`, `=verbatim=`, `~code~`); links and bare URLs. +## Compatibility + +Versions mean something as of 1.0. The **stable surface** — changing incompatibly requires +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. | +| 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. | + +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 + 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 +1.82; the floor comes from dependencies. Raising it is a minor version, never a patch. + ## Dependencies Parser is hand-written recursive descent (not `nom`/`chumsky`/`pest` — org is @@ -702,7 +728,7 @@ development server), `toml` (config), `chrono`, `camino`, `walkdir`, `clap`, `an ``` cargo build -cargo test # 156 tests +cargo test # 191 tests cargo run -- init my-site # scaffold a new site cargo run -- build fixtures/minimal.org -o minimal.html # single file cargo run -- build fixtures/site -o _site # whole site (incremental) new file mode 100644 @@ -0,0 +1,78 @@ +# Releasing + +A release is three things that must agree: a version in `Cargo.toml`, a git tag, and a +changelog entry. The release workflow checks the first two against each other and refuses +to build if they differ, because a release tagged `v0.18.0` containing a binary that +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 +``` + +## Every release + +1. **Write the changelog entry first.** [CHANGELOG.md](CHANGELOG.md) names behaviour, not + commits — someone reading it wants to know what their next build will do differently. + Anything that changes rendered HTML gets said out loud. + +2. **Bump the version** in `Cargo.toml`, and build once so `Cargo.lock` follows. + + Patch for fixes that change nothing about the stable surface. Minor for new config + keys, new template variables, an MSRV bump, or output that changes to track Emacs more + closely. Major for anything that breaks the promises in the README's Compatibility + section — config keys, template context, CLI, or URLs. + +3. **Check it.** + + ```bash + cargo test + cargo clippy --all-targets -- -D warnings + cargo run -- build docs -o docs/_site --strict + cargo package + ``` + + `cargo package` is the one people forget: it builds the crate exactly as crates.io will + receive it, and catches a file the `exclude` list should not have removed. + +4. **Verify against a real corpus.** The test suite says the code does what it did; a + corpus says the *site* does. Build a site you know with `--no-cache` and diff the + output against the previous version's. A release that quietly changes 200 pages should + do so on purpose. + +5. **Commit, tag, push.** + + ```bash + git commit -am "0.18: <what changed>" + git tag -a v0.18.0 -m "0.18.0" + git push && git push --tags + ``` + +6. **Publish the crate.** + + ```bash + cargo publish + ``` + + This is irreversible: a published version can be yanked but never replaced. + +7. **Finish the GitHub release.** Pushing the tag builds binaries for macOS (arm64 and + x86_64) and Linux (gnu and musl) and opens a *draft* release with them attached. Paste + the changelog entry in and publish it. The draft is deliberate — a release that + publishes itself before anyone has read it cannot be edited quietly. + +## If a release goes wrong + +Yank rather than delete, and ship a fix as a new version: + +```bash +cargo yank --version 0.18.0 +``` + +Yanking stops new dependents from selecting it; anyone who already has it keeps working. +Then release `0.18.1` with the fix and a changelog entry that says what happened. @@ -112,3 +112,10 @@ built 182 page(s) (182 rendered, 0 cached), copied 3 asset(s) from content -> _s Both zeros matter. Unresolved links are internal links pointing at nothing; diagnostics are malformed org that degraded rather than failing. With =--strict= neither can reach this line, because either would have failed the build. + +* After an upgrade + +The first build on a new version is worth running with =--no-cache=, so you compare the +new output to the old rather than to a cache written by both. What a version number +promises — and what it does not — is in [[file:11-versioning.org][Versioning and +upgrades]]. new file mode 100644 @@ -0,0 +1,72 @@ +#+TITLE: Versioning and upgrades +#+DESCRIPTION: What a version number promises, what it does not, and how to upgrade safely. +#+LEDE: The stable surface is what you build a site against, not what the code happens to do. + +A generator you point at ten years of writing needs to be boring about compatibility. +This page says exactly what is promised. + +* The stable surface + +Changing any of this incompatibly requires a major version. + +| Stable | What that covers | +|--------+------------------| +| =org-ssg.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. | + +*URLs are on that list deliberately.* A generator that quietly moves your pages breaks +every link anyone has ever made to you, and no upgrade note fixes an inbound link. + +Adding things — a new config key, a new template variable — is a minor release. Nothing +you already wrote stops working. + +* What is not stable + +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 +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. +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. + +The class names the documentation names are the ones to write CSS against: +=post-list=, =post-list-item=, =figure-number=, =table-number=, =section-number-N=, +=footnote-ref=, =verbatim=, and the =on=/=off=/=trans= classes on checkbox items. + +** The Rust API + +The crate is on crates.io so the binary can be installed with =cargo install=. The library +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 +dependencies, and a dependency raising its own is invisible until someone on an older +compiler tries to build. + +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 +#+END_SRC + +=--no-cache= makes the first build after an upgrade a full one, so you are comparing the +new version's output to the old version's output rather than to a cache written by a +mixture of both. =--strict= turns a link that stopped resolving into a failure. + +If you keep your built site in version control, the diff after that command *is* the +upgrade report — which is the most useful review a generator can give you, and the reason +the changelog names behaviour rather than commits.