# Security headers applied site-wide. Wave 1K. # These are static-site recommended defaults. CSP is intentionally NOT set here # because the site emits inline JSON-LD (SEOHead.astro) and inline GTM (when # PUBLIC_ENABLE_SEO=true); adding `script-src` without a nonce strategy would # silently break both. CSP rollout is tracked as a separate post-deploy task. # # The site-wide page cache belongs in this block rather than a `/*.html` one at # the end of the file, for two independent reasons. Patterns match the REQUEST # path, and `trailingSlash: 'never'` means a page arrives as `/pricing`, never # `/pricing.html` — so a `.html` pattern matches no real page URL at all. And # every matching block applies in file order with the last one winning, so a # catch-all placed below `/_astro/*` would strip `immutable` back off every # hashed asset. First block, so everything below is free to override it. /* Strict-Transport-Security: max-age=31536000; includeSubDomains; preload X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin X-XSS-Protection: 1; mode=block Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() Cache-Control: public, max-age=300 /_astro/* Cache-Control: public, max-age=31536000, immutable /images/* Cache-Control: public, max-age=2592000 /resources/images/* Cache-Control: public, max-age=2592000 /resources/_astro/* Cache-Control: public, max-age=31536000, immutable /docs/* Cache-Control: public, max-age=60, stale-while-revalidate=300 /docs/llms.txt Cache-Control: public, max-age=3600 /docs/llms-full.txt Cache-Control: public, max-age=3600 /docs/updates.xml Cache-Control: public, max-age=600 /developer-docs/* Cache-Control: public, max-age=60, stale-while-revalidate=300 /developer-docs/llms.txt Cache-Control: public, max-age=3600 /developer-docs/llms-full.txt Cache-Control: public, max-age=3600 /developer-docs/updates.xml Cache-Control: public, max-age=600 /developer-docs/.health.json Cache-Control: public, max-age=60 # The /integrations directory full-text dump, same lifetime as its docs and # developer-docs siblings above. /integrations/llms-full.txt Cache-Control: public, max-age=3600 # The `.md` corpus. Must stay below `/*` — all matching blocks apply in # file order and the later one wins, so this is what overrides the site-wide # defaults above. # # `noindex`, because each of these ~4.6k files is a near-verbatim duplicate of # an indexable HTML page that actively points crawlers at it via # ``. A .md carries no and no # , so the HTTP header is the only index control available. noindex does # not block fetching — agents still read them; it only keeps the duplicates out # of the index. # # This X-Robots-Tag is the SOLE de-indexing control for the whole .md corpus, # and it is load-bearing: nothing else keeps these files out of the index. # It only works if the file is actually fetched. Never add a robots.txt # `Disallow` for `*.md` on top of it — a Disallow blocks the fetch, so Google # never reads this header, and the `rel="alternate"` link on every HTML page # leaves the .md eligible for URL-only indexing instead. See the # tried-and-reverted note in src/lib/robots-policy.mjs. # # REMOVED — do not re-add. `{path}` is a literal placeholder that # zopdev/static-server:v1.0.0 does not interpolate, so the header was either # absent from responses or would have advertised a literal `{path}` URL. Verified # against prod: `curl -sD- https://zop.dev/pricing.md` returns X-Robots-Tag from # this same block but no Link header at all. It was dead config making a promise # it did not keep. If a per-file canonical is ever wanted, it needs the .md route # to emit it, or a static-server release that documents real interpolation. # # `Content-Type`, because the route that emits these cannot set it: Astro runs # `output: 'static'`, which serialises endpoints to disk and drops the Response # headers. Downstream, Go's builtin MIME table has no `.md` entry and the # distroless image ships no /etc/mime.types, so an unpinned `.md` is sniffed to # text/plain — and static-server leaves a *directly requested* .md as-is even # with content negotiation in place. This line is the only declaration. # # This block depends on zopdev/static-server#23, which is what teaches the server to # read this file at all — merged, so every block here is live from the next # static-server release. Before it, the whole file was inert, the # HSTS/X-Frame-Options block at the top included. # # `Cache-Control` is declared rather than inherited. Under merge-and-last-wins # each .md would otherwise pick up a lifetime by accident of block order — 60s # under /docs/* and /developer-docs/*, 300s from /* elsewhere — and under # replace semantics it would lose nosniff along with it, on the one file type # this block exists to pin a Content-Type for. Stating it makes the result the # same either way. 300s matches the /* page default: a .md changes exactly when # its HTML page does. /*.md X-Robots-Tag: noindex Content-Type: text/markdown; charset=utf-8 Cache-Control: public, max-age=300 # Wave 1L — tune homepage + content-index caches to crawl-budget-friendly values. # Was `max-age=0, must-revalidate` which burned crawl budget on every Googlebot # hit. SWR lets the CDN serve stale immediately while revalidating in the # background — fast for users, easier on the origin. # RFC 8288 Link headers, on the one page an agent reaches without being told a # URL. Everything named here is discoverable another way — the alternate is also # a in the HTML, the sitemap is in robots.txt, llms.txt names the rest — # but only from inside a body the agent has to download and parse first. A HEAD # request now answers "where is the machine-readable version of this site" in # one round trip. # # In THIS block rather than a second `/` one: matching blocks merge and the last # value wins per header name, so two root blocks would work but leave a reader # checking which. The alternate is per-page, which is why it is not on `/*` — # advertising `` everywhere would point every page at the homepage's # markdown. # # rel values are the registered ones: `alternate` (RFC 8288), `describedby` # (RFC 6906), `service-desc` (RFC 8631). `sitemap` is the widely-used # unregistered relation, and is what scanners look for. / Cache-Control: public, max-age=3600, stale-while-revalidate=86400 Link: ; rel="sitemap", ; rel="alternate"; type="text/markdown", ; rel="service-desc"; type="application/json", ; rel="describedby"; type="text/plain", ; rel="api-catalog" /resources/blogs Cache-Control: public, max-age=3600, stale-while-revalidate=86400 /resources/ebooks Cache-Control: public, max-age=3600, stale-while-revalidate=86400 # Wave 1 — explicit cache + content-type for new static SEO files. /robots.txt Cache-Control: public, max-age=3600 /llms.txt Cache-Control: public, max-age=3600 /.well-known/security.txt Cache-Control: public, max-age=86400 # The MCP pointer manifest. Unlike the `.md` corpus above, no Content-Type line # is needed: Go's builtin MIME table does know `.json`, so static-server serves # it correctly unaided. # # ACAO is belt-and-braces rather than load-bearing — prod already returns # `Access-Control-Allow-Origin: *` on `/` today. It is declared here anyway # because this file's own comments (see the merge-vs-last-wins note above) # leave it unsettled whether a later matching block MERGES with `/*` or # REPLACES it. Under replace semantics an inherited ACAO would vanish exactly # where it matters, since MCP clients and readiness scanners fetch this # cross-origin from a browser context. Stating it makes the result identical # either way — the same defensive reasoning the `/*.md` block uses for # Cache-Control. # # One hour, matching /llms.txt: this file changes only when the MCP server's # endpoints or scopes do, which is rare, but it must not go stale for a day # when they do. # nosniff + X-Frame-Options are restated below rather than inherited from # `/*`. The /*.md comment above records that merge-vs-replace is unsettled; # under replace semantics this block would otherwise drop both. /.well-known/mcp.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 # The rest of the machine-readable discovery set, all generated by # scripts/generate-well-known.mjs. # # These blocks are not optional. `_headers` patterns are matched per request # path and every rule that matches contributes, but there is no `/.well-known/*` # glob here — only exact paths. Without a block, each of these falls through to # `/*` alone and loses its Access-Control-Allow-Origin, which is the one header # that decides whether a browser-context MCP client or readiness scanner can # read it at all. A silently unreadable discovery document is worse than a # missing one: the client sees a network error, not a 404, and cannot tell why. # # All of these were unreachable in production until the Dockerfile stopped # copying the site into ./static — GoFr registers /.well-known/{name} as a # Swagger catch-all when it finds ./static/openapi.json, and answered 500 for # every single-segment path under the prefix. See the comment in the Dockerfile. /.well-known/mcp/server-card.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 /.well-known/agent-skills/index.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 /.well-known/ard.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 /.well-known/agent-card.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 # RFC 9727 requires this path to carry NO file extension, so nothing can sniff # its type — Go would serve it as text/plain and a client checking the media # type would reject it. This Content-Type line is the only thing that makes the # document readable as what it is, and the profile parameter is part of the # spec, not decoration. /.well-known/api-catalog Content-Type: application/linkset+json;profile="https://www.rfc-editor.org/info/rfc9727" Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 # The generated OpenAPI document (src/pages/openapi.json.js). Same reasoning as # the mcp.json block above: no Content-Type line needed because Go's MIME table # knows .json, and ACAO restated rather than inherited because merge-vs-replace # is unsettled and API-spec tooling fetches this cross-origin. # # Astro runs `output: 'static'`, so the Content-Type the route sets is dropped # when the endpoint is serialised to disk — this block is the only header # declaration that actually reaches production. # nosniff + X-Frame-Options are restated below rather than inherited from # `/*`. The /*.md comment above records that merge-vs-replace is unsettled; # under replace semantics this block would otherwise drop both. /openapi.json Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: DENY Cache-Control: public, max-age=3600 # Drawer body fragments, fetched on demand by DrawerFeatures.jsx. These are the # 27 `