:8000 { root * /srv file_server # SPA fallback — rewrites unknown paths to index.html try_files {path} /index.html # Security headers header { X-Content-Type-Options nosniff X-Frame-Options DENY Referrer-Policy strict-origin-when-cross-origin # Remove Caddy's Server header -Server # Same policy as the tag in index.html — keep the two in sync. # As a real header it can carry frame-ancestors, which browsers ignore # when CSP arrives via . # # frame-src / worker-src / manifest-src are spelled out even though they # already resolve to 'self' through default-src, so that the intent is # visible next to the directive it constrains. There is no third-party # frame source: that is what keeps a location message from leaking the # reader's IP to a tile server. See the note in index.html. Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: http: https:; img-src 'self' data: blob: http: https:; media-src 'self' blob: data: http: https:; font-src 'self' data:; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'self'; worker-src 'self'; manifest-src 'self'; frame-ancestors 'none'" } # HSTS only over TLS: sent in the clear it is ignored by browsers anyway, and # advertising a max-age on a plain-HTTP listener buys nothing. @tls expression {http.request.scheme} == "https" header @tls Strict-Transport-Security "max-age=31536000" # Gzip compression (Caddy does this automatically via zstd + gzip) encode zstd gzip # Cache static assets aggressively (JS/CSS have content hashes from Vite) @static path *.js *.css *.woff2 *.woff *.ttf *.ico *.png *.jpg *.svg *.webp header @static Cache-Control "public, max-age=31536000, immutable" # Never cache index.html (so deploys take effect immediately) @html path *.html header @html Cache-Control "no-cache" }