DomainLens

Guides

nginx for SEO: Redirects, Status Codes, Compression and HTTP/2

Four parts of an nginx config decide how a site looks to a crawler: which hostname it settles on, what status codes it returns, how much it sends, and how fast it can send it.

Check your site before you start fixing

Run a fresh DomainLens audit and use the report as your priority list.

Run a free SEO audit

One canonical hostname, one hop

Decide on a single scheme and hostname, then send everything else there in exactly one redirect. The mistake is chaining: an HTTP-to-HTTPS server block, then a separate www-stripping block, so a request travels twice before it arrives.

A dedicated redirect server block that fixes both at once costs nothing and removes the chain entirely.

Canonical redirect in one hop
# Everything that is not https://example.com goes there directly
server {
    listen 80;
    listen 443 ssl;
    http2 on;
    server_name www.example.com example.com;

    # Only redirect when we are not already canonical
    if ($host != example.com) { return 301 https://example.com$request_uri; }
    if ($scheme != https)     { return 301 https://example.com$request_uri; }
}

# Prefer return over rewrite: it is faster and cannot
# accidentally fall through to another rule.

Status codes nginx gets wrong by default

The SPA fallback is the dangerous one. A blanket try_files $uri /index.html means every nonexistent URL returns 200 with the app shell, and Google indexes an unbounded set of soft 404s. Route the fallback deliberately rather than catching everything.

SituationCorrect responseCommon misconfiguration
Page does not exist404 with a useful pageRewriting to /404.html with a 200
Page permanently gone410Serving 404 forever
Moved permanently301302, because it is the default
SPA fallback route200 for real routes onlytry_files sending everything to index.html
Upstream down503 with Retry-After500, which reads as a broken site
A custom 404 that keeps its status
error_page 404 /404.html;

location = /404.html {
    internal;          # not reachable directly
    root /var/www/html;
}

# WRONG — this returns 200 for missing pages:
# error_page 404 =200 /404.html;
#
# Verify:  curl -sI https://example.com/nope | head -n1
#          should read HTTP/2 404

Compression and protocol

HTTP/2 removes the connection limits that made asset concatenation necessary, but it does not fix a slow origin. Measure before assuming it helps — and see gzip vs brotli for choosing the encoding.

  • Enable gzip for text types and serve pre-compressed brotli for static assets where available.
  • Set gzip_min_length to around 256 — below that the overhead outweighs the saving.
  • Leave woff2, images and video out of gzip_types; they are already compressed.
  • Enable HTTP/2 with the http2 directive on the listen block — the old listen … http2 syntax is deprecated in current nginx.
  • Add gzip_vary on so caches store the compressed and uncompressed variants separately.

Headers worth setting at the server

nginx is the right place for headers that should apply everywhere, because setting them in application code means every route has to remember.

There is one behaviour that catches nearly everyone: add_header does not accumulate across configuration levels. A location block containing its own add_header discards every header inherited from the server block rather than adding to them. A site can therefore have a correct HSTS header on most pages and silently lose it on the one location that sets a cache header, with nothing in the config looking wrong.

The always parameter matters for the same reason. Without it, add_header applies only to successful responses, so error pages and redirects go out without your security headers. Since redirects are exactly what an attacker would target, always is the safer default for anything security-related.

Caching and security headers
# Fingerprinted assets: cache hard
location ~* \.(css|js|woff2|png|jpg|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

# HTML: revalidate so updates are seen
location / {
    add_header Cache-Control "no-cache";
}

# Applies to every response
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;

# Careful: a location-level add_header replaces
# inherited ones rather than adding to them.

How DomainLens contributes

DomainLens reports the status codes, redirect chain, compression and cache policy a live URL returns — which is how a config that looks right on disk gets checked against what it actually serves. Use the HTTP headers checker for a single URL, and see the technical SEO checklist for the wider sweep.

Does nginx configuration affect rankings?
Through what it returns, yes: status codes, redirects and speed all matter. The software itself is not a factor — Apache configured equivalently performs the same in search.
Why does my custom 404 page return 200?
Almost certainly an error_page directive with =200, or an SPA fallback catching every unmatched URL. Both produce soft 404s that Google will not index but will keep crawling.
Is HTTP/2 worth enabling for SEO?
It is worth enabling, and it is essentially free. It improves how efficiently many resources load, but it will not rescue a slow server response.
return or rewrite for redirects?
return, whenever the destination is known. It is faster, clearer, and cannot accidentally continue into another rule the way rewrite can.

Related resources