Skip to content

Support .hhaccess per-directory config files for StaticSite #11

Description

@roelvangils

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

  1. hidden → 404
  2. deny/allow → 403
  3. auth → 401 (with WWW-Authenticate: Basic realm="…")
  4. redirect/gone → 30x/410, done
  5. rewrite → new internal path
  6. index/autoindex/fallback → determine file
  7. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions