When working with dynamically generated assets (CSS, JS, images) in Nginx, simply checking file extensions won't work for setting cache headers. The standard approach of matching file patterns fails when PHP generates these resources on-the-fly.
Your initial attempt used:
if ($sent_http_content_type = "text/css") {
expires 7d;
}
This doesn't work because $sent_http_content_type
isn't available during the rewrite phase when expires directives are processed. The header isn't set yet when Nginx evaluates this condition.
Here's the proper way to handle MIME-type-based caching in Nginx 1.4.1:
http {
map $sent_http_content_type $expires {
default off;
"text/css" 7d;
"application/javascript" 7d;
"image/jpeg" 30d;
"image/png" 30d;
"image/webp" 30d;
"font/woff2" 90d;
}
server {
location / {
expires $expires;
# Your other config...
}
}
}
For PHP-generated content, add these headers in your PHP script first:
header('Content-Type: text/css'); // or appropriate MIME type
header('X-Accel-Expires: 604800'); // 7 days in seconds
Then in Nginx:
location ~ \.php$ {
fastcgi_ignore_headers X-Accel-Expires;
expires $expires;
# Your PHP-FPM config...
}
After implementing, verify with curl:
curl -I http://yoursite.com/path/to/asset.css
Look for the Expires
and Cache-Control
headers in the response.
The map
directive is evaluated at runtime but highly optimized in Nginx. For high-traffic sites, consider:
- Using static file patterns where possible
- Implementing a CDN for dynamic assets
- Adding cache-control: public for shared proxies
When dealing with dynamically generated assets (CSS, JS, images) in PHP applications, traditional file extension-based caching approaches fall short. The server needs to examine the actual Content-Type header before deciding cache duration.
The common pitfall is trying to use response headers in rewrite/if conditions. Nginx processes these during request phase, before response headers exist. Here's why your approach didn't work:
# This WON'T work as expected
if ($sent_http_content_type = "text/css") {
expires 7d;
}
Solution 1: Map Block with Known Extensions
For cases where you can map file patterns to MIME types:
map $request_uri $expires {
default off;
~*\.css$ 7d;
~*\.js$ 7d;
~*\.(jpg|jpeg|gif|png|webp)$ 30d;
}
server {
expires $expires;
# other config...
}
Solution 2: Lua Module for Dynamic Inspection
For true MIME-type detection, use ngx_http_lua_module:
location / {
header_filter_by_lua_block {
if ngx.header.content_type == "text/css" then
ngx.header["Expires"] = "7d"
end
}
# other config...
}
Solution 3: Proxy Cache with Header Inspection
For reverse proxy scenarios:
location / {
proxy_pass http://backend;
proxy_ignore_headers Set-Cookie;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 301 1h;
proxy_cache_valid any 1m;
proxy_cache_bypass $http_pragma;
proxy_cache_revalidate on;
# Set expires based on backend response
add_header X-Cache-Status $upstream_cache_status;
expires $upstream_http_x_expires;
}
Remember that content-type based expiration requires:
- Additional processing overhead
- Potential race conditions with upstream responses
- Careful testing with vary headers
Verify with curl:
curl -I http://yoursite.com/style.css | grep -i "expires\|content-type"
Or using nginx -T to test configuration syntax before reloading.