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

Publish .well-known

Deploying a real site with orgo deleted its security.txt. The dot-entry rule
that keeps `.git` and `.env` out of a published site also excluded
`.well-known`, and rsync --delete finished the job on the server.

`.well-known` is the one dot-directory the web defines (RFC 8615). It holds
security.txt, ACME challenges, and other files whose entire purpose is to be
served. It is now the single exception, in both places the rule is applied: the
source tree and asset roots.

Also documents excluding .orgo-cache.json from a deploy, which the same publish
put on the live site. Nothing in it is secret, but it is not part of anyone's
site — and a deploy that *deletes* it remotely is worse than one that copies it,
since the next build then re-renders everything.
 docs/guide/10-deploying.org | 15 +++++++++++++++
 src/site.rs                 | 24 +++++++++++++++++++-----
 tests/config.rs             | 36 ++++++++++++++++++++++++++++++++++++
 3 files changed, 70 insertions(+), 5 deletions(-)

diff --git a/docs/guide/10-deploying.org b/docs/guide/10-deploying.org
index 37a70fd..7ce122c 100644
--- a/docs/guide/10-deploying.org
+++ b/docs/guide/10-deploying.org
@@ -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
diff --git a/src/site.rs b/src/site.rs
index 5c24d78..71fe86c 100644
--- a/src/site.rs
+++ b/src/site.rs
@@ -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
diff --git a/tests/config.rs b/tests/config.rs
index 8851086..8bec048 100644
--- a/tests/config.rs
+++ b/tests/config.rs
@@ -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");
+}