Cache-Control: no-store sounds reasonable until it starts messing with how Google indexes your canonical URLs. Here is what happened and how we fixed it.
For a few months we had a weird issue in Google Search Console where the canonical URL it was reporting for some pages was different from the canonical we had set in the HTML. The page said one thing, Google indexed another. We chased it through structured data validators, checked for duplicate meta tags, verified the head tags were rendering correctly. Everything looked right.
The actual cause was a Cache-Control: no-store header on our nginx responses.
Here is the thing about no-store: it tells every intermediary, including Google's crawlers, that the response must not be cached and must not be stored. When Googlebot sees this, its behavior around canonical reconciliation gets weird. It sometimes decides to override the in-page canonical with what it considers the "correct" version based on other signals, especially if it has crawled a different version of the URL before.
The no-store header had been added by someone (me, a few months earlier) as a quick fix to make sure users always got fresh content after a deploy. Totally reasonable intent, completely wrong tool for the job.
The right approach for a Vite SPA is to use different cache strategies for different file types:
# HTML files: short cache, revalidate
location ~* .html$ {
add_header Cache-Control "public, max-age=300, must-revalidate";
}
# Hashed JS/CSS assets: cache forever
location ~* .(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Images: cache for a week
location ~* .(png|jpg|webp|svg|ico)$ {
add_header Cache-Control "public, max-age=604800";
}Vite outputs content-hashed filenames for JS and CSS (like index-a1b2c3.js), so you can safely set a very long cache duration for those. The hash changes with every build, so browsers and crawlers always get the latest version. Only the HTML itself needs a shorter cache.
After changing this, the Search Console canonical issues resolved themselves over about two weeks as Googlebot re-crawled the affected pages. Nothing immediate, but steady improvement.
One lesson here: no-store is basically never the right answer for a public website. no-cache (which means "revalidate before using cached version") is usually what you want when you need freshness guarantees. no-store says "do not touch the cache at all" which causes problems in ways that are hard to trace.
We also added Cloudflare Cache Rules on top of this to cache the HTML at the edge, which further improved LCP scores. But the nginx headers had to be right first or Cloudflare would just cache the wrong behavior.