Summary
Add support for a per-directory config file, .hhaccess, for StaticSite. Think .htaccess, but deliberately simpler: one directive per line, globs instead of regexes, and passwords written directly in the file (no .htpasswd hashing — the site-level basic_auth array is already stored in plaintext, so this is consistent with how Hohenheim works today).
Design principles
- StaticSite only (first version). Proxy/Node sites have their own filesystem and backend; this doesn't belong there.
- Directory walk per request: map the request path to a directory, walk from the site root down to it and collect every
.hhaccess along the way. Deepest file wins on conflicts (like Apache). Cache parsed files on mtime so there's no fs hit per request.
- Never served:
.hhaccess itself always returns 404, even when show_hidden_files is enabled.
- One directive per line,
# for comments, no XML-style blocks, no regexes. Globs keep the file readable and avoid ReDoS.
Proposed syntax
# Access
auth "Internal docs" roel:secret
auth jelle:alsosecret # multiple lines = multiple accounts
allow 10.0.0.0/8 # IPs that bypass the password
deny all # close the folder entirely (403)
# Redirects (301 unless specified)
redirect /old/page.html /new/page
redirect /blog/* https://blog.example.com/$1 302
redirect /docs/*/intro /docs/$1/getting-started
# Rewrites (internal, URL stays the same)
rewrite /api/* /api.json
rewrite /*.md /render.html?file=$1
fallback /index.html # SPA: anything that doesn't exist → index.html
# Files
index index.html README.html # order of preference
autoindex off
hidden *.bak *.psd .git/* # never serve (404)
# Headers & caching
header X-Robots-Tag noindex
header Cache-Control "max-age=3600" for *.css *.js *.woff2
cache 1y for *.png *.jpg *.svg # shorthand for Cache-Control + immutable
cors * # Access-Control-Allow-Origin
# Status
error 404 /404.html
error 403 /forbidden.html
gone /legacy/* # 410
Glob rules
* matches within a single path segment, ** across segments (gitignore/minimatch semantics).
- Every
*/** is a capture group: $1, $2, … in the target. $0 is the full match.
- Paths are relative to the directory containing the
.hhaccess, unless they start with / (then relative to the site root). This keeps files relocatable.
- First match wins, top to bottom.
redirect and rewrite are evaluated in source order; a rewrite may re-match at most once (one hop, no loops).
Directives
| Directive |
Notes |
auth [realm] user:pass |
HTTP Basic auth. Realm is optional; multiple lines add multiple accounts. |
allow ip/cidr |
Bypass auth/deny for these addresses. Hohenheim already has per-domain listen_on IPs, so LAN-only folders are a real use case. |
deny all |
403 for everyone not in allow. |
redirect from to [status] |
External redirect, 301 by default. |
rewrite from to |
Internal rewrite. |
fallback path |
Per-folder version of the fallback_file setting — useful for multiple SPAs under one site. |
index file... |
Index file preference order. |
autoindex on|off |
Override the site setting per folder. |
hidden glob... |
Files that always 404. |
header Name value [for glob...] |
Add a response header, optionally only for matching files. |
cache duration [for glob...] |
Shorthand: 1y, 1d, 1h, off. Adds immutable for long durations. |
cors origin |
Sets Access-Control-Allow-Origin. |
error status path |
Custom error page. |
gone glob |
410 for removed content — better for SEO than 404. |
Deliberately out of scope: proxy directives (a text file in a static folder shouldn't become network configuration), env variables, conditionals (if), handlers, .htpasswd-style hashed passwords.
Processing order per request
hidden → 404
deny/allow → 403
auth → 401 (with WWW-Authenticate: Basic realm="…")
redirect/gone → 30x/410, done
rewrite → new internal path
index/autoindex/fallback → determine file
header/cache/cors added; error on 4xx
Implementation notes
- New class
Develry.HhaccessFile (parse + match) and an HhaccessResolver per StaticSite that resolves path → stacked config, cached on mtime.
- Hook into
StaticSite.handleRequest before ecstatic. auth can reuse Site#checkBasicAuth by injecting the folder's credentials array.
- Site-level
basic_auth and .hhaccess auth stack: folder auth does not replace site auth, both must pass.
- One new
StaticSite setting: hhaccess: Boolean (default on), so it can be disabled for untrusted upload folders.
Summary
Add support for a per-directory config file,
.hhaccess, forStaticSite. Think.htaccess, but deliberately simpler: one directive per line, globs instead of regexes, and passwords written directly in the file (no.htpasswdhashing — the site-levelbasic_autharray is already stored in plaintext, so this is consistent with how Hohenheim works today).Design principles
.hhaccessalong the way. Deepest file wins on conflicts (like Apache). Cache parsed files onmtimeso there's no fs hit per request..hhaccessitself always returns 404, even whenshow_hidden_filesis enabled.#for comments, no XML-style blocks, no regexes. Globs keep the file readable and avoid ReDoS.Proposed syntax
Glob rules
*matches within a single path segment,**across segments (gitignore/minimatch semantics).*/**is a capture group:$1,$2, … in the target.$0is the full match..hhaccess, unless they start with/(then relative to the site root). This keeps files relocatable.redirectandrewriteare evaluated in source order; arewritemay re-match at most once (one hop, no loops).Directives
auth [realm] user:passallow ip/cidrauth/denyfor these addresses. Hohenheim already has per-domainlisten_onIPs, so LAN-only folders are a real use case.deny allallow.redirect from to [status]rewrite from tofallback pathfallback_filesetting — useful for multiple SPAs under one site.index file...autoindex on|offhidden glob...header Name value [for glob...]cache duration [for glob...]1y,1d,1h,off. Addsimmutablefor long durations.cors originAccess-Control-Allow-Origin.error status pathgone globDeliberately out of scope:
proxydirectives (a text file in a static folder shouldn't become network configuration), env variables, conditionals (if), handlers,.htpasswd-style hashed passwords.Processing order per request
hidden→ 404deny/allow→ 403auth→ 401 (withWWW-Authenticate: Basic realm="…")redirect/gone→ 30x/410, donerewrite→ new internal pathindex/autoindex/fallback→ determine fileheader/cache/corsadded;erroron 4xxImplementation notes
Develry.HhaccessFile(parse + match) and anHhaccessResolverperStaticSitethat resolves path → stacked config, cached onmtime.StaticSite.handleRequestbefore ecstatic.authcan reuseSite#checkBasicAuthby injecting the folder's credentials array.basic_authand.hhaccessauthstack: folder auth does not replace site auth, both must pass.StaticSitesetting:hhaccess: Boolean(default on), so it can be disabled for untrusted upload folders.