MIME Types, Charset, and Caching Headers

Introduction

Browsers rely on Content-Type and charset to render pages correctly, and on cache headers to avoid re-downloading assets on every visit. Nginx sets these via mime.types, charset, expires, and add_header. This chapter fixes wrong MIME issues, ensures UTF-8 text, and applies sensible cache policies for static sites.

Prerequisites

MIME Types and mime.types

Nginx maps file extensions to MIME types:

nginx
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
}

Code explanation:

  • mime.types defines hundreds of mappings (.htmltext/html, .csstext/css)
  • default_type applies when extension is unknown—often triggers download instead of display

Common mappings

ExtensionContent-Type
.htmltext/html
.csstext/css
.jsapplication/javascript
.jsonapplication/json
.pngimage/png
.svgimage/svg+xml
.woff2font/woff2

Wrong MIME symptoms

SymptomLikely cause
CSS not appliedServed as text/plain or octet-stream
JS not executedWrong type or missing ; charset=utf-8 for modules
Download dialog for HTMLdefault_type used; missing extension

Fix: ensure file has correct extension, or override:

nginx
location ~* \.mjs$ {
    types { application/javascript mjs; }
}

Inspect response headers:

bash
curl -I http://127.0.0.1/style.css

Look for Content-Type: text/css.

Charset for Text Responses

nginx
http {
    charset utf-8;
    charset_types text/html text/css application/javascript application/json;
}

Code explanation:

  • Adds ; charset=utf-8 to listed text types
  • Prevents mojibake for non-ASCII content when files are UTF-8

Ensure HTML declares UTF-8:

html
<meta charset="UTF-8">

Both file encoding and headers should agree.

expires and Cache-Control

expires directive

nginx
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
}

Code explanation:

  • expires 30d sets Expires header and Cache-Control: max-age=2592000
  • public allows shared caches (CDN) to store the response

add_header for fine control

nginx
location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}
 
location = /index.html {
    add_header Cache-Control "no-cache";
}

Code explanation:

  • Hashed assets (app.8f3a2.js) — long max-age + immutable is safe
  • index.htmlno-cache forces revalidation so users get new asset references after deploy

Tip

Cache Strategy for SPAs

Long cache for /assets/*, short or no cache for index.html—standard Vite/React production pattern.

etag and Conditional Requests

Nginx sends ETag by default for static files. Clients may send:

http
If-None-Match: "abc123"

Server responds 304 Not Modified without body—saves bandwidth.

nginx
etag on;

Usually leave etag on unless a CDN replaces validation logic.

if_modified_since works with Last-Modified similarly (defaults on).

Disable Caching for Development

nginx
location / {
    add_header Cache-Control "no-store, no-cache, must-revalidate";
}

Use only in dev/staging—never on production assets you want browsers to cache.

Layered location Example

Code explanation:

  • /assets/ — fingerprinted files from modern bundlers
  • index.html — always revalidate after deploy
  • charset utf-8 — site-wide text encoding hint

Verify Headers

bash
curl -I http://app.example.com/assets/main.css
curl -I http://app.example.com/index.html

Compare Cache-Control and Content-Type between asset and HTML.

FAQ

add_header in nested locations?

Headers from inner location may replace parent add_header for that response—test with curl -I.

Must I set MIME for every extension?

No—mime.types covers common web types. Add overrides for rare extensions only.

CDN in front of Nginx?

CDN respects origin headers. Set cache policy at Nginx; CDN may override with its own rules.

expires vs Cache-Control only?

expires is convenient; add_header Cache-Control alone is enough if you prefer explicit control.

Why immutable?

Tells browsers the URL content never changes—safe for hash-named files; wrong for URLs that can update in place.

JSON API cache headers?

Dynamic API responses often use Cache-Control: no-store—reverse proxy chapter covers API caching separately.