# catcrafts.net Caddy site block # # Two upstreams: Caddy's file_server for build artifacts, and catcrafts-server # for everything else. Point `root` at the host directory you bind-mount into # the runner as /deploy (the "-v /path/to/webroot:/deploy" in the runner's # config.yaml). # # catcrafts-server speaks PLAINTEXT HTTP/1.1 on localhost — Caddy terminates # TLS. That is also why the backend uses Crafter.Network's ListenerHTTP1 rather # than its HTTP/3 listener: Caddy cannot reverse_proxy to an h3 upstream. # Do not expose port 8081 directly. catcrafts.net { root * /srv/catcrafts.net encode zstd gzip header { Referrer-Policy "strict-origin-when-cross-origin" X-Content-Type-Options "nosniff" -Server } # ── cross-origin isolation, scoped ──────────────────────────────────── # # The WASM runtime needs a cross-origin-isolated context (SharedArrayBuffer # / threads), and these three headers are what provide it. They are NOT # optional on a page that boots the module — without them it loads and the # runtime fails. # # But they are scoped to the paths that actually load it, rather than applied # site-wide. COEP: require-corp blocks every cross-origin subresource that # does not opt in, so applying it to pages that have no wasm would constrain # them for no benefit — and the shop's payment pages later must not inherit # that restriction. @isolated path /demos/* /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js \ /catcrafts-head.js /files.json /variants.json /*.wgsl header @isolated { Cross-Origin-Opener-Policy "same-origin" Cross-Origin-Embedder-Policy "require-corp" Cross-Origin-Resource-Policy "same-origin" } # Subresources an isolated document pulls in must carry CORP themselves. header /styles.css Cross-Origin-Resource-Policy "same-origin" header /favicon.svg Cross-Origin-Resource-Policy "same-origin" # ── build artifacts: served from disk ───────────────────────────────── # # file_server does sendfile, precompressed variants and range requests far # better than the backend would. `precompressed` serves the .zst / .gz # siblings the CI build produces, so the ~800 KB module is never recompressed # per request. Cache-busted by the ?v= in index.html. @static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js \ /catcrafts-head.js /files.json /variants.json /styles.css \ /favicon.svg /robots.txt /*.wgsl /*.jpg /posts.json /rates.json handle @static { header Cache-Control "public, max-age=31536000, immutable" file_server { precompressed zstd gzip } } # ── mirrored post media ────────────────────────────────────────────── # # Deliberately NOT under the web root: that directory is mirrored with # `rsync --delete` on every deploy, and this media is not always # reproducible — if a source instance deletes a file, our copy is the only # one left. Living on the app mount puts it physically outside the delete. # # Filenames are the content hash, so a changed file gets a new name and the # immutable cache lifetime is honest. handle_path /media/* { root * /srv/catcrafts-app/media header Cache-Control "public, max-age=31536000, immutable" file_server } # ── everything else: server-rendered ───────────────────────────────── # # Pages, /feed.xml, /sitemap.xml and later /api/*. The backend sets its own # Cache-Control and returns real status codes — a 404 for an unknown path and # a 301 for the retired /blog URLs, which a client-side router cannot do. handle { reverse_proxy 127.0.0.1:8081 { health_uri /api/healthz } } # If the backend is down, fall back to the static wasm shell so the site # degrades to a client-rendered app rather than a bare Caddy error page. # This must be handle_errors, not handle_response: handle_response only # fires on responses an upstream actually sent, and a backend that is not # running refuses the connection — that is an error, which only # handle_errors sees (verified against a dead port). The error status is # kept, so a crawler sees the honest 502 while a human gets the working # client-rendered app; only SSR and real 404s are lost. handle_errors 502 503 504 { rewrite * /index.html header Cache-Control "no-store" file_server } }