feat(gandalf): declare the not-found page in bunny.toml
All checks were successful
Test / test (push) Successful in 6s

How a site answers missing paths now belongs to its repository like its
edge rules: bunny.toml declares [site] not_found as "default", "spa" or
a page from the build such as "/404.html". The action reads and sends
the table on start and finalize; the gateway validates it before the
upload and applies it to the Storage Zone at finalization, recording
and purging only when it changed. A deployment without [site] resets
the default; older action versions leave the setting alone.

The console's Settings tab only shows the applied value, and the admin
API no longer sets it. The maintainer guide and llms.txt describe the
setting under Configuration.
This commit is contained in:
Eric Wendland 2026-10-02 23:57:33 +02:00
commit ffb46edc65
4 changed files with 72 additions and 28 deletions

View file

@ -307,6 +307,12 @@ redirect = { to = "https://example.org{{path}}", status = 301 }
'''
SITE_SECTION = '''
[site]
not_found = "/404.html"
'''
def gateway_args(root: Path, edge_rules: Path | None) -> Any:
return static_site.argparse.Namespace(
directory=root,
@ -332,9 +338,10 @@ class EdgeRuleTests(unittest.TestCase):
def test_loads_declared_rules(self) -> None:
with tempfile.TemporaryDirectory() as directory:
rules = static_site.load_edge_rules(
self.write_rules(directory, MARKDOWN_RULES)
rules, site = static_site.load_site_config(
self.write_rules(directory, MARKDOWN_RULES + SITE_SECTION)
)
self.assertEqual(site, {"not_found": "/404.html"})
self.assertEqual(
rules,
[
@ -357,6 +364,7 @@ class EdgeRuleTests(unittest.TestCase):
# Rule contents are validated by the gateway; see handler_test.ts.
cases = {
"unknown keys": 'headers = "x"\n' + MARKDOWN_RULES,
"must be a \\[site\\] table": 'site = "spa"\n',
"must be \\[\\[edge_rules\\]\\] tables": 'edge_rules = ["x"]\n',
"unsupported value": MARKDOWN_RULES + "since = 2026-10-02\n",
"not valid TOML": "[[edge_rules]\n",
@ -364,7 +372,7 @@ class EdgeRuleTests(unittest.TestCase):
for message, text in cases.items():
with self.subTest(message), tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(DeployError, message):
static_site.load_edge_rules(self.write_rules(directory, text))
static_site.load_site_config(self.write_rules(directory, text))
def test_edge_rules_need_the_gateway(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@ -392,14 +400,21 @@ class EdgeRuleTests(unittest.TestCase):
root = Path(directory) / "dist"
root.mkdir()
write_site(root)
rules = self.write_rules(directory, MARKDOWN_RULES)
for path, expected in ((rules, ["markdown", "old-blog"]), (None, [])):
rules = self.write_rules(directory, MARKDOWN_RULES + SITE_SECTION)
cases = (
(rules, ["markdown", "old-blog"], {"not_found": "/404.html"}),
(None, [], {}),
)
for path, expected, site in cases:
result = static_site.deploy_bunny(gateway_args(root, path))
for sent in (
start_deployment.call_args.args[5],
finalize_deployment.call_args.args[7],
):
self.assertEqual([rule["name"] for rule in sent], expected)
# No [site] means the defaults, which reset earlier settings.
self.assertEqual(start_deployment.call_args.args[6], site)
self.assertEqual(finalize_deployment.call_args.args[8], site)
self.assertEqual(result["edge_rules"], expected)
keys.append(start_deployment.call_args.args[3])
# Changing only the rules must not replay the previous deployment.