krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

Commit 141ea8a99b

141ea8a99b9f4fc79a2b219173ee5bfee64d311b

parent: c8b514e1a0

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-30 16:15 UTC

Stop copying a previous build's output, and point at the new docs site

`excluded_dirs` drops the output directory from discovery only when it sits
inside the source, so building into the source (`-o _site`) and then previewing
elsewhere (`-o /tmp/preview`) copied the first site into the second: 2 assets
became 21 on orgo's own documentation, and the preview carried a stale nested
copy of the whole site.

Discovery now prunes any source directory holding an `.orgo-cache.json`. Every
build writes that manifest into its output, so it marks a directory as output
rather than content, and no new configuration is needed to say so. The
mirror-image case was already guarded: `collect_assets` refuses a `build.assets`
root that contains the output directory.

The documentation site moved to https://orgo.krz.sh, served from the repository's
pages branch. `homepage` and the README's 25 links follow it.

`is_excluded` and `is_hidden` had their doc comments interleaved by an earlier
edit, leaving `is_excluded` documented by a sentence fragment.

Closes #3

Layout: unified · split

Cargo.toml +1 −1
@@ -9,7 +9,7 @@ readme = "README.md"
99keywords = ["org-mode", "static-site", "ssg", "emacs", "blog"]
1010categories = ["command-line-utilities", "text-processing"]
1111repository = "https://gitbay.org/krz/orgo"
12homepage = "https://krazywarez.github.io/orgo/"
12homepage = "https://orgo.krz.sh/"
1313
1414# The compiler floor. orgo's own code needs 1.82 (`Option::is_none_or`); the floor is
1515# 1.88 because dependencies in Cargo.lock declare it — `plist` and `time`, both by way of
README.md +25 −25
@@ -14,7 +14,7 @@ heading's TODO state and tags, `#+` keywords, ID links, captions on images. orgo
1414of it, and its output is checked page by page against what Emacs' own exporter produces
1515from the same file.
1616
17**Documentation: <https://krazywarez.github.io/orgo/>** — that site is written in org and
17**Documentation: <https://orgo.krz.sh/>** — that site is written in org and
1818built by orgo, so it doubles as the longest worked example available.
1919
2020## Install
@@ -35,7 +35,7 @@ cargo install --path .
3535```
3636
3737Either way you get an `orgo` command on your `PATH`. Full notes, including how to run it
38without installing anything: <https://krazywarez.github.io/orgo/install.html>
38without installing anything: <https://orgo.krz.sh/install.html>
3939
4040## Your First Site
4141
@@ -76,15 +76,15 @@ Each of these is a few lines of config, and each has a page in the guide:
7676
7777| Add | Documented in |
7878|---|---|
79| A blog index, newest first | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
80| Tag pages, and an index of tags | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
81| An RSS feed | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
82| Numbered pages when a list gets long | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
83| A built-in theme for a blog, a wiki or a doc site | [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) |
84| Your own design, in ordinary HTML templates | [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) |
85| Drafts that stay unpublished until you say so | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
86| A table of contents on long posts | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
87| Clean URLs that survive a renamed file | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
79| A blog index, newest first | [Collections](https://orgo.krz.sh/guide/03-collections.html) |
80| Tag pages, and an index of tags | [Collections](https://orgo.krz.sh/guide/03-collections.html) |
81| An RSS feed | [Collections](https://orgo.krz.sh/guide/03-collections.html) |
82| Numbered pages when a list gets long | [Collections](https://orgo.krz.sh/guide/03-collections.html) |
83| A built-in theme for a blog, a wiki or a doc site | [Configuration](https://orgo.krz.sh/guide/02-configuration.html) |
84| Your own design, in ordinary HTML templates | [Templates](https://orgo.krz.sh/guide/04-templates.html) |
85| Drafts that stay unpublished until you say so | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) |
86| A table of contents on long posts | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) |
87| Clean URLs that survive a renamed file | [Authoring](https://orgo.krz.sh/guide/06-authoring.html) |
8888
8989Rebuilds only touch the pages that actually changed, so saving a post on a site with
9090hundreds of them stays instant.
@@ -110,28 +110,28 @@ Read the numbers with three things in mind. The weblorg figures include Emacs st
110110loading its packages, which you pay on every publish and cannot avoid. orgo emits 13 pages
111111weblorg does not, one per tag, so it is doing slightly more work. And the two do not
112112produce byte-identical output — the differences are deliberate and listed under
113[Org support](https://krazywarez.github.io/orgo/guide/05-org-support.html).
113[Org support](https://orgo.krz.sh/guide/05-org-support.html).
114114
115115Apple M2 Pro, 12 cores, macOS 26.6, Emacs 30.2, orgo built with `--release`.
116116
117117## Docs
118118
119<https://krazywarez.github.io/orgo/>
119<https://orgo.krz.sh/>
120120
121121| Page | What is in it |
122122|---|---|
123| [Quick start](https://krazywarez.github.io/orgo/quickstart.html) | A working site in two commands, then your own writing, then your own design. |
124| [Install](https://krazywarez.github.io/orgo/install.html) | Getting the binary, and running it without installing anything. |
125| [Commands](https://krazywarez.github.io/orgo/guide/01-cli.html) | Every command and flag, and what each is for. |
126| [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) | Every setting in `orgo.toml`, what it changes, and what it costs. |
127| [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) | Blog indexes, tag pages, pagination and RSS feeds. |
128| [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) | Layouts, and every variable a template can use. |
129| [Org support](https://krazywarez.github.io/orgo/guide/05-org-support.html) | Which org syntax is handled, which is not, and how the rest degrades. |
130| [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) | URLs, drafts, excerpts, tables of contents. |
131| [Incremental builds](https://krazywarez.github.io/orgo/guide/07-incremental.html) | How it decides what to rebuild. |
132| [Watching and serving](https://krazywarez.github.io/orgo/guide/08-workflow.html) | The write-save-see loop. |
133| [Auditing](https://krazywarez.github.io/orgo/guide/09-auditing.html) | Reading a corpus before trusting a tool with it. |
134| [Deploying](https://krazywarez.github.io/orgo/guide/10-deploying.html) | Producing a production build, and putting it somewhere. |
123| [Quick start](https://orgo.krz.sh/quickstart.html) | A working site in two commands, then your own writing, then your own design. |
124| [Install](https://orgo.krz.sh/install.html) | Getting the binary, and running it without installing anything. |
125| [Commands](https://orgo.krz.sh/guide/01-cli.html) | Every command and flag, and what each is for. |
126| [Configuration](https://orgo.krz.sh/guide/02-configuration.html) | Every setting in `orgo.toml`, what it changes, and what it costs. |
127| [Collections](https://orgo.krz.sh/guide/03-collections.html) | Blog indexes, tag pages, pagination and RSS feeds. |
128| [Templates](https://orgo.krz.sh/guide/04-templates.html) | Layouts, and every variable a template can use. |
129| [Org support](https://orgo.krz.sh/guide/05-org-support.html) | Which org syntax is handled, which is not, and how the rest degrades. |
130| [Authoring](https://orgo.krz.sh/guide/06-authoring.html) | URLs, drafts, excerpts, tables of contents. |
131| [Incremental builds](https://orgo.krz.sh/guide/07-incremental.html) | How it decides what to rebuild. |
132| [Watching and serving](https://orgo.krz.sh/guide/08-workflow.html) | The write-save-see loop. |
133| [Auditing](https://orgo.krz.sh/guide/09-auditing.html) | Reading a corpus before trusting a tool with it. |
134| [Deploying](https://orgo.krz.sh/guide/10-deploying.html) | Producing a production build, and putting it somewhere. |
135135
136136## Building
137137
src/site.rs +20 −7
@@ -1332,7 +1332,10 @@ fn discover(
13321332 };
13331333 let rel = path.strip_prefix(src).unwrap_or(path);
13341334 // The source root itself always passes; `filter_entry` prunes whole subtrees.
1335 rel.as_str().is_empty() || !is_excluded(rel, &skip_dirs)
1335 if rel.as_str().is_empty() {
1336 return true;
1337 }
1338 !is_excluded(rel, &skip_dirs) && !is_build_output(e)
13361339 }) {
13371340 let entry = entry.with_context(|| format!("walking {src}"))?;
13381341 if !entry.file_type().is_file() {
@@ -1479,9 +1482,6 @@ fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec
14791482 dirs
14801483}
14811484
1482/// Is this source-relative path excluded from discovery?
1483///
1484/// Dot-entries are skipped wholesale. That is the conventional rule for site generators,
14851485/// Is this path component a dot-entry that must not be published?
14861486///
14871487/// Dot-directories are excluded because a source directory is very often a git repository,
@@ -1499,9 +1499,22 @@ fn is_hidden(component: &str) -> bool {
14991499/// The one dot-directory the web expects to be published.
15001500const WELL_KNOWN: &str = ".well-known";
15011501
1502/// and the reason is safety rather than tidiness: a source directory is very often a git
1503/// repository, and publishing `.git` — or `.env` — is a way to leak a project's entire
1504/// history alongside its homepage.
1502/// Is this directory a site orgo built earlier?
1503///
1504/// Every build writes `.orgo-cache.json` into its output directory, so a directory in the
1505/// source carrying one is output rather than content someone wrote. `excluded_dirs` only
1506/// covers an output directory nested in the source; without this, building into the
1507/// source (`-o _site`) and then previewing elsewhere (`-o /tmp/preview`) copies the whole
1508/// first site into the second, one asset at a time.
1509fn is_build_output(entry: &walkdir::DirEntry) -> bool {
1510 entry.file_type().is_dir() && entry.path().join(".orgo-cache.json").is_file()
1511}
1512
1513/// Is this source-relative path excluded from discovery?
1514///
1515/// Dot-entries are skipped wholesale, and the reason is safety rather than tidiness: a
1516/// source directory is very often a git repository, and publishing `.git` — or `.env` —
1517/// is a way to leak a project's entire history alongside its homepage.
15051518fn is_excluded(rel: &Utf8Path, skip_dirs: &[Utf8PathBuf]) -> bool {
15061519 if rel.components().any(|c| is_hidden(c.as_str())) {
15071520 return true;
tests/incremental.rs +29
@@ -513,3 +513,32 @@ fn editing_one_template_rebuilds_only_the_pages_that_use_it() {
513513 r.rendered
514514 );
515515}
516
517/// A previous build's output, left in the source, is not content. Building into the
518/// source and then previewing elsewhere used to copy the whole first site into the
519/// second — 2 assets became 21 on orgo's own documentation.
520#[test]
521fn a_previous_build_output_is_not_copied_as_assets() {
522 let src = tmpdir("prev-output-src");
523 write(&src, "index.org", "#+TITLE: Home\n\nHome.\n");
524 write(&src, "logo.svg", "<svg/>");
525
526 // Build into the source, as docs/guide/10-deploying.org does.
527 let nested = src.join("_site");
528 build_site(&src, &nested, &BuildOptions::default()).unwrap();
529 assert!(manifest_path(&nested).exists(), "the build wrote its cache manifest");
530
531 // Then preview elsewhere, as docs/guide/09-auditing.org does.
532 let preview = tmpdir("prev-output-preview");
533 let r = build_site(&src, &preview, &BuildOptions::default()).unwrap();
534
535 assert_eq!(
536 r.assets,
537 vec![Utf8PathBuf::from("logo.svg")],
538 "only the real asset is copied, not the earlier build's output"
539 );
540 assert!(
541 !preview.join("_site").exists(),
542 "the earlier site must not be nested inside the new one"
543 );
544}