{ admin off auto_https off } :8000 { bind 127.0.0.1 handle_path /ll_api/* { reverse_proxy 127.0.0.1:8001 } # Bursae is a complete SPA/BFF at its own origin, mounted under a public # prefix. `handle_path` removes /bursae before forwarding, so its backend can # retain normal /api and /assets routes. Its content may be framed only by the # specified Bursae site. redir /bursae /bursae/ permanent handle_path /bursae/* { header { -X-Frame-Options Content-Security-Policy "frame-ancestors 'self' https://bursae.manueldeprada.com" } reverse_proxy 127.0.0.1:4100 } # Vendored renderer assets (markdown-it, KaTeX, mdrender.js), served by their # own file_server so the listing below can hide `vendor` without breaking # them: `hide` matches every component of a path, so a hidden `vendor` would # 404 /vendor/* too and every .md page would lose its renderer. No `browse` # here, so /vendor/ itself is not listable. handle /vendor/* { root * /home/prada/static_serve file_server } handle { root * /home/prada/static_serve # The public demo is iframed into littlelearner-ll.github.io, a different # origin, so it has to be explicitly framable from there (and only from # there + localhost, for previewing that site locally). Everything the # page loads is same-origin, with script and styles inline in the # document, hence 'self' + 'unsafe-inline' and no other sources. @demo { path /ll-demo-chat /ll-demo-chat/* not path /ll-demo-chat/stats* } header @demo Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self' https://littlelearner-ll.github.io http://localhost:* http://127.0.0.1:*" # The stats dashboard is NOT for embedding: same policy, frame-ancestors # 'none'. There is no PHP in this stack (static Caddy + FastAPI, no # php-fpm), so the requested .php URL is served by rewriting it to the # real HTML page, which reads the password-gated /demo/stats endpoints. @stats path /ll-demo-chat/stats.php /ll-demo-chat/stats.html header @stats { Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" Cache-Control "no-store" X-Robots-Tag "noindex, nofollow" } rewrite /ll-demo-chat/stats.php /ll-demo-chat/stats.html # Raw markdown source: /foo.md?raw=1 -> download the original file @mdraw { path *.md query raw=1 } header @mdraw Content-Disposition attachment # Rendered markdown view: any other *.md -> HTML via the renderer template. # Scoped in its own handle so `templates` only touches the renderer and # never tries to parse arbitrary .html files under the root as templates. # CLAUDE.md is excluded: it is in the hide list below, and without this it # would still answer 200 with a renderer shell whose ?raw=1 fetch 404s. @mdview { path *.md not query raw=1 not path /CLAUDE.md */CLAUDE.md } handle @mdview { rewrite * /_render.html templates file_server } file_server browse { hide Caddyfile Caddyfile.bak CLAUDE.md _render.html tmux-client-2448020.log vendor } } }