Conversation
WalkthroughThe PR adds explicit caching directives to the static/_headers file for JavaScript, CSS, static assets, and common image formats, establishing time-bound browser and CDN cache policies. A CSP header formatting adjustment is also included with no semantic changes. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@static/_headers`:
- Around line 22-28: The image caching rules currently only include "/*.png",
"/*.webp", and "/*.svg" so common formats like .jpg/.jpeg (and optionally .gif
and .ico) fall back to the short global cache; add equivalent header blocks for
"/*.jpg" and "/*.jpeg" (and optionally "/*.gif" and "/*.ico") with the same
Cache-Control value "public, max-age=86400, stale-while-revalidate=604800" so
favicons and JPEG images get the intended long-lived caching behavior.
- Around line 1-3: The global catch-all header rule `/*` is being merged with
more specific rules (e.g., `/*.js`) causing duplicated Cache-Control values;
update the `static/_headers` rules so specific asset paths override the global
rule by using Cloudflare Pages' override directive: prefix the specific path
rules (like `/*.js`, `/*.css`, etc.) with `! Cache-Control` and then provide the
desired Cache-Control for that path, or remove/limit the `/*` global rule so it
doesn't match asset paths; adjust all affected rules referenced (the `/*` global
rule and per-extension rules such as `/*.js`) to prevent header concatenation by
ensuring only one Cache-Control directive applies per request.
🧹 Nitpick comments (1)
static/_headers (1)
10-10:block-all-mixed-contentCSP directive is deprecated.This directive is redundant since
upgrade-insecure-requestsis already present (which supersedes it). Browsers are dropping support forblock-all-mixed-content. It's harmless today but worth removing to keep the CSP clean.
| # HTML pages - short browser cache, longer CDN cache, stale fallback | ||
| /* | ||
| Cache-Control: public, max-age=60, s-maxage=3600, stale-while-revalidate=86400 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n static/_headersRepository: plastic-labs/blog
Length of output: 1901
🌐 Web query:
Netlify _headers file path specificity precedence rules duplicate headers
💡 Result:
Path matching / specificity (multiple for blocks or multiple path sections in _headers)
- Netlify effectively applies the most specific matching path rule for a request; headers from a broader rule like
/*are not inherited when a more specific rule (for example/foo/*) matches. [1] - If you’re trying to “set sitewide, then tweak/override on one route”, you generally must repeat the header on each route where you want it, because the broader rule won’t carry through to more-specific matches. [1]
Duplicate / repeated headers
1) Duplicates inside the same path block in _headers
- If you list the same header field name multiple times under one path in
_headers, Netlify will concatenate them into a single header value (per RFC 7230 behavior). This is the supported way to express multi-value headers in_headers. [2]
2) Duplicates in netlify.toml
- In
netlify.toml, multi-value headers should be expressed as a multiline string (not repeated keys). [2]
3) Same header key set by Netlify defaults vs your custom config
- Header rules are “cumulative” relative to Netlify’s defaults in the sense that your configured values can override default headers when you set the same header name yourself. [3]
Special case: caching header precedence (if you set multiple cache-control variants)
- If more than one of Netlify’s supported caching headers is present (for example
Netlify-CDN-Cache-Control,CDN-Cache-Control,Cache-Control), Netlify will respect the most specific one for its own CDN behavior (while still forwarding others downstream). [4]
Sources:
[1] Gatsby plugin note describing Netlify _headers behavior (“does not inherit… more specific routes replace”). (gatsbyjs.com)
[2] Netlify Docs: “Multi-value headers” behavior for _headers and netlify.toml. (docs.netlify.com)
[3] Netlify Support: header rules cumulative vs defaults; your values override defaults. (answers.netlify.com)
[4] Netlify Docs: caching “Header precedence” (Netlify-CDN-Cache-Control / CDN-Cache-Control / Cache-Control). (docs.netlify.com)
Citations:
- 1: https://www.gatsbyjs.com/plugins/gatsby-plugin-netlify-headers/?utm_source=openai
- 2: https://docs.netlify.com/manage/routing/headers/
- 3: https://answers.netlify.com/t/setting-response-headers-only-on-documents/6144/10?utm_source=openai
- 4: https://docs.netlify.com/platform/caching/?utm_source=openai
🌐 Web query:
Cloudflare Pages _headers file header precedence merging behavior
💡 Result:
On Cloudflare Pages, _headers rules are merged, not “last rule wins”:
- If a request URL matches multiple
_headersblocks, the response inherits all headers from all matching rules. [1] - If the same header name is set by more than one matching rule, Cloudflare joins the values with a comma (it does not override). [1]
- To remove a header that would otherwise be applied (including one applied by a broader match), use the detach syntax:
! Header-Name. [1] _headersapplies to static asset responses; it does not apply to Pages Functions responses (set headers in the Function instead). [1]- If a request matches both
_redirectsand_headers, redirects are applied first (redirect takes priority over headers). [1]
Source: Cloudflare Pages docs: “Headers”. [1]
References
- Cloudflare Developers — Pages: Headers documentation. (developers.cloudflare.com)
Citations:
🏁 Script executed:
fd -e toml -e json -e yml | grep -E '(netlify|wrangler|vercel|cloudflare)' | head -20Repository: plastic-labs/blog
Length of output: 43
🏁 Script executed:
ls -la | grep -E '(netlify|wrangler|vercel)'Repository: plastic-labs/blog
Length of output: 43
🏁 Script executed:
cat package.json 2>/dev/null | grep -A5 -B5 'scripts\|deploy\|host'Repository: plastic-labs/blog
Length of output: 425
🏁 Script executed:
find . -maxdepth 2 -type f \( -name "netlify.toml" -o -name "wrangler.toml" -o -name "vercel.json" -o -name ".netlify" -o -name "*.toml" \) 2>/dev/null | head -20Repository: plastic-labs/blog
Length of output: 43
🏁 Script executed:
cat README.md 2>/dev/null | head -50Repository: plastic-labs/blog
Length of output: 817
🏁 Script executed:
git remote -v 2>/dev/nullRepository: plastic-labs/blog
Length of output: 183
🏁 Script executed:
find . -type f -path "./.github/workflows/*" | head -10Repository: plastic-labs/blog
Length of output: 210
🏁 Script executed:
cat .github/workflows/* 2>/dev/null | grep -i "netlify\|cloudflare\|pages\|vercel" | head -20Repository: plastic-labs/blog
Length of output: 314
🏁 Script executed:
ls -la | head -20Repository: plastic-labs/blog
Length of output: 1248
Use Cloudflare Pages ! directive to override Cache-Control on specific paths, or avoid the /* global rule.
On Cloudflare Pages, multiple matching path rules are merged. A request for /script.js matches both the global /* rule and the /*.js rule, causing both Cache-Control headers to be concatenated with a comma. This results in a malformed header like Cache-Control: public, max-age=60, ..., public, max-age=300, ..., which breaks caching.
To fix: either prefix specific rules with ! Cache-Control to override the global header (e.g., /*.js becomes ! Cache-Control followed by the desired header), or restructure to avoid the global rule matching cached assets. Verify Cloudflare's header precedence behavior to confirm the approach.
Also applies to: 12-28
🤖 Prompt for AI Agents
In `@static/_headers` around lines 1 - 3, The global catch-all header rule `/*` is
being merged with more specific rules (e.g., `/*.js`) causing duplicated
Cache-Control values; update the `static/_headers` rules so specific asset paths
override the global rule by using Cloudflare Pages' override directive: prefix
the specific path rules (like `/*.js`, `/*.css`, etc.) with `! Cache-Control`
and then provide the desired Cache-Control for that path, or remove/limit the
`/*` global rule so it doesn't match asset paths; adjust all affected rules
referenced (the `/*` global rule and per-extension rules such as `/*.js`) to
prevent header concatenation by ensuring only one Cache-Control directive
applies per request.
| # Images | ||
| /*.png | ||
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | ||
| /*.webp | ||
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | ||
| /*.svg | ||
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 |
There was a problem hiding this comment.
Missing common image formats (.jpg, .jpeg, .gif, .ico).
The image caching rules only cover .png, .webp, and .svg. If the site serves .jpg/.jpeg images (very common in blog content) or .ico (favicon), those will fall back to the 60s global cache. Consider adding rules for at least .jpg and .jpeg.
Suggested addition
/*.svg
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
+/*.jpg
+ Cache-Control: public, max-age=86400, stale-while-revalidate=604800
+/*.jpeg
+ Cache-Control: public, max-age=86400, stale-while-revalidate=604800
+/*.gif
+ Cache-Control: public, max-age=86400, stale-while-revalidate=604800
+/*.ico
+ Cache-Control: public, max-age=86400, stale-while-revalidate=604800📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Images | |
| /*.png | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.webp | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.svg | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| # Images | |
| /*.png | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.webp | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.svg | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.jpg | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.jpeg | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.gif | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 | |
| /*.ico | |
| Cache-Control: public, max-age=86400, stale-while-revalidate=604800 |
🤖 Prompt for AI Agents
In `@static/_headers` around lines 22 - 28, The image caching rules currently only
include "/*.png", "/*.webp", and "/*.svg" so common formats like .jpg/.jpeg (and
optionally .gif and .ico) fall back to the short global cache; add equivalent
header blocks for "/*.jpg" and "/*.jpeg" (and optionally "/*.gif" and "/*.ico")
with the same Cache-Control value "public, max-age=86400,
stale-while-revalidate=604800" so favicons and JPEG images get the intended
long-lived caching behavior.
add caching headers — same strategy as honcho-landing and evals, adapted for quartz (no content-hashed filenames so js/css use 5min browser cache instead of immutable).
CF edge wasn't caching anything (cf-cache-status: DYNAMIC on html and contentIndex.json). s-maxage fixes this so only the first visitor to a given edge pays origin latency.