SeeDropHost

Redirects and headers

Checked against the code on 2026-10-30 (version 0.1.0) · Markdown version

Rules can come from two places, with the same behaviour:

  1. a _redirects file and a _headers file at the root of the deploy;
  2. [[redirects]] and [[headers]] in netlify.toml or seedrop.toml (same format; seedrop.toml wins if both exist). These come after the _redirects / _headers rules.

Lines that are not understood are listed in the deploy report under rules.problems (file and line) and are ignored. The rule files themselves (_redirects, _headers, netlify.toml, seedrop.toml) are never served as public files.

_redirects

One rule per line: from to [status][!] [Condition=a,b]. Text after # is a comment.

# permanent redirect (301 is the default)
/old-page          /new-page
/blog/*            /articles/:splat        301
/products/:id      /p/:id                  302
# rewrite: serve another file, URL stays the same
/app/*             /app/index.html         200
# custom 404 for a section
/docs/*            /docs/404.html          404
# force: applies even if /promo.html exists
/promo             /sale                   302!
# only for requests that arrive on this host
https://example.com/*   https://www.example.com/:splat   301!
# conditions
/                  /pt/                    302  Language=pt
/                  /br/                    302  Country=br
# proxy to a public address
/api/*             https://api.example.com/:splat  200
# send to a function (needs SDH_FUNCTIONS=1 on the server)
/api/*             /.netlify/functions/:splat      200

What is supported

  • Status codes: 200, 301, 302, 303, 307, 308, 404, 410. Default: 301.
  • Splat: * captures the rest of the path; use it as :splat in the target. /blog/* also matches /blog.
  • Placeholders: :name captures one path segment.
  • Absolute origin: https://www.example.com/* only matches requests whose Host is that name. The protocol only counts when the server knows it (TLS on the socket, or X-Forwarded-Proto with SDH_TRUST_PROXY=1).
  • Order: the first matching rule wins; _redirects before the config file.
  • Shadowing: a rule without ! does not apply when the requested path exists as a file (404 rules always apply). Add ! to force it.
  • Query string: kept on redirects when the target has none.
  • Conditions: Language= matches the Accept-Language header (pt also matches pt-BR). Country= uses the CF-IPCountry header, so it only means something with Cloudflare in front — do not use it for anything that must be secure.
  • Targets: a path starting with / (not //), or a full http:// / https:// address. Anything else (//host, javascript:…) is refused with a problem in the report. If a path target turns into an outside address after :splat is filled in, the rule is ignored.
  • Proxy: a 200 rule whose target is http(s)://… fetches that address and returns its answer. Only public internet addresses are allowed — checked again at connection time, so DNS cannot switch to a private address afterwards — with no redirect following, a 10-second timeout and a 10 MB limit read as a stream. Only GET/HEAD reach it, and only the Accept header is forwarded.

Not supported

  • Other conditions (Role=, Cookie=) and query-parameter matching such as id=:id: the rule is parsed but never matches. Nothing redirects by mistake.
  • In netlify.toml: query, signed and headers inside [[redirects]] are ignored with a warning in the report.
  • Anonymous drops ignore rules that point outside the site.

_headers

A path line (starting with /, wildcards and :placeholders allowed) followed by indented Name: value lines.

/*
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
/assets/*
  Cache-Control: public, max-age=31536000, immutable
/*.md
  Content-Type: text/markdown; charset=utf-8

The same in netlify.toml:

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

All matching blocks apply, in order; a later value replaces an earlier one with the same name.

  • **Set-Cookie is always dropped** (with a problem in the report): every site shares the base domain with the dashboard, so a cookie could leak to other sites. Use a function to set cookies.
  • On anonymous drops, Content-Security-Policy, Service-Worker-Allowed and Access-Control-* are dropped too, and the page is always noindex.

Headers SeeDropHost sets by itself

HeaderValue
Content-Typefrom the file extension (HTML, CSS, JS, JSON, TXT, XML, SVG, images, fonts, PDF, MP4, webmanifest); others application/octet-stream
ETagfrom the content hash; If-None-Match answers 304
Cache-Controlpublic, max-age=0, must-revalidate
X-Content-Type-Optionsnosniff
X-SDH-Deployid of the deploy that answered
X-Robots-Tagnoindex on drafts, older deploys and anonymous drops
X-SDH-Preview1 when that noindex was added by SeeDropHost (not by your _headers) — the migration check uses it

Clean URLs, trailing slash and 404

  • /about serves /about.html; /docs/ serves /docs/index.html.
  • Each page has one address: if only about/index.html exists, /about answers 301 to /about/; if only about.html exists, /about/ answers 301 to /about. The query string is kept.
  • When nothing matches, /404.html is served with status 404 if it exists.