krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
f018f6e1dab362139ea30ca74d426f5e51bea24a
unsigned
author: Christian Cleberg <hello@cleberg.net> · 2026-08-12T00:22:55Z
docs/guide/10-deploying.org | 15 +++++++++++++++ src/site.rs | 24 +++++++++++++++++++----- tests/config.rs | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) @@ -113,6 +113,21 @@ Both zeros matter. Unresolved links are internal links pointing at nothing; diag are malformed org that degraded rather than failing. With =--strict= neither can reach this line, because either would have failed the build. +* Do not publish the cache + +=<output>/.orgo-cache.json= is a build artefact that happens to live in the output +directory, because it describes exactly that directory. Nothing breaks if it is served — +it holds hashes and paths, not secrets — but it is not part of your site, so leave it +behind: + +#+BEGIN_SRC sh +rsync -r --delete-before --exclude '.orgo-cache.json' _site/ server:/var/www/example.com/ +#+END_SRC + +Anything that uploads a directory wholesale needs the same exclusion. A deploy that +*deletes* it on the far side is worse than one that copies it: the next build then has no +cache to reuse and re-renders everything. + * Telling a search engine where things are A build with =site.base_url= set writes =sitemap.xml= at the site root, listing every @@ -1400,7 +1400,7 @@ fn collect_assets( .strip_prefix(&base) .map(|p| p.to_owned()) .unwrap_or_else(|_| abs.clone()); - if rel.components().any(|c| c.as_str().starts_with('.')) { + if rel.components().any(|c| is_hidden(c.as_str())) { continue; } assets.push(Asset { from: abs, rel }); @@ -1462,14 +1462,28 @@ fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec /// Is this source-relative path excluded from discovery? /// /// Dot-entries are skipped wholesale. That is the conventional rule for site generators, +/// Is this path component a dot-entry that must not be published? +/// +/// Dot-directories are excluded because a source directory is very often a git repository, +/// and publishing `.git` — or `.env` — leaks a project's entire history alongside its +/// homepage. `.well-known` is the exception the web actually defines (RFC 8615): it holds +/// `security.txt`, ACME challenges, and other files whose entire purpose is to be served. +/// Excluding it is how a deploy quietly deletes a site's security contact. +fn is_hidden(component: &str) -> bool { + component.starts_with('.') + && component != "." + && component != ".." + && component != WELL_KNOWN +} + +/// The one dot-directory the web expects to be published. +const WELL_KNOWN: &str = ".well-known"; + /// and the reason is safety rather than tidiness: a source directory is very often a git /// repository, and publishing `.git` — or `.env` — is a way to leak a project's entire /// history alongside its homepage. fn is_excluded(rel: &Utf8Path, skip_dirs: &[Utf8PathBuf]) -> bool { - if rel - .components() - .any(|c| c.as_str().starts_with('.') && c.as_str() != "." && c.as_str() != "..") - { + if rel.components().any(|c| is_hidden(c.as_str())) { return true; } skip_dirs @@ -2463,3 +2463,39 @@ fn the_sitemap_can_be_disabled() { assert!(!out.join("sitemap.xml").exists(), "disabled means absent"); } + +/// `.well-known` is the one dot-directory the web defines (RFC 8615): `security.txt`, +/// ACME challenges, and other files whose whole purpose is to be served. Excluding it +/// with the rest is how a deploy silently deletes a site's security contact — which is +/// exactly what happened the first time this ran against a real server. +#[test] +fn well_known_is_published_but_other_dot_entries_are_not() { + let root = tmpdir("wellknown"); + let src = root.join("src"); + std::fs::create_dir_all(src.join(".well-known")).unwrap(); + std::fs::create_dir_all(src.join(".git")).unwrap(); + std::fs::create_dir_all(root.join("static/.well-known")).unwrap(); + write_site(&src); + std::fs::write(src.join(".well-known/security.txt"), "Contact: mailto:a@b.c\n").unwrap(); + std::fs::write(src.join(".git/config"), "[core]\n").unwrap(); + std::fs::write(src.join(".env"), "SECRET=1\n").unwrap(); + std::fs::write(root.join("static/.well-known/assetlinks.json"), "[]\n").unwrap(); + std::fs::write( + src.join("orgo.toml"), + "[build]\nassets = [\"../static\"]\n", + ) + .unwrap(); + let out = root.join("out"); + build(&src, &out); + + assert!( + out.join(".well-known/security.txt").exists(), + "from the source directory" + ); + assert!( + out.join(".well-known/assetlinks.json").exists(), + "and from an asset root" + ); + assert!(!out.join(".git/config").exists(), ".git stays out"); + assert!(!out.join(".env").exists(), ".env stays out"); +}