Serve with nginx
Serve a Reed build from nginx, at the root or under a base path, with correct Markdown types and caching.
This recipe is written from the nginx documentation and Reed’s output layout. It has not
been run against a live server. After your first deploy, check two things: a .md mirror
comes back as text/markdown, and a nested page URL such as /docs/ship/deploy/
resolves. The commands are in the last section.
nginx serves files from disk. You copy Reed’s build to the server and write a server
block. Nothing runs per request, and nothing calls back to Reed. That is the whole
guarantee on What you actually own.
nginx does not read _headers. The four things that file and other hosts handle for you
become four pieces of configuration here: content types, directory index resolution, the
404 page, and caching.
Choose where the files live
Section titled “Choose where the files live”Site at the root of its hostname. Copy the contents of dist/ to a directory and make
that directory the root.
Site under a path, with base: "/docs". The built files sit at the root of dist/,
but every URL starts with /docs/. You have two ways to line them up:
rootover a staged tree. Copydist/into/var/www/site/docs/and setroot /var/www/site;. The path on disk matches the URL, just like thestage/layout on Deploy. Use this when nginx serves only static files on that hostname, or when you want the same tree as your other hosts.aliasstraight at the build. Copydist/to/var/www/reed-docs/and map the location to it:location /docs/ { alias /var/www/reed-docs/; }. nginx replaces the/docs/prefix with that path. Use this when/belongs to another application on the sameserver. Keep the trailing slash on both the location and the alias path.
The rest of this page uses root over a staged tree.
1. Put the build on the server
Section titled “1. Put the build on the server”reed build --out distrsync -a --delete dist/ server:/var/www/site/docs/--delete removes files that are no longer in the build, so a page you deleted stops
being served. _headers is copied along with everything else. nginx ignores it.
2. The server block
Section titled “2. The server block”server { listen 80; server_name docs.example.com; root /var/www/site;
charset utf-8; charset_types text/html text/plain text/css text/markdown application/javascript text/javascript;
location /docs/ { try_files $uri $uri/ =404; }
location ~ \.md$ { types { } default_type text/markdown; }
location /docs/_astro/ { add_header Cache-Control "public, max-age=31536000, immutable"; }
error_page 404 /docs/404.html;
gzip on; gzip_vary on; gzip_types text/css text/plain text/markdown application/javascript text/javascript application/json image/svg+xml application/xml;}Add your TLS listen and certificate lines as usual. The sections below explain each
part.
Markdown mirrors are not downloads
Section titled “Markdown mirrors are not downloads”nginx picks a response type from its types map, usually the mime.types file shipped
with it. That file has no entry for .md, so the mirrors get default_type. That is
commonly application/octet-stream, and a browser downloads the file instead of showing
it.
The location ~ \.md$ block fixes that. An empty types { } clears the map for that
location, and default_type text/markdown then applies to every .md file. The nginx
documentation uses the same pattern to force a type for a location.
You can instead add text/markdown md; to a types block at http level. Do not put a
types block with only that line in a server block. A types block at a lower level
replaces the inherited map instead of adding to it, and every other file loses its type.
The charset
Section titled “The charset”Reed’s _headers asks for charset=utf-8 on both .md and llms.txt. Assistant
fetchers reject a text/* response with no charset. See
Response headers.
charset utf-8; makes nginx add the charset to the Content-Type of every type listed
in charset_types. The default list includes text/plain, which covers llms.txt, but
not text/markdown. That is why the example sets charset_types explicitly.
Directory pages and trailing slashes
Section titled “Directory pages and trailing slashes”Reed writes each page as ship/deploy/index.html and links to it as
/docs/ship/deploy/.
try_files $uri $uri/ =404 tries the exact file first, then the directory, then answers
404. When the directory form matches, nginx serves the index.html inside it. A request
for /docs/ship/deploy without the slash is redirected to /docs/ship/deploy/, which is
the form in Reed’s canonical URLs and sitemap.
.md mirrors and llms.txt are plain files. They match $uri directly.
The 404 page
Section titled “The 404 page”Every build includes 404.html. On a based site it is at docs/404.html, so
error_page 404 /docs/404.html; points there. The status stays 404. nginx serves the
page body with it.
The page references its CSS and scripts with absolute /docs/... paths, so it renders
correctly at any URL depth.
Caching
Section titled “Caching”Everything under _astro/ has a content hash in its file name. A changed file gets a new
name, so an old name never has new content. That makes these files safe to cache for a
year and mark immutable.
Do not apply that header to HTML, .md, llms.txt, the sitemap or pagefind/. Their
names stay the same between builds, so a long cache keeps readers on an old version. Leave
them to nginx’s default ETag and Last-Modified revalidation.
One nginx rule to know: add_header directives are inherited from the enclosing level
only if the current level has none of its own. If you add security headers at server
level, repeat them inside location /docs/_astro/, or they disappear from those
responses. nginx 1.29.3 and later also has add_header_inherit to change this.
Compression
Section titled “Compression”gzip on compresses text/html always. Other types are compressed only if you list
them in gzip_types. Include text/markdown and text/plain, since the mirrors and
llms.txt are text meant to be fetched whole. gzip_vary on adds Vary: Accept-Encoding
so shared caches keep compressed and plain copies apart.
Behind a reverse proxy or CDN
Section titled “Behind a reverse proxy or CDN”If nginx sits behind another proxy, the proxy passes requests through and serves nginx’s
headers. Nothing in Reed’s output depends on the proxy. Check that the proxy does not
override Content-Type for .md, and that it does not cache HTML longer than you intend.
Verify after the first deploy
Section titled “Verify after the first deploy”Check that a nested Markdown mirror has the right content type:
curl -s -o /dev/null -w "%{content_type}\n" https://docs.example.com/docs/ship/deploy.mdExpect text/markdown; charset=utf-8. application/octet-stream means the .md
location is not matching. A missing charset means charset_types does not list
text/markdown.
Check that a nested page resolves, and that llms.txt has its charset:
curl -s -o /dev/null -w "%{http_code}\n" https://docs.example.com/docs/ship/deploy/curl -s -o /dev/null -w "%{content_type}\n" https://docs.example.com/docs/llms.txtExpect 200 and text/plain; charset=utf-8. A 404 on the page usually means the files
are at /var/www/site/ instead of /var/www/site/docs/.
Finally, request a URL that does not exist and confirm a 404 status with Reed’s page, then grep a live page for a string from this build, as Deploy describes.
Related
Section titled “Related”- Cloudflare Workers uses the same staged layout at the edge.
- Vercel is the other host on these pages that needs the header rules translated.
- Build output lists every file you are serving.
- site and base for what
basechanges. - llms.txt for why its content type matters.