# The one Caddy snippet. # # Three server designs each brought their own, on three different ports, and all # three wrote to this filename. This is the one that replaced them: one service, # one port, one prefix. CONTRACT.md §5. # # Install it beside your Caddyfile and import it into whichever site serves the # Tera browser build: # # import /etc/caddy/snippets/tera-api.snippet # # tera.lumbridgecorp.com { # import tera_api # root * /srv/tera/dist # @assets path /assets/* # header @assets Cache-Control "public, max-age=31536000, immutable" # @documents not path /assets/* # header @documents Cache-Control "no-cache" # file_server # } # # The API and the static build are deliberately the same origin. Nothing here # needs CORS, which is why TERA_CORS_ORIGIN defaults to empty — set it only for # a Vite dev server on another port. # # # ## Two doors out of one root # # `office.` and `tera.` are one build, and the app reads its own hostname to # decide which one it is. A crawler cannot do that — it reads the HTML and # nothing else — so `npm run build` emits a second shell, `office.html`, which is # byte-identical below `` and carries the office's title, description and # share card. Point the office door's fallback at it and the two unfurl as the # two places they are: # # office.lumbridgecorp.com { # import tera_api # root * /srv/tera/dist # the SAME root as tera., not a copy # @assets path /assets/* # header @assets Cache-Control "public, max-age=31536000, immutable" # @documents not path /assets/* # header @documents Cache-Control "no-cache" # try_files {path} {path}/index.html /office.html # file_server # } # # The root is shared rather than copied on purpose — a deploy cannot then leave # the two doors on different builds — and `office.html` comes out of the same # `npm run build` as `index.html`, so the shells cannot drift from each other # either. The only line that differs between the two site blocks is the # `try_files` fallback. # # Keep the cache rules in the static handler when the site also serves the API. # The HTML shells and RELEASE_SHA are stable names and must revalidate after a # deploy; Vite's /assets/* files are content-addressed and may stay immutable. # # A deployment that skips this is not broken: the office door keeps working and # simply unfurls with the city's card, which is what it did before there was a # second shell at all. (tera_api) { handle /api/v1/* { reverse_proxy 127.0.0.1:8431 { # Fail fast rather than holding a browser connection open while the # API is restarting. systemd brings it back in under two seconds. transport http { dial_timeout 2s } } } # The API stamps its own Cache-Control — `private, no-store` by default, and # `public, max-age=…` only where a route opted in. Do not add a cache # directive here: this file cannot tell which route answered, and the # fail-closed policy is only fail-closed if nothing downstream overrides it. header { X-Content-Type-Options nosniff Referrer-Policy strict-origin-when-cross-origin } }