Commit f018f6e1da
f018f6e1dab362139ea30ca74d426f5e51bea24a
parent: 9fdb0c2c02
Unsigned
cmc <hello@cleberg.net> · 2026-08-12 00:22 UTC
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.
Layout: unified · split
docs/guide/10-deploying.org
+15
| @@ -113,6 +113,21 @@ Both zeros matter. Unresolved links are internal links pointing at nothing; diag |
| 113 | 113 | are malformed org that degraded rather than failing. With =--strict= neither can reach |
| 114 | 114 | this line, because either would have failed the build. |
| 115 | 115 | |
| 116 | * Do not publish the cache |
| 117 | |
| 118 | =<output>/.orgo-cache.json= is a build artefact that happens to live in the output |
| 119 | directory, because it describes exactly that directory. Nothing breaks if it is served — |
| 120 | it holds hashes and paths, not secrets — but it is not part of your site, so leave it |
| 121 | behind: |
| 122 | |
| 123 | #+BEGIN_SRC sh |
| 124 | rsync -r --delete-before --exclude '.orgo-cache.json' _site/ server:/var/www/example.com/ |
| 125 | #+END_SRC |
| 126 | |
| 127 | Anything that uploads a directory wholesale needs the same exclusion. A deploy that |
| 128 | *deletes* it on the far side is worse than one that copies it: the next build then has no |
| 129 | cache to reuse and re-renders everything. |
| 130 | |
| 116 | 131 | * Telling a search engine where things are |
| 117 | 132 | |
| 118 | 133 | A build with =site.base_url= set writes =sitemap.xml= at the site root, listing every |
src/site.rs
+19 −5
| @@ -1400,7 +1400,7 @@ fn collect_assets( |
| 1400 | 1400 | .strip_prefix(&base) |
| 1401 | 1401 | .map(|p| p.to_owned()) |
| 1402 | 1402 | .unwrap_or_else(|_| abs.clone()); |
| 1403 | | if rel.components().any(|c| c.as_str().starts_with('.')) { |
| 1403 | if rel.components().any(|c| is_hidden(c.as_str())) { |
| 1404 | 1404 | continue; |
| 1405 | 1405 | } |
| 1406 | 1406 | assets.push(Asset { from: abs, rel }); |
| @@ -1462,14 +1462,28 @@ fn excluded_dirs(src: &Utf8Path, config: &Config, out: Option<&Utf8Path>) -> Vec |
| 1462 | 1462 | /// Is this source-relative path excluded from discovery? |
| 1463 | 1463 | /// |
| 1464 | 1464 | /// Dot-entries are skipped wholesale. That is the conventional rule for site generators, |
| 1465 | /// Is this path component a dot-entry that must not be published? |
| 1466 | /// |
| 1467 | /// Dot-directories are excluded because a source directory is very often a git repository, |
| 1468 | /// and publishing `.git` — or `.env` — leaks a project's entire history alongside its |
| 1469 | /// homepage. `.well-known` is the exception the web actually defines (RFC 8615): it holds |
| 1470 | /// `security.txt`, ACME challenges, and other files whose entire purpose is to be served. |
| 1471 | /// Excluding it is how a deploy quietly deletes a site's security contact. |
| 1472 | fn is_hidden(component: &str) -> bool { |
| 1473 | component.starts_with('.') |
| 1474 | && component != "." |
| 1475 | && component != ".." |
| 1476 | && component != WELL_KNOWN |
| 1477 | } |
| 1478 | |
| 1479 | /// The one dot-directory the web expects to be published. |
| 1480 | const WELL_KNOWN: &str = ".well-known"; |
| 1481 | |
| 1465 | 1482 | /// and the reason is safety rather than tidiness: a source directory is very often a git |
| 1466 | 1483 | /// repository, and publishing `.git` — or `.env` — is a way to leak a project's entire |
| 1467 | 1484 | /// history alongside its homepage. |
| 1468 | 1485 | fn is_excluded(rel: &Utf8Path, skip_dirs: &[Utf8PathBuf]) -> bool { |
| 1469 | | if rel |
| 1470 | | .components() |
| 1471 | | .any(|c| c.as_str().starts_with('.') && c.as_str() != "." && c.as_str() != "..") |
| 1472 | | { |
| 1486 | if rel.components().any(|c| is_hidden(c.as_str())) { |
| 1473 | 1487 | return true; |
| 1474 | 1488 | } |
| 1475 | 1489 | skip_dirs |
tests/config.rs
+36
| @@ -2463,3 +2463,39 @@ fn the_sitemap_can_be_disabled() { |
| 2463 | 2463 | |
| 2464 | 2464 | assert!(!out.join("sitemap.xml").exists(), "disabled means absent"); |
| 2465 | 2465 | } |
| 2466 | |
| 2467 | /// `.well-known` is the one dot-directory the web defines (RFC 8615): `security.txt`, |
| 2468 | /// ACME challenges, and other files whose whole purpose is to be served. Excluding it |
| 2469 | /// with the rest is how a deploy silently deletes a site's security contact — which is |
| 2470 | /// exactly what happened the first time this ran against a real server. |
| 2471 | #[test] |
| 2472 | fn well_known_is_published_but_other_dot_entries_are_not() { |
| 2473 | let root = tmpdir("wellknown"); |
| 2474 | let src = root.join("src"); |
| 2475 | std::fs::create_dir_all(src.join(".well-known")).unwrap(); |
| 2476 | std::fs::create_dir_all(src.join(".git")).unwrap(); |
| 2477 | std::fs::create_dir_all(root.join("static/.well-known")).unwrap(); |
| 2478 | write_site(&src); |
| 2479 | std::fs::write(src.join(".well-known/security.txt"), "Contact: mailto:a@b.c\n").unwrap(); |
| 2480 | std::fs::write(src.join(".git/config"), "[core]\n").unwrap(); |
| 2481 | std::fs::write(src.join(".env"), "SECRET=1\n").unwrap(); |
| 2482 | std::fs::write(root.join("static/.well-known/assetlinks.json"), "[]\n").unwrap(); |
| 2483 | std::fs::write( |
| 2484 | src.join("orgo.toml"), |
| 2485 | "[build]\nassets = [\"../static\"]\n", |
| 2486 | ) |
| 2487 | .unwrap(); |
| 2488 | let out = root.join("out"); |
| 2489 | build(&src, &out); |
| 2490 | |
| 2491 | assert!( |
| 2492 | out.join(".well-known/security.txt").exists(), |
| 2493 | "from the source directory" |
| 2494 | ); |
| 2495 | assert!( |
| 2496 | out.join(".well-known/assetlinks.json").exists(), |
| 2497 | "and from an asset root" |
| 2498 | ); |
| 2499 | assert!(!out.join(".git/config").exists(), ".git stays out"); |
| 2500 | assert!(!out.join(".env").exists(), ".env stays out"); |
| 2501 | } |