A continuation of Janady13/crawler-signal-reference / framework-http-content-headers.md. The complete source identity, every recorded occurrence, and linked equations are on its first page.
Source transcript · continued
Download complete source text ↓2,325 lines · 94.8 KB
## 11. ASSET CLASS RECIPES
Each block is a paste ready location stanza for the five content headers. Copy the ones relevant to your build.
### 11.1 HTML (UTF-8 always, no Content-Disposition)
```nginx
location ~* \.html$ {
# Content-Type: text/html; charset=utf-8 (auto from mime.types + charset utf-8)
# Content-Language set at server level: en-US
# Content-Encoding: gzip or br (auto from gzip/brotli on)
# Content-Length: auto for static, chunked when on the fly compression active
# Content-Disposition: none (default inline)
expires 0;
add_header Cache-Control "public, max-age=0, must-revalidate" always;
}
```
### 11.2 CSS
```nginx
location ~* \.css$ {
# Content-Type: text/css; charset=utf-8 (auto)
# Content-Encoding: gzip/br/zstd (auto, large savings)
# Content-Length: auto for precompressed, chunked otherwise
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
```
### 11.3 JavaScript
```nginx
location ~* \.(js|mjs)$ {
# Content-Type: application/javascript; charset=utf-8 (auto)
# Content-Encoding: gzip/br/zstd (auto)
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
```
### 11.4 JSON
```nginx
location ~* \.json$ {
# Content-Type: application/json; charset=utf-8 (auto)
# Content-Encoding: gzip/br/zstd (auto)
expires 1h;
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
}
# For schema and structured data
location ~* \.jsonld$ {
types { application/ld+json jsonld; }
expires 1h;
add_header Cache-Control "public, max-age=3600" always;
}
```
### 11.5 XML (sitemaps, feeds)
```nginx
location ~* \.xml$ {
# Content-Type: application/xml; charset=utf-8 (auto)
# Content-Encoding: gzip/br/zstd (auto)
expires 10m;
add_header Cache-Control "public, max-age=600" always;
}
```
### 11.6 SVG
```nginx
location ~* \.svg$ {
# Content-Type: image/svg+xml; charset=utf-8 (auto)
# Content-Encoding: gzip/br/zstd (auto; SVG is text, compresses ~70 percent)
expires 30d;
add_header Cache-Control "public, max-age=2592000" always;
}
```
### 11.7 Raster images (PNG, JPEG, WebP, AVIF)
```nginx
location ~* \.(png|jpe?g|gif|webp|avif|ico)$ {
# Content-Type: image/png, image/jpeg, etc (auto)
# Content-Encoding: none (already compressed internally)
# Make sure these are NOT in gzip_types or brotli_types
expires 30d;
add_header Cache-Control "public, max-age=2592000" always;
}
```
### 11.8 Fonts (woff2, woff, ttf, otf)
```nginx
location ~* \.(woff2|woff|ttf|otf|eot)$ {
# Content-Type: font/woff2, font/ttf, etc (auto)
# Content-Encoding: NONE for woff2 (already brotli compressed)
# gzip/br OK for ttf, otf (uncompressed formats)
# CORS: required if loaded from a different origin
add_header Access-Control-Allow-Origin "*" always;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
```
### 11.9 PDF (with explicit attachment)
```nginx
location ~* \.pdf$ {
# Content-Type: application/pdf (auto)
# Content-Encoding: NONE (PDF already compressed internally)
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
add_header Content-Disposition "attachment" always;
add_header X-Content-Type-Options "nosniff" always;
}
# Or for inline preview
location /preview/ {
add_header Content-Disposition "inline" always;
}
```
### 11.10 Downloadable archives (zip, tar, gz)
```nginx
location ~* \.(zip|tar|gz|bz2|7z|rar)$ {
# Content-Type: application/zip, application/x-tar, etc (auto)
# Content-Encoding: NONE (these ARE the archive; do not double compress)
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
add_header Content-Disposition "attachment" always;
}
```
### 11.11 User uploaded content (security hardened)
```nginx
location /user-uploads/ {
# Force download, force binary type, prevent XSS via uploaded HTML
add_header Content-Type "application/octet-stream" always;
add_header Content-Disposition "attachment" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'none'" always;
expires 1d;
}
```
### 11.12 robots.txt, ai.txt, llms.txt, security.txt
```nginx
location ~* /(robots\.txt|ai\.txt|llms\.txt|humans\.txt|\.well-known/security\.txt)$ {
# Content-Type: text/plain; charset=utf-8 (auto from .txt)
# Content-Encoding: gzip (auto; saves bandwidth on long robots files)
expires 1h;
add_header Cache-Control "public, max-age=3600" always;
}
```
### 11.13 WebAssembly modules
```nginx
location ~* \.wasm$ {
# Content-Type: application/wasm (must add to types {} if missing from mime.types)
types { application/wasm wasm; }
# Content-Encoding: gzip/br/zstd (auto; .wasm benefits from compression)
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
```
### 11.14 PWA manifest
```nginx
location ~* \.webmanifest$ {
types { application/manifest+json webmanifest; }
# Charset: utf-8 (auto via charset_types)
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
}
```
### 11.15 Generated CSV exports (from FastAPI sidecar)
```nginx
location /export/ {
proxy_pass http://127.0.0.1:9090;
proxy_pass_header Content-Type;
proxy_pass_header Content-Disposition;
# FastAPI must emit:
# Content-Type: text/csv; charset=utf-8
# Content-Disposition: attachment; filename="export-2026-05-24.csv"
}
```
---
## 12. BUBBLES NGINX REFERENCE BLOCK (PASTE READY)
The complete content header stanza, layered with the caching stanza from framework-http-caching-headers.md.
```nginx
# /etc/nginx/nginx.conf (or http context)
http {
# ===== CONTENT TYPE =====
include /etc/nginx/mime.types;
default_type application/octet-stream;
types {
image/avif avif;
font/woff2 woff2;
application/wasm wasm;
application/manifest+json webmanifest manifest;
application/ld+json jsonld;
model/gltf-binary glb;
model/gltf+json gltf;
text/markdown md;
}
# ===== CHARSET =====
charset utf-8;
charset_types
text/html
text/css
text/plain
text/xml
text/csv
application/javascript
application/json
application/xml
application/atom+xml
application/rss+xml
application/ld+json
image/svg+xml;
# ===== COMPRESSION: GZIP =====
gzip on;
gzip_vary on;
gzip_min_length 1100;
gzip_comp_level 6;
gzip_proxied any;
gzip_static on;
gzip_types
text/plain
text/css
text/xml
text/csv
application/javascript
application/json
application/xml
application/atom+xml
application/rss+xml
application/ld+json
application/manifest+json
application/wasm
image/svg+xml
font/ttf
font/otf;
gzip_disable "msie6";
# ===== COMPRESSION: BROTLI (if module installed) =====
brotli on;
brotli_static on;
brotli_comp_level 6;
brotli_min_length 1100;
brotli_types
text/plain
text/css
text/xml
text/csv
application/javascript
application/json
application/xml
application/atom+xml
application/rss+xml
application/ld+json
application/manifest+json
application/wasm
image/svg+xml
font/ttf
font/otf;
# ===== COMPRESSION: ZSTD (if module installed) =====
zstd on;
zstd_static on;
zstd_comp_level 6;
zstd_min_length 1100;
zstd_types
text/plain
text/css
text/xml
application/javascript
application/json
application/xml
application/ld+json
image/svg+xml;
}
# Per server block
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
root /var/www/sites/example.com;
index index.html;
# ===== CONTENT-LANGUAGE =====
# Default language for the site
# NOTE: snippets pattern is required so location blocks do not drop this
set $site_language "en-US";
# ===== SECURITY FAMILY (no MIME sniffing, etc) =====
include snippets/common-security-headers.conf;
add_header Content-Language $site_language always;
# ===== ASSET CLASS BLOCKS =====
# (See Section 11 for all asset class recipes; combine with caching recipes from framework-http-caching-headers.md)
location ~* \.html$ {
include snippets/common-security-headers.conf;
add_header Content-Language $site_language always;
add_header Cache-Control "public, max-age=0, must-revalidate" always;
}
location ~* \.(css|js|mjs)$ {
include snippets/common-security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* \.(woff2|woff|ttf|otf|eot)$ {
include snippets/common-security-headers.conf;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* \.(png|jpe?g|gif|webp|avif|svg|ico)$ {
include snippets/common-security-headers.conf;
add_header Cache-Control "public, max-age=2592000" always;
}
location ~* \.pdf$ {
include snippets/common-security-headers.conf;
add_header Content-Disposition "attachment" always;
add_header Cache-Control "public, max-age=86400" always;
}
location /user-uploads/ {
include snippets/common-security-headers.conf;
add_header Content-Type "application/octet-stream" always;
add_header Content-Disposition "attachment" always;
add_header Content-Security-Policy "default-src 'none'" always;
add_header Cache-Control "public, max-age=86400" always;
}
location /api/ {
include snippets/common-security-headers.conf;
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_header Content-Type;
proxy_pass_header Content-Disposition;
proxy_pass_header Content-Language;
add_header Vary "Accept-Encoding" always;
}
location / {
include snippets/common-security-headers.conf;
add_header Content-Language $site_language always;
add_header Cache-Control "public, max-age=0, must-revalidate" always;
try_files $uri $uri/ $uri.html =404;
}
}
```
The shared security header snippet at `/etc/nginx/snippets/common-security-headers.conf`:
```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
```
After deploying:
```bash
nginx -t && systemctl reload nginx
```
---
## 13. AUDIT CHECKLIST
Run through these 50 items for any production site. Each item is a single curl command or visual inspection. Pass means the listed expected value appears.
### Content-Type
1. [ ] HTML response has `Content-Type: text/html; charset=utf-8`.
2. [ ] CSS response has `Content-Type: text/css; charset=utf-8`.
3. [ ] JavaScript response has `Content-Type: application/javascript; charset=utf-8` (or `text/javascript`).
4. [ ] JSON response has `Content-Type: application/json` (charset optional).
5. [ ] XML sitemap has `Content-Type: application/xml`.
6. [ ] SVG response has `Content-Type: image/svg+xml`.
7. [ ] AVIF image has `Content-Type: image/avif`.
8. [ ] WebP image has `Content-Type: image/webp`.
9. [ ] WOFF2 font has `Content-Type: font/woff2` (NOT `application/octet-stream`).
10. [ ] WASM module has `Content-Type: application/wasm`.
11. [ ] PDF has `Content-Type: application/pdf`.
12. [ ] `X-Content-Type-Options: nosniff` is present on every response.
13. [ ] No response has `Content-Type: application/octet-stream` for known file extensions.
14. [ ] No response has `Content-Type: text/html` for non HTML files.
15. [ ] `default_type` in nginx config is `application/octet-stream` (not `text/html`).
### Content-Language
16. [ ] HTML response has `Content-Language: en-US` (or the correct site language).
17. [ ] Language code uses hyphen, not underscore (`en-US` not `en_US`).
18. [ ] Region code is uppercase two letter ISO 3166-1 (`US` not `usa`).
19. [ ] HTML `<html>` element has matching `lang` attribute.
20. [ ] HTML `<head>` has matching `<meta http-equiv="content-language" content="...">`.
21. [ ] Multi region sites have hreflang annotations in `<head>` for every language version.
22. [ ] hreflang includes `x-default` for the global default.
23. [ ] Localized paths (e.g. `/es/`) emit the correct `Content-Language` for that locale.
### Content-Encoding
24. [ ] HTML response with `Accept-Encoding: gzip` returns `Content-Encoding: gzip`.
25. [ ] HTML response with `Accept-Encoding: br` returns `Content-Encoding: br` (if brotli installed).
26. [ ] HTML response with `Accept-Encoding: zstd` returns `Content-Encoding: zstd` (if zstd installed).
27. [ ] CSS, JS, JSON, XML, SVG all compress.
28. [ ] Already compressed types (JPEG, PNG, WebP, MP4, WOFF2) do NOT have `Content-Encoding`.
29. [ ] Compressed responses have `Vary: Accept-Encoding`.
30. [ ] Request without `Accept-Encoding` returns uncompressed body.
31. [ ] No use of `Content-Encoding: deflate`.
32. [ ] Brotli module loaded via `load_module` if used.
33. [ ] Zstd module loaded via `load_module` if used.
34. [ ] Precompressed `.gz`, `.br`, `.zst` files exist for static assets in production.
### Content-Length
35. [ ] Static file response has `Content-Length` (when not on the fly compressed).
36. [ ] On the fly compressed response uses `Transfer-Encoding: chunked`.
37. [ ] No response has both `Content-Length` and `Transfer-Encoding`.
38. [ ] Declared `Content-Length` matches actual body byte count.
39. [ ] Large precompressed files have `Content-Length` showing the compressed size.
### Content-Disposition
40. [ ] PDF download endpoints have `Content-Disposition: attachment`.
41. [ ] Zip and archive endpoints have `Content-Disposition: attachment`.
42. [ ] User upload serving endpoints have `Content-Disposition: attachment` AND `Content-Type: application/octet-stream`.
43. [ ] Non download responses (HTML pages, CSS, JS, images) do NOT have `Content-Disposition: attachment`.
44. [ ] Filename parameters are wrapped in double quotes.
45. [ ] Non ASCII filenames include both `filename=` (ASCII fallback) and `filename*=` (RFC 5987 UTF-8).
46. [ ] User input is never directly concatenated into Content-Disposition values without sanitization.
### Cross-cutting
47. [ ] `nginx -t` passes without warnings.
48. [ ] `nginx -T` (effective config) shows all expected directives.
49. [ ] Lighthouse "Properly size images" and "Enable text compression" warnings are clear.
50. [ ] Bing Webmaster Tools shows correct language detection (if Bing account is connected).
A site that passes all 50 is ready for production traffic and crawling.
---
## 14. COMMON PITFALLS
Fifteen patterns to recognize and avoid.
**Pitfall 1: Default type set to `text/html`.**
Symptom: unknown file extensions render as broken HTML in the browser.
Why it breaks: `default_type text/html` causes nginx to label every unknown extension as HTML, opening an XSS surface for any user controlled path.
Fix: `default_type application/octet-stream`. Add specific types via `types {}` block for any extension you actually serve.
**Pitfall 2: Charset missing from text Content-Type.**
Symptom: pages with non ASCII characters (curly quotes, em dashes, accented characters) display as mojibake.
Why it breaks: browser falls back to Windows-1252 or another locale default.
Fix: enable `charset utf-8;` and `charset_types` directive (Section 5.5).
**Pitfall 3: Mime type missing for new file extensions.**
Symptom: AVIF images not loading, WOFF2 fonts falling back to system fonts, WASM modules failing to instantiate.
Why it breaks: `/etc/nginx/mime.types` on older Debian releases predates these formats. nginx falls back to `default_type`.
Fix: add types in the http block (Section 5.6).
**Pitfall 4: Compressing already compressed content.**
Symptom: high CPU on nginx, no measurable savings.
Why it breaks: JPEGs, PNGs, MP4s, WOFF2 fonts are already compressed. Gzipping them costs CPU and may even increase size by a few bytes.
Fix: do not list `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `video/*`, `audio/*`, `font/woff2`, `application/zip` in `gzip_types`, `brotli_types`, or `zstd_types`.
**Pitfall 5: Vary: Accept-Encoding missing.**
Symptom: random clients receive binary garbage; cache hit rates from CDNs or corporate proxies break.
Why it breaks: a cache serves a gzipped body to a client that did not request gzip.
Fix: `gzip_vary on;` (Section 7.4 and framework-http-caching-headers.md Section 9).
**Pitfall 6: Content-Disposition: attachment on every response.**
Symptom: every page tries to download instead of render.
Why it breaks: a server wide `add_header Content-Disposition "attachment"` was added somewhere.
Fix: remove the global declaration. Only apply `attachment` to specific download locations.
**Pitfall 7: Unsanitized filename in Content-Disposition.**
Symptom: security scanner reports vulnerability; user uploads can inject CRLF and forge headers.
Why it breaks: unsanitized user input flowing directly into a header value.
Fix: sanitize per Section 9.4. Never trust client supplied filenames.
**Pitfall 8: User uploaded HTML rendered inline.**
Symptom: stored XSS in user uploads.
Why it breaks: file served with `Content-Type: text/html` and no `Content-Disposition: attachment`.
Fix: see Section 9.5. Force `attachment` plus `application/octet-stream` on every user upload endpoint.
**Pitfall 9: Content-Language using underscores or wrong case.**
Symptom: language detection fails in screen readers, translation tools, Bing indexing.
Why it breaks: `en_US` is POSIX locale syntax, not BCP 47. `EN-US` violates the conventional casing.
Fix: `en-US` exactly. Lowercase language, uppercase region, hyphen between.
**Pitfall 10: Setting Content-Language without matching `<html lang>`.**
Symptom: inconsistent language signals; some crawlers ignore the page, some misindex.
Why it breaks: the HTTP header says `es-MX`, the HTML says `<html lang="en">`. Crawlers do not know which to trust.
Fix: align all three layers: HTTP header, meta tag, html attribute.
**Pitfall 11: Both Content-Length and Transfer-Encoding present.**
Symptom: HTTP request smuggling vulnerability scanner alert.
Why it breaks: per the RFC, this combination must not occur. Different intermediaries may interpret framing differently, enabling smuggling.
Fix: find the upstream emitting both. Remove the manual Content-Length and let chunked dominate.
**Pitfall 12: Content-Length wrong for an upstream response.**
Symptom: clients report "incomplete response" or truncated content.
Why it breaks: upstream computed Content-Length before adding (or after removing) some bytes.
Fix: find the upstream code path. Either trust the framework to set Content-Length automatically, or compute it after the body is finalized.
**Pitfall 13: Double compression (serving a .gz file with Content-Encoding: gzip).**
Symptom: browser fails to decompress.
Why it breaks: a `.tar.gz` archive is application data; serving it as `Content-Encoding: gzip` tells the browser to decompress the wire bytes, but then the result is still a `.tar` that the browser cannot show.
Fix: serve `.gz` files (when intended as downloads) with `Content-Type: application/gzip` and NO `Content-Encoding`. Use `gzip_static on` for precompressed CSS/JS, which sets the headers correctly.
**Pitfall 14: Add_header inheritance trap on Content-Type.**
Symptom: nosniff or other security headers missing on certain file types after a location block override.
Why it breaks: any `add_header` in a location wipes out all parent add_headers (see framework-http-caching-headers.md Section 5.5).
Fix: snippet include pattern in every location.
**Pitfall 15: Hardcoded Content-Type overriding nginx mime detection.**
Symptom: PNG returns `Content-Type: text/html`, JS returns `Content-Type: text/plain`, etc.
Why it breaks: someone added `add_header Content-Type "..."` in the wrong scope.
Fix: search the config: `grep -rn "Content-Type" /etc/nginx/`. Remove inappropriate manual declarations. Let mime.types handle it.
---
## 15. DIAGNOSTIC COMMANDS
Reference of every command useful for content header investigation.
### Inspect content headers
```bash
# Full header set
curl -sI https://example.com/page.html
# Content family only
curl -sI https://example.com/page.html | grep -iE "content-(type|language|encoding|length|disposition)"
# As Googlebot
curl -sI -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://example.com/page.html
# As Bingbot (matters for Content-Language)
curl -sI -A "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)" \
https://example.com/page.html
# As ClaudeBot
curl -sI -A "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)" \
https://example.com/page.html
```
### Verify Content-Type
```bash
# Confirm declared type
curl -sI https://example.com/script.js | grep -i content-type
# Verify actual file type
curl -s -o /tmp/asset https://example.com/script.js
file --mime-type /tmp/asset
# Compare to declared
# Detect mojibake risk: dump first 200 bytes
curl -s https://example.com/ | head -c 200 | xxd
# Verify HTML meta charset is in first 1024 bytes
curl -s https://example.com/ | head -c 1024 | grep -i charset
```
### Verify Content-Language
```bash
# Header check
curl -sI https://example.com/ | grep -i content-language
curl -sI https://example.com/es/ | grep -i content-language
# HTML lang attribute
curl -s https://example.com/ | grep -oE '<html[^>]*lang="[^"]*"'
# Meta http-equiv
curl -s https://example.com/ | grep -oE '<meta[^>]*http-equiv="content-language"[^>]*>'
# All hreflang annotations
curl -s https://example.com/ | grep -oE 'hreflang="[^"]*"' | sort -u
```
### Verify Content-Encoding
```bash
# Compression negotiation
for enc in gzip br zstd identity; do
SIZE=$(curl -s -H "Accept-Encoding: $enc" --output - https://example.com/page.html | wc -c)
HDR=$(curl -sI -H "Accept-Encoding: $enc" https://example.com/page.html | grep -i content-encoding | awk '{print $2}' | tr -d '\r')
echo "$enc: declared=$HDR size=$SIZE"
done
# Verify compressed payload actually decompresses to expected content
curl -s --compressed https://example.com/page.html > /tmp/decompressed.html
curl -s -H "Accept-Encoding: identity" https://example.com/page.html > /tmp/raw.html
diff /tmp/decompressed.html /tmp/raw.html && echo "Match"
# Check that brotli module is loaded
nginx -V 2>&1 | grep -o brotli
ls /etc/nginx/modules/ | grep brotli
# Check that zstd module is loaded
nginx -V 2>&1 | grep -o zstd
ls /etc/nginx/modules/ | grep zstd
```
### Verify Content-Length
```bash
# Declared vs actual (identity, no compression)
DECLARED=$(curl -sI -H "Accept-Encoding: identity" https://example.com/style.css | grep -i content-length | awk '{print $2}' | tr -d '\r')
ACTUAL=$(curl -s -H "Accept-Encoding: identity" https://example.com/style.css | wc -c)
echo "Declared: $DECLARED, Actual: $ACTUAL"
[ "$DECLARED" = "$ACTUAL" ] && echo "Match" || echo "MISMATCH"
# Confirm chunked when compressed
curl -sI -H "Accept-Encoding: gzip" https://example.com/page.html | grep -iE "content-length|transfer-encoding"
# Look for the dangerous both-headers combo
curl -sI https://example.com/api/data | awk 'tolower($0) ~ /^(content-length|transfer-encoding):/ {print}'
```
### Verify Content-Disposition
```bash
# Confirm attachment for downloads
curl -sI https://example.com/downloads/report.pdf | grep -i content-disposition
# Confirm inline (or absent) for renderable content
curl -sI https://example.com/page.html | grep -i content-disposition
# Test that user uploads are forced to download
curl -sI https://example.com/user-uploads/file.html | grep -iE "content-(type|disposition)"
# Both must be present and restrictive
# Check filename quoting
curl -sI https://example.com/downloads/report.pdf | grep -i content-disposition
# Filename should be wrapped in double quotes
```
### Server side investigation
```bash
# Check nginx mime.types
cat /etc/nginx/mime.types | grep -i avif
cat /etc/nginx/mime.types | grep -i woff2
cat /etc/nginx/mime.types | grep -i wasm
# Show all add_header in active config
nginx -T 2>/dev/null | grep -i add_header
# Show all compression config in effect
nginx -T 2>/dev/null | grep -iE "gzip|brotli|zstd"
# Show all charset directives
nginx -T 2>/dev/null | grep -i charset
# Confirm modules loaded
nginx -V 2>&1 | tr ' ' '\n' | grep -iE "brotli|zstd"
# Inspect source file encoding
file -bi /var/www/sites/example.com/index.html
# Expected: text/html; charset=utf-8
# Find files that are not UTF-8
find /var/www/sites/example.com -name "*.html" -exec file -bi {} \; | grep -v utf-8
# After config changes
nginx -t && systemctl reload nginx
```
### Browser DevTools quick reference
In Chrome DevTools Network panel, click any request, then:
* **Headers** tab: shows all response headers including the five in this framework.
* **Response** tab: shows the decompressed body. If the body looks like binary garbage here, Content-Encoding is wrong.
* **Size column**: shows wire size (compressed). Hover for "encoded size" and "actual size" (decompressed).
* The "Type" column shows the browser's interpretation based on Content-Type (Document, Stylesheet, Script, XHR, Image, Font, etc).
To force a download when the server returned inline:
* Right click on a link, "Save Link As".
* Or add `?download=true` and have the upstream check for the query and add `Content-Disposition: attachment`.
---
## 16. CROSS-REFERENCES
* [framework-http-caching-headers.md](framework-http-caching-headers.md): Cache-Control, ETag, Last-Modified, Expires, Vary, Age. Vary: Accept-Encoding is shared between this framework and that one; gzip weakens ETags is shared knowledge.
* [UNIVERSAL-RANKING-FRAMEWORK.md](UNIVERSAL-RANKING-FRAMEWORK.md): the master ranking reference. Section 24 (Bubbles Nginx config) inherits content header configuration from here.
* [SEO-BUILD-REFERENCE.md](SEO-BUILD-REFERENCE.md) v2.4: the build playbook. References this framework for any content header decision.
* [framework-security-headers.md](framework-security-headers.md): HSTS, CSP, X-Frame-Options, X-Content-Type-Options. The nosniff header is critical for the Content-Type discipline described here.
* [framework-corewebvitals.md](framework-corewebvitals.md): LCP, INP, CLS targets. Content-Encoding directly affects LCP (smaller payloads transmit faster).
* [framework-hreflang.md](framework-hreflang.md): international SEO. Companion to Content-Language Section 6 here.
* [framework-compression.md](framework-compression.md): deep dive on gzip, brotli, zstd configuration, performance tuning, and migration. This file references it for advanced compression scenarios.
* Google official documentation on Content-Type and indexing: https://developers.google.com/search/docs/crawling-indexing/indexable-content
* Google international and multilingual sites: https://developers.google.com/search/docs/specialty/international/overview
* MDN Content-Type: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type
* MDN Content-Disposition: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition
* RFC 9110 (HTTP Semantics): https://www.rfc-editor.org/rfc/rfc9110
* RFC 6266 (Content-Disposition in HTTP): https://www.rfc-editor.org/rfc/rfc6266
* RFC 5987 (Character set and language encoding for HTTP header parameters): https://www.rfc-editor.org/rfc/rfc5987
---
## APPENDIX A: ONE PAGE QUICK REFERENCE
For the person who just wants the answer.
### Content-Type table
| Asset type | Content-Type |
|---|---|
| HTML | `text/html; charset=utf-8` |
| CSS | `text/css; charset=utf-8` |
| JavaScript | `application/javascript; charset=utf-8` |
| JSON | `application/json` |
| JSON-LD | `application/ld+json` |
| XML sitemap | `application/xml` |
| SVG | `image/svg+xml` |
| PNG | `image/png` |
| JPEG | `image/jpeg` |
| WebP | `image/webp` |
| AVIF | `image/avif` |
| WOFF2 | `font/woff2` |
| PDF | `application/pdf` |
| ZIP | `application/zip` |
| WASM | `application/wasm` |
| Web manifest | `application/manifest+json` |
| Plain text | `text/plain; charset=utf-8` |
| Unknown (safe default) | `application/octet-stream` |
### Content-Language
* Always `en-US` (or correct BCP 47) on every HTML response.
* Match `<html lang="en-US">`.
* Match `<meta http-equiv="content-language" content="en-US">`.
* Add hreflang for multi region sites.
### Content-Encoding
* Compress: HTML, CSS, JS, JSON, XML, SVG, plain text, fonts (ttf/otf only).
* Do NOT compress: JPEG, PNG, WebP, AVIF, MP4, WOFF2 (already compressed).
* Always pair with `Vary: Accept-Encoding`.
* Prefer precompressed (`gzip_static`, `brotli_static`, `zstd_static`) for static assets.
### Content-Length
* Auto for static files.
* Auto for precompressed files (the compressed size).
* Replaced with `Transfer-Encoding: chunked` when on the fly compression is active.
* Never appears with `Transfer-Encoding` together.
### Content-Disposition
* `attachment` for downloads (PDF, ZIP, generated CSVs).
* Omitted (default `inline`) for HTML, CSS, JS, images that should render.
* User uploads: always `attachment` plus `application/octet-stream`.
* Filenames wrapped in double quotes, both `filename` and `filename*` for non ASCII.
Three commands every operator should know:
```bash
# Confirm content type is correct
curl -sI https://example.com/script.js | grep -i content-type
# Confirm compression is working
curl -sI -H "Accept-Encoding: br" https://example.com/page.html | grep -iE "content-encoding|vary"
# Apply changes
nginx -t && systemctl reload nginx
```
And two end to end test invocations:
```bash
# Verify a JSON API returns the right type and decompresses correctly
curl -s --compressed -H "Accept-Encoding: br, gzip" https://example.com/api/data | head -c 200
curl -sI --compressed https://example.com/api/data | grep -iE "content-(type|encoding|length)"
# Verify a PDF download has the right disposition
curl -sI https://example.com/downloads/report.pdf | grep -iE "content-(type|disposition|length)"
```
If both produce expected output, the content header stack is correctly wired.
---
End of framework-http-content-headers.md.