
Security headers ship with your build
Ivan Roslov2 min readYour static site can now set its own response headers from a text file. Drop a _headers file in your build root, and Lessly's origin applies it to every response. Security headers on static hosting used to mean bringing your own nginx or standing up an edge function for the one job. Now it is one file, in the same format Cloudflare Pages and Netlify already read. I wanted our own headers to travel with the build, not live in a proxy someone has to remember.
The file lives at the output directory — dist/_headers. A path, then indented header lines under it:
# security headers, whole site
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=()
# one header, one page
/pricing
Link: <https://lessly.com/pricing>; rel="canonical"
Deploy that, and the headers are on the wire. Here is our own site, answering with its own headers:
$ curl -sI https://lessly.com/
link: </llms.txt>; rel="alternate"; type="text/plain"
permissions-policy: camera=(), microphone=()
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: DENY
cache-control: no-cache, must-revalidate
The platform defaults survive: hashed assets keep their year-long immutable, and HTML keeps the no-cache you can read in that output. Your _headers file is an overlay on top, not a replacement.
It is safe by construction. A deny-list keeps you off the headers that carry the response itself (Content-Type, Content-Length, Content-Encoding, ETag, and the hop-by-hop set), while Cache-Control stays overridable, and a malformed or oversized (over 64 KB) file is ignored rather than served, so a bad edit cannot take the site down. There is no panel to configure: the file ships and switches atomically with the deploy, and it applies on every serving path, down to your own 404.html.
Because the format matches Cloudflare Pages and Netlify, an existing _headers migrates as-is — path rules read top to bottom with the last match winning, * splats across slashes, :name matches one segment, ! Header-Name removes a header, and absolute-URL rules are skipped.
Look again at the first line of that output — Link: </llms.txt>. A Link header (RFC 8288) points a page at its own alternates, so a reader that would rather not parse your HTML (an agent, a feed tool) reaches the plain-text copy from the response alone.
We put lessly.com's own headers there the same way: one file in the build. So check it yourself. Run curl -sI against your own Lessly static site and read your headers back off the wire.
Related posts
Building Lessly in the open
Follow along, and get early access to the private beta.


