Guide

Redirects, headers and 404 pages

Move pages and add headers with _redirects and _headers files, choose caching, and control which file each URL serves and how your 404 page is used.

On this page

Two small text files in your site control how it answers: _redirects moves or rewrites URLs, and _headers adds HTTP headers. This guide also explains which file a URL serves, and how your own 404 page is used.

The syntax is the one other common static hosts use, so files you already have usually work unchanged.

Three related settings live in your project's Settings → Routing & SEO, with no file needed: how page URLs are spelled (trailing slashes and .html), whether unknown URLs fall back to index.html for a single-page app, and whether search engines are asked to stay away. They are described in URL settings below.

Where the files go

Put _redirects and _headers at the top of your published site, next to index.html:

How you deployWhere the files go
Import from GitHubThe top of your output directory. With a framework, that usually means the public (or static) folder of your project, which the build copies into the output.
Upload a zipThe top of the zip, next to index.html.
CLIThe top of the folder you deploy.

Files with these names in subfolders are ignored. Neither file is served to visitors: https://example.com/_redirects is a 404. Paths under .zeloxa/ are reserved for Zeloxa and are not published.

What you'll see: in the deployment log, a line such as "Site config: 3 redirect rules, 2 header rules." The files are read once, when you deploy. Change them and deploy again for changes to take effect.

_redirects

One rule per line: where from, where to, and an optional status code.

# from           to                         status
/old-page        /new-page                  301
/blog/*          /news/:splat               301
/team/:name      /people/:name              301
/shop            https://shop.example.com   302
/app/*           /app/index.html            200

Status codes

StatusWhat it does
301Permanent redirect. Use it when a page has moved for good.
302Temporary redirect. This is the default when you leave the status out.
303, 307, 308Also redirects. 308 is the permanent one that keeps the request method.
200A rewrite: the visitor stays on the URL they asked for, and is shown the destination's page.

A ! after the status (301!, 200!) forces the rule: it applies even when a file exists at the URL. Without it, a file wins (see Matching).

Matching

  • * matches the rest of the path, including slashes. Use it in the destination as :splat. /blog/* also matches /blog itself.
  • :name, as a whole path segment, matches one segment. Use it in the destination as :name.
  • Matching ignores the query string and a trailing slash, so /about and /about/ are the same. It is case-sensitive: /About does not match /about.
  • The visitor's query string is kept: /old-page?ref=mail goes to /new-page?ref=mail.
  • Rules are checked from top to bottom. The first match wins.
  • A rule only applies when no file exists at the URL. If /about would serve about.html, a /about /about-us 301 rule is skipped. To move a page that is still in your build, force the rule: /about /about-us 301!.
  • A rule that would send visitors to the URL they are already on is skipped, so you cannot create a loop by accident.

Destinations

  • A path on your site, starting with /.
  • Or a full address starting with https://.

A 200 rewrite must point at a path on your own site. It cannot fetch another website.

What is refused

A line that cannot be used is skipped, with a warning in the deployment log. The deploy itself still succeeds. Lines are refused when:

  • the status is not one of 200, 301, 302, 303, 307 or 308;
  • the line has more than three parts (conditions such as Country= or Role=, and matching on query parameters, are not supported);
  • the source does not start with /, contains ? or #, or has more than one *;
  • :splat is written in the source (use *), or one :name appears twice;
  • the destination starts with http://, // or anything other than / or https://, contains spaces, or contains a user name or password;
  • a placeholder is used in another website's address (https://:city.example.com/);
  • the destination uses a :name the source does not define;
  • a 200 rewrite has a query string or # in its destination.

_headers

A path pattern on its own line, then the headers for it, each on an indented line:

/*
  X-Frame-Options: DENY
  Permissions-Policy: camera=(), microphone=()

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/downloads/*
  Content-Disposition: attachment
  • Patterns use the same * and :name rules as _redirects.
  • Every matching pattern applies, from top to bottom. If two patterns set the same header, the values are joined with a comma.
  • :name and :splat in a value are filled in from the pattern.
  • ! Header-Name on an indented line removes a header.
  • Header rules apply to every page and file served, including your 404 page. They do not apply to redirects.

These headers cannot be set or removed, and are refused with a warning: Content-Length, Content-Encoding, Content-Range, Transfer-Encoding, Connection, Keep-Alive, TE, Trailer, Upgrade, Host, Set-Cookie, Location (use _redirects), ETag, Date, Age, Server, and any name starting with X-Zeloxa-, CF- or Proxy-.

Zeloxa always sends X-Content-Type-Options: nosniff. Its default Referrer-Policy: strict-origin-when-cross-origin can be replaced or removed (! Referrer-Policy). Security headers you turn on under Settings → Security headers (such as HSTS) take priority over the same header in _headers. On a password-protected site, Cache-Control is always private, no-store.

Lines are refused when a pattern line contains a space (header lines must be indented), a header line comes before any pattern, a header name is not valid, or a value contains control characters. A pattern with no headers under it is skipped too.

Examples

Move pages

/about-us          /about           301
/blog/2023/*       /blog/:splat     301
/products/:id      /shop/:id        301

Send the old domain's paths to a new site

/*    https://www.new-example.com/:splat    301

Single-page apps (React, Vue and others)

A single-page app needs every unknown URL to load index.html, so its own router can show the right screen.

You usually do not need a rule. Zeloxa recognises a single-page app when you deploy it and answers every unknown URL without a file extension with index.html (see SPA fallback). To be sure, set SPA fallback to On under Settings → Routing & SEO.

The rule other hosts use works here too:

/*    /index.html    200

Files that exist (your scripts, styles and images) are still served, because a rule only applies where there is no file. Do not write it as 200!: a forced rule would answer those files with index.html as well.

Security headers

/*
  X-Frame-Options: SAMEORIGIN
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Cross-Origin-Opener-Policy: same-origin

HSTS and a Content Security Policy can also be switched on without a file, under Settings → Security headers.

Caching

Zeloxa picks sensible caching for you:

FileDefault Cache-Control
.html, .htm, .json, .xml, .txt, .webmanifestpublic, max-age=0, must-revalidate (always checked, so a deploy shows at once)
Names with a content hash, such as app.4f2a9c1b.js, logo-3e8a51f0.png or index-B4kP_x9Q.js, and everything under _astro/ and _next/static/public, max-age=31536000, immutable (kept for a year)
Everything elsepublic, max-age=0, s-maxage=86400, stale-while-revalidate=604800

Override it with _headers when you know better:

/fonts/*
  Cache-Control: public, max-age=31536000, immutable

/feed.xml
  Cache-Control: public, max-age=300

Warnings in the deployment log

Problems in either file never fail a deploy. Each skipped line is reported as a warning in the deployment's log (on the Deployments tab choose View logs, or npx zeloxa logs from the CLI):

Site config: 2 redirect rules, 1 header rule.
Warning: _redirects line 4: destination must start with / or https://; skipped.
Warning: _headers line 7: Set-Cookie cannot be set from _headers; skipped.

Up to 25 warnings are listed, then "…and N more warnings."

Limits

LimitValue
Redirect rules2,000 (the rest are skipped, with a warning)
Header path patterns100
Headers under one pattern50
Line length, either file2,000 characters
Size of either file512 KB (a larger file is ignored, with a warning)

Which file a URL serves

For a static site, Zeloxa looks for these files, in order, and serves the first one that exists:

The visitor asks forZeloxa tries
/index.html
/aboutabout.html, then about/index.html
/about/about/index.html
/about.htmlabout.html
/assets/app.jsassets/app.js
  • A path whose last part has an extension (.js, .png, .pdf, or any . followed by letters or digits) is looked up exactly as written.
  • With the default Trailing slash: Auto setting, /about/ redirects to /about, and /about.html is served as it is. Choose Always or Never under Settings → Routing & SEO for one canonical form (see URL settings).
  • A _redirects rule that matches is used only when the lookup above finds no file, unless the rule is forced with !.

If nothing matches, see the next section.

Your 404 page

When no file matches (and no _redirects rule applies):

  1. With SPA fallback: On, a URL without a file extension is answered with index.html and status 200, even if you ship a 404.html.
  2. Otherwise, if your site has a 404.html at the top, it is shown, with status 404. This applies to every missing URL, pages and files alike. Only the top-level 404.html is used.
  3. Otherwise, with SPA fallback on (or Auto for a build that looks like a single-page app), a URL without a file extension gets index.html with status 200.
  4. Otherwise, a plain "Page not found." page is shown, with status 404.

A missing /robots.txt is the exception: see Search engines.

URL settings

These are per project, under Settings → Routing & SEO, and also in the API (GET and PATCH /sites/{siteId}/settings). Changes take effect within 30 seconds, without a new deploy.

Trailing slash

SettingWhat visitors get
Auto (default)/about/ redirects to /about. /about serves about.html or about/index.html. /about.html is served as it is.
AlwaysPage URLs end in a slash. /about, /about.html and /about/index.html redirect to /about/.
NeverPage URLs have no slash and no .html. /about/, /about.html and /about/index.html redirect to /about.

Redirects are 308 and keep the query string. With Always or Never, /index.html redirects to /, and a page is only redirected when its canonical URL serves the same file. URLs of other files (/app.js) and anything under /api/ are never changed. Always suits sites built as folders (about/index.html); a page built as about.html that uses relative links can break under it.

SPA fallback

What a URL without a file extension that matches no file gets:

SettingBehaviour
Auto (default)Decided when you deploy: on if the build looks like a single-page app, off otherwise.
Onindex.html with status 200, for your app's router.
OffYour 404.html (or a plain not-found page) with status 404.

A build looks like a single-page app when it has no 404.html, its index.html loads a script file, and it has at most two other HTML files. A 200.html at the top also marks one. Builds deployed before this setting existed keep working as they always did, as single-page apps. The deployment log says which way a build was read: "Looks like a single-page app" or "Looks like a multi-page site".

Search engines

  • Discourage search engines sends X-Robots-Tag: noindex, nofollow with every response and serves a robots.txt that disallows everything.
  • If your build has no robots.txt, Zeloxa serves one: User-agent: * and Allow: /, plus a Sitemap: line when your build has a sitemap.xml. On preview addresses, and with Discourage search engines on, it says Disallow: / instead. Your own robots.txt always wins and is served exactly as you wrote it.
  • Every address of your site redirects plain HTTP to HTTPS.
  • Once a custom domain is verified, it becomes the project's primary domain and the project's own address redirects to it (choose another, or keep both live, on the Domains tab). Preview addresses are never redirected.

Full-stack apps

Builds that include server code (a _worker.js at the top) work differently. Files are only served when they exist at exactly the requested path; everything else goes to your app. _redirects rules and the 404 rules above are not applied, because your app's router owns its URLs. _headers rules still apply to the files that are served.

Ready when you are

Everything in this guide is under Hosting in your dashboard.

Open Hosting

Something unclear or missing? Tell us.