[{"data":1,"prerenderedAt":933},["ShallowReactive",2],{"blog-nuxt-caching-isr-commerce-en":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":25,"sources":35,"cover":72,"og":73,"expertise":74,"locales":75,"lang":76,"title":79,"description":80,"coverAlt":81,"metaTitle":82,"takeaways":83,"faq":89,"toc":105,"blocks":133,"others":662},"nuxt-caching-isr-commerce","2026-10-05",11,"web",[9,10,11,12,13],"Nuxt","Caching","ISR","Headless commerce","Cache invalidation",[15,16,17,18,19,20,21,22,23,24],"nuxt route rules caching","nuxt isr","nitro cache swr","cache invalidation purge by cache tag","nuxt isr cloudflare","stale while revalidate commerce","headless commerce caching","vercel isr pricing","cdn cache key vary","personalized prices cached pages",[26,29,32],{"name":27,"url":28},"Content delivery network","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FContent_delivery_network",{"name":30,"url":31},"Cache (computing)","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCache_(computing)",{"name":33,"url":34},"Static site generator","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FStatic_site_generator",[36,39,42,45,48,51,54,57,60,63,66,69],{"title":37,"url":38},"Nuxt docs: rendering modes and route rules (v4)","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fconcepts\u002Frendering",{"title":40,"url":41},"Nuxt blog: Nuxt 4.3 release (ISR and SWR payload extraction)","https:\u002F\u002Fnuxt.com\u002Fblog\u002Fv4-3",{"title":43,"url":44},"Nitro docs: caching","https:\u002F\u002Fnitro.build\u002Fguide\u002Fcache",{"title":46,"url":47},"Cloudflare docs: default cache behavior","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fconcepts\u002Fdefault-cache-behavior\u002F",{"title":49,"url":50},"Cloudflare docs: purge by tags","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fhow-to\u002Fpurge-cache\u002Fpurge-by-tags\u002F",{"title":52,"url":53},"Cloudflare docs: Workers Cache","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Fcache\u002F",{"title":55,"url":56},"Cloudflare docs: cache responses (CF-Cache-Status)","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fconcepts\u002Fcache-responses\u002F",{"title":58,"url":59},"Vercel docs: incremental static regeneration","https:\u002F\u002Fvercel.com\u002Fdocs\u002Fincremental-static-regeneration",{"title":61,"url":62},"Vercel docs: response headers (x-vercel-cache)","https:\u002F\u002Fvercel.com\u002Fdocs\u002Fheaders\u002Fresponse-headers",{"title":64,"url":65},"Shopify docs: Hydrogen caching strategies","https:\u002F\u002Fshopify.dev\u002Fdocs\u002Fstorefronts\u002Fheadless\u002Fhydrogen\u002Fcaching",{"title":67,"url":68},"RFC 5861: HTTP Cache-Control extensions for stale content","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5861",{"title":70,"url":71},"Regulation (EU) 2016\u002F679 (GDPR), EUR-Lex","https:\u002F\u002Feur-lex.europa.eu\u002Feli\u002Freg\u002F2016\u002F679\u002Foj","\u002Fimages\u002Fblog\u002Fnuxt-caching-isr-commerce\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fnuxt-caching-isr-commerce\u002Fog.jpg","vue-nuxt-developer",[76,77,78],"en","de","hu","Caching a headless commerce front end in Nuxt: route rules, ISR and purge","How to cache a headless commerce front end in Nuxt: route rules, Nitro and CDN keys, ISR limits, tag purge and keeping customer prices out of shared caches.","A page slides out of stacked cache trays, with a clock at the side.","Caching headless commerce in Nuxt · Balázs Csorba",[84,85,86,87,88],"Choose the caching mode per route class, not per site: prerender what never changes, use stale-while-revalidate for catalogue data, and keep cart, checkout and account pages uncached.","The name isr does not mean edge caching everywhere; the Nuxt documentation limits the CDN part to platforms that support it, currently Netlify and Vercel, so verify the headers you actually get.","Nitro has no tag purge at the origin layer. Tag responses at write time, purge by tag at the CDN from a signed webhook, and keep a short max age as the fallback.","Cache the shell of a customer-specific page and fetch prices through an uncached, private endpoint. Never let customer data reach a shared cache, and test that with two accounts.","Measure hit ratio from the response headers of each layer, and track the age of the oldest cached entry, because a staleness budget nobody checks is not a budget.",[90,93,96,99,102],{"q":91,"a":92},"What is the difference between swr and isr in Nuxt route rules?","Both add stale-while-revalidate behaviour on the server or a reverse proxy. The Nuxt documentation says isr can also add the response to the CDN cache on platforms that support this, currently Netlify and Vercel. On a plain Node server, the CDN part does not apply, so check the response headers in your deployment.",{"q":94,"a":95},"How do you purge a cached Nuxt page when a product changes?","Nitro does not provide tag purge at the origin, so tag the response at the CDN with a Cache-Tag header, for example one tag per SKU, and purge by tag from a webhook when the product changes. Keep a short max age with stale-while-revalidate as the fallback when a purge call fails.",{"q":97,"a":98},"Can you cache pages that show B2B customer prices?","Yes, if the cached part is the shared shell and the price comes from a separate endpoint that is never shared. Mark that endpoint private or no-store, and keep customer-specific HTML out of every shared cache, including CDN caches and proxies.",{"q":100,"a":101},"Does Vercel ISR cost money?","As of October 2026, Vercel lists ISR as available on all plans. CDN cache reads and writes are free, while reads and writes from durable storage incur regional costs that appear in the ISR section of the usage dashboard.",{"q":103,"a":104},"Does a cache hit on Cloudflare Workers reduce the bill?","Not the request charge. Cloudflare documents that cache hits count as requests but do not consume CPU time, so a higher hit ratio saves CPU, while the request fees stay the same for the same traffic.",[106,109,112,115,118,121,124,127,130],{"id":107,"title":108},"which-mode-per-route-class","Which caching mode fits each route class?",{"id":110,"title":111},"nuxt-route-rules-what-each-mode-does","What do the Nuxt route rules actually do?",{"id":113,"title":114},"how-nitro-caches-on-node-and-at-the-edge","How does Nitro cache a response?",{"id":116,"title":117},"invalidating-on-catalog-change","How do catalogue changes reach the cache?",{"id":119,"title":120},"personalized-b2b-prices-with-cached-pages","How do you cache pages that show customer prices?",{"id":122,"title":123},"stale-while-revalidate-trade-offs","Where should you not use stale-while-revalidate?",{"id":125,"title":126},"measuring-hit-ratio-and-footguns","How do you measure hits, and what usually goes wrong?",{"id":128,"title":129},"checklist-and-rollout","A rollout checklist",{"id":131,"title":132},"sources","Sources",[134,142,145,148,151,226,229,235,236,244,258,264,266,267,297,325,367,406,407,422,424,427,431,438,439,455,458,468,471,472,483,492,519,520,539,584,587,588,591,610,622,623],{"type":135,"content":136},"paragraph",[137,141],{"tag":138,"children":139},"strong",[140],"Caching a headless commerce front end in Nuxt"," is mostly a question of which pages may be shared. Catalogue pages, category listings and marketing pages can be cached at the edge for minutes, hours or until the next deploy, because every visitor sees the same bytes. A product page for a logged-in B2B customer cannot be handled that way, because the price, the orderable quantity and the add-to-cart state depend on who is asking. The mechanics are not hard. The mistakes come from applying one cache mode to every route.",{"type":135,"content":143},[144],"The working rule is short: choose a mode per route class, keep personalized data out of anything shared, decide in advance how stale each class may be, and decide how a catalogue change reaches the cache. Nuxt route rules give you the mode per path. Nitro decides how the server caches the response. The CDN decides what is shared and how purges work. Each layer has its own defaults, its own header vocabulary and its own limits, and as of October 2026 they differ between Vercel, Cloudflare and a plain Node server. The sections below walk through those layers, then the purge paths, the personalization boundary and the measurement.",{"type":146,"level":147,"id":107,"text":108},"heading",2,{"type":135,"content":149},[150],"Prerender what never changes, use stale-while-revalidate for catalogue data that tolerates a short delay, and keep cart, checkout and account pages uncached and server-rendered. The table is a starting point. The staleness budgets are business decisions that you should agree per class, not framework defaults.",{"type":152,"head":153,"rows":166},"table",[154,156,158,160,162,164],[155],"Route class",[157],"Mode",[159],"Cached where",[161],"Staleness budget",[163],"Personalization",[165],"Invalidation lever",[167,180,192,204,217],[168,170,172,174,176,178],[169],"Marketing and legal pages",[171],"prerender",[173],"build output, CDN",[175],"until next deploy",[177],"none",[179],"redeploy",[181,183,185,187,189,190],[182],"Category listings",[184],"swr or isr",[186],"server and CDN",[188],"agreed per class",[177],[191],"tag purge on catalogue change",[193,195,197,199,200,202],[194],"Product detail shell",[196],"swr, client price call",[198],"HTML shell only",[188],[201],"none in HTML",[203],"tag purge plus max age",[205,207,209,211,213,215],[206],"Customer prices and stock",[208],"client island, uncached API",[210],"nowhere shared",[212],"none, read per request",[214],"per customer",[216],"not applicable",[218,220,222,223,224,225],[219],"Cart, checkout, account",[221],"ssr, no-store",[210],[177],[214],[216],{"type":135,"content":227},[228],"The customer-specific data path is not a cache mode at all. It stays out of every shared cache by design, and the page structure should make that boundary visible in code. The diagram shows the request path for a cached product page.",{"type":230,"src":231,"alt":232,"wide":233,"caption":234},"image","\u002Fimages\u002Fblog\u002Fnuxt-caching-isr-commerce\u002Fdiagram-6.webp","Request path for a cached B2B product page Four boxes from left to right: the CDN edge serves the cached HTML shell; the client islands hydrate in the browser; the price API is called uncached with a private cache policy; the cart and account pages are server-rendered and never cached. Personalized data never enters the first box.",true,"The request path: cacheable HTML comes from the edge, and anything that depends on the customer goes to an uncached endpoint or to the server.",{"type":146,"level":147,"id":110,"text":111},{"type":135,"content":237},[238,239,243],"Route rules map a path pattern to rendering and caching behaviour. The Nuxt 4 documentation lists redirect, proxy, ssr, cors, headers, swr, isr, prerender and noScripts among the rules, and Nuxt wraps matching routes with the cache handlers that Nitro provides. Nuxt 4.3 added payload extraction for ISR and SWR routes, so the ",{"tag":240,"children":241},"code",[242],"_payload.json"," files for those routes are generated too and can be cached next to the HTML.",{"type":135,"content":245},[246,247,250,251,254,255,257],"The three modes differ in one important way. ",{"tag":240,"children":248},[249],"swr"," adds cache headers and caches the response on the server or a reverse proxy for a TTL, then regenerates it in the background after expiry. ",{"tag":240,"children":252},[253],"isr"," behaves the same way, with one addition: the response can also go into the CDN cache, but the Nuxt documentation limits that to platforms that support it, which it names as Netlify or Vercel. ",{"tag":240,"children":256},[171]," produces the HTML at build time and keeps it until the next deploy.",{"type":135,"content":259},[260,261,263],"The consequence is that ",{"tag":240,"children":262},[253]," is not a portable name for edge caching. On Vercel it is backed by platform ISR. On a Node server, the same rule gives you stale-while-revalidate at the origin layer, and the CDN story has to be built separately. Write the rule with the platform in mind, and check the response headers in the deployed environment rather than trusting the name.",{"type":240,"code":265},"\u002F\u002F nuxt.config.ts (sketch)\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002F': { prerender: true },\n    '\u002Fcategories\u002F**': { swr: 600 },                  \u002F\u002F shared catalogue data, revalidated in the background\n    '\u002Fproducts\u002F**': { swr: 300 },                    \u002F\u002F shell only; prices come from the client island\n    '\u002Fcart\u002F**': { ssr: true, headers: { 'cache-control': 'private, no-store' } },\n    '\u002Faccount\u002F**': { ssr: true, headers: { 'cache-control': 'private, no-store' } },\n  },\n})",{"type":146,"level":147,"id":113,"text":114},{"type":135,"content":268},[269,270,273,274,277,278,281,282,285,286,289,290,292,293,296],"Nitro’s cache is built on ",{"tag":240,"children":271},[272],"ocache"," and takes pluggable storage. Route rules with a cache option are wrapped automatically in ",{"tag":240,"children":275},[276],"defineCachedHandler",", and ",{"tag":240,"children":279},[280],"swr: true"," is a shortcut for ",{"tag":240,"children":283},[284],"cache: { swr: true, maxAge }",". Two defaults matter. ",{"tag":240,"children":287},[288],"maxAge"," defaults to one second, so an entry without an explicit age is nearly worthless, and the guide tells you to set it for anything you want cached. And ",{"tag":240,"children":291},[280]," without a ",{"tag":240,"children":294},[295],"staleMaxAge"," writes no storage TTL. The docs call that the ISR pattern, which means cache growth is bounded by the storage backend rather than by time.",{"type":135,"content":298},[299,300,302,303,306,307,309,310,313,314,309,317,320,321,324],"Nitro also synthesises the cache headers. A ",{"tag":240,"children":301},[288]," of 60 produces ",{"tag":240,"children":304},[305],"max-age=60",". Adding ",{"tag":240,"children":308},[249]," adds ",{"tag":240,"children":311},[312],"s-maxage=60",", and adding ",{"tag":240,"children":315},[316],"staleMaxAge: 600",{"tag":240,"children":318},[319],"stale-while-revalidate=600",". Setting ",{"tag":240,"children":322},[323],"sendCacheControl: false"," keeps the entry on the server without advertising it to browsers and CDNs, which is useful when an origin should cache a response that the shared layers should not store.",{"type":135,"content":326},[327,328,331,332,335,336,331,339,342,343,345,346,349,350,352,353,356,357,360,361,366],"Keys are conservative by default. Query parameters and cookies do not reach the handler and do not vary the key, so ",{"tag":240,"children":329},[330],"\u002Fpage?a=1"," and ",{"tag":240,"children":333},[334],"\u002Fpage?utm=x"," share one entry. ",{"tag":240,"children":337},[338],"allowQuery",{"tag":240,"children":340},[341],"allowCookies"," opt back in. The docs warn that ",{"tag":240,"children":344},[341]," emits ",{"tag":240,"children":347},[348],"Vary: Cookie",", which can collapse hit rates in CDNs and shared proxies, and they suggest moving the choice into the URL through ",{"tag":240,"children":351},[338]," where possible. ",{"tag":240,"children":354},[355],"set-cookie"," is removed before caching, so a session cookie cannot leak to another user through the cache, and a response whose own ",{"tag":240,"children":358},[359],"Vary"," names a header outside the key is returned but not stored. Locale is a key dimension too: each language variant needs its own entry, and the hreflang links must point only at variants that can actually be served, as covered in ",{"tag":362,"to":363,"children":364},"link","\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang",[365],"multilingual Nuxt SEO with hreflang",".",{"type":135,"content":368},[369,370,373,374,377,378,381,382,331,384,387,388,331,391,394,395,331,398,401,402,405],"The origin cache is only the first layer, and CDN defaults are not about HTML. Cloudflare’s zone cache does not cache HTML or JSON by default; it caches by file extension, and when a response carries no ",{"tag":240,"children":371},[372],"Cache-Control"," or ",{"tag":240,"children":375},[376],"Expires"," header it applies default TTLs by status code. Vercel ISR is available on all plans and keeps cached content until it is revalidated or has gone unaccessed for 31 days. Cloudflare Workers Cache is a newer option owned by the Worker. Its key includes the entrypoint, the path and query string, the Worker version and ",{"tag":240,"children":379},[380],"ctx.props",", but not the host or the HTTP method. It is driven entirely by ",{"tag":240,"children":383},[372],{"tag":240,"children":385},[386],"CDN-Cache-Control"," headers, bypasses automatically on ",{"tag":240,"children":389},[390],"Set-Cookie",{"tag":240,"children":392},[393],"Authorization",", and stores only GET and HEAD. Variants are stored per exact header value, so ",{"tag":240,"children":396},[397],"Accept-Encoding: gzip, br",{"tag":240,"children":399},[400],"br, gzip"," are separate entries, and ",{"tag":240,"children":403},[404],"Vary: *"," disables caching.",{"type":146,"level":147,"id":116,"text":117},{"type":135,"content":408},[409,410,413,414,417,418,421],"Tag responses at write time, purge by tag from a webhook, and keep a short max age as the fallback. Nitro has no tag-based purge at the origin layer, so tags live at the CDN. On Cloudflare, a response with a ",{"tag":240,"children":411},[412],"Cache-Tag"," header can be purged by tag from the dashboard or the API. Tags are case-insensitive and cannot contain spaces. The aggregate header is limited to 16 KB, roughly a thousand unique tags, and an API tag may be up to 1,024 characters. A dashboard purge takes up to 100 tags, and the API limits each request to 100 operations, or 500 on Enterprise. After a tag purge, the next request returns ",{"tag":240,"children":415},[416],"CF-Cache-Status: MISS",", or ",{"tag":240,"children":419},[420],"EXPIRED"," behind Tiered Cache, and the page refills from the origin.",{"type":240,"code":423},"\u002F\u002F Purge the CDN cache for every product whose PIM record changed in this batch\nconst res = await fetch(`https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F${zoneId}\u002Fpurge_cache`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application\u002Fjson' },\n  body: JSON.stringify({ tags: changedSkus.map((sku) => `sku:${sku}`) }), \u002F\u002F split into chunks of 100\n})\nif (!res.ok) throw new Error(`purge failed: ${res.status}`)  \u002F\u002F fall back to the max age, then alert",{"type":135,"content":425},[426],"The webhook needs the same care as any write path: verify the signature, deduplicate events, and coalesce bursts, because a bulk import can fire thousands of changes in a minute. Purge calls are a rate-limit risk in their own right. Workers Caching purges always use the Free-tier purge rate limits, so a Worker-owned cache that relies on frequent purging should batch them.",{"type":230,"src":428,"alt":429,"wide":233,"caption":430},"\u002Fimages\u002Fblog\u002Fnuxt-caching-isr-commerce\u002Fdiagram-21.webp","Purge propagation after a catalogue change Five steps from left to right: a PIM update triggers a signed webhook; the webhook purges the matching cache tag; the next request for that product is a miss and refills the cache. Without a purge, the stale-while-revalidate window serves the previous copy until the background refresh completes.","Purge propagation: a catalogue change removes the tagged entry, the next request refills it, and an untagged entry is served stale until the background refresh finishes.",{"type":135,"content":432},[433,434,437],"Tags are the precise lever, but the cheap alternative is a short max age with stale-while-revalidate. RFC 5861 defines ",{"tag":240,"children":435},[436],"stale-while-revalidate"," as an extension that lets a cache serve a stale response for a set number of seconds while it revalidates in the background. Vercel ISR follows that behaviour, purges HTML and data payloads atomically across all CDN regions, and keeps serving stale content with a 30-second TTL after a failed revalidation. The price is that a customer can briefly see the previous value, so the window length is a business decision, not a technical one.",{"type":146,"level":147,"id":119,"text":120},{"type":135,"content":440},[441,442,445,446,449,450,454],"Cache the shell, not the price. The HTML for a product page is the same for everyone, so it can be shared. The price, the contract discount and the add-to-cart state come from a client island that calls an endpoint which is never shared, such as ",{"tag":240,"children":443},[444],"GET \u002Fapi\u002Fprice?sku=…&customer=…"," with ",{"tag":240,"children":447},[448],"Cache-Control: private",". A configured product adds another layer, since the options change the price and the orderable state; ",{"tag":362,"to":451,"children":452},"\u002Fblog\u002Fheadless-product-configurator-b2b",[453],"headless product configurators for B2B"," cover that case, and the same boundary applies to it.",{"type":135,"content":456},[457],"When the price API is down, the page needs a defined fallback, and the order of preference matters. Show the list price with a sign-in prompt for the contract price, never a customer price cached from someone else. Do not serve a customer-specific fragment from a shared cache as a fallback. Disable add-to-cart when stock cannot be confirmed, and log the degraded state so it shows up in the hit and error metrics.",{"type":459,"variant":460,"title":461,"body":462},"callout","warn","A public header on customer data is a leak",[463],[464,465,467],"Shopify’s Hydrogen documentation warns that a public ",{"tag":240,"children":466},[372]," header, or no header at all, on a route that returns customer-specific data lets shared caches store the rendered page and serve it to other users, exposing names, emails and order history. The documented fix is to disable caching at both the sub-request level and the full-page level for customer queries.",{"type":135,"content":469},[470],"This is also a compliance matter in the EU. A shared cache that serves one customer’s page to another is a disclosure of personal data, and the GDPR addresses that through the integrity and confidentiality principle in Article 5(1)(f) and the security of processing in Article 32. Run a leak test with two test accounts, one of them on a different price list, against the deployed environment before launch and after every change to the route rules. In more than ten years of Vue, Nuxt, TypeScript, Node and PHP work, mostly on B2B e-commerce and PIM platforms such as Spryker and Pimcore, this is the question that comes up most often: which routes may be cached when every price depends on the logged-in customer.",{"type":146,"level":147,"id":122,"text":123},{"type":135,"content":473},[474,475,478,479,482],"Never for checkout gates, for availability at the moment of purchase, or for any value that is binding at read time. Stale-while-revalidate is safe for data where a short wrong value is acceptable and corrects itself, such as category listings, descriptions and images. Commerce teams already use these presets, and Hydrogen’s documented caching strategies are a useful reference: a short strategy of 10 seconds, a long strategy of one hour fresh plus a stale window that makes one day in total, and ",{"tag":240,"children":476},[477],"no-store"," for data that must never be reused. The default sub-request strategy is ",{"tag":240,"children":480},[481],"max-age=1"," with a stale window of 86,399 seconds.",{"type":135,"content":484},[485,486,331,488,491],"RFC 5861 defines the mechanism, not the policy. It was published in 2010 and introduced ",{"tag":240,"children":487},[436],{"tag":240,"children":489},[490],"stale-if-error"," as cache extensions, and whether a given cache honours them is up to that cache. The stale window is therefore a promise you make to customers, and it should be written next to the route rule that enforces it. Use this rough split when you draw the lines:",{"type":493,"ordered":494,"items":495},"list",false,[496,504,509,514],[497,500,501,503],{"tag":138,"children":498},[499],"Checkout and cart:"," ",{"tag":240,"children":502},[477],", always.",[505,508],{"tag":138,"children":506},[507],"Stock at purchase time:"," read live, or accept a checked failure when the order is created.",[510,513],{"tag":138,"children":511},[512],"Search with feature flags:"," key the flag into the URL, or do not cache the result.",[515,518],{"tag":138,"children":516},[517],"Catalogue data and content:"," stale-while-revalidate with a written budget.",{"type":146,"level":147,"id":125,"text":126},{"type":135,"content":521},[522,523,526,527,530,531,534,535,538],"Measure from the headers, not from dashboards you have not set up yet. Vercel returns ",{"tag":240,"children":524},[525],"x-vercel-cache"," with values such as HIT, MISS, STALE, PRERENDER, REVALIDATED and BYPASS, and its documentation notes that dynamic routes and runtime-cache fetches often show MISS even when they are served from the runtime cache. Cloudflare returns ",{"tag":240,"children":528},[529],"CF-Cache-Status"," with HIT, MISS, BYPASS, DYNAMIC, EXPIRED, REVALIDATED, UPDATING and STALE, and sets ",{"tag":240,"children":532},[533],"Age"," on HIT, STALE and UPDATING. Nitro emits its own ",{"tag":240,"children":536},[537],"x-cache"," header with HIT, STALE, REVALIDATED and MISS. The same page can therefore report two statuses, one from the origin layer and one from the CDN, and a dashboard that reads only one of them will mislead you.",{"type":493,"ordered":494,"items":540},[541,549,561,566,574,579],[542,545,546,548],{"tag":138,"children":543},[544],"Caching authenticated responses."," Any response that depends on a cookie or an ",{"tag":240,"children":547},[393]," header needs an explicit private or no-store policy. Workers Cache bypasses on those headers, but the rule that matters is the one you write for every other cache layer.",[550,553,554,556,557,560],{"tag":138,"children":551},[552],"Vary creep."," Adding ",{"tag":240,"children":555},[348]," or a broad ",{"tag":240,"children":558},[559],"Accept"," header splits entries until the hit ratio collapses.",[562,565],{"tag":138,"children":563},[564],"Poisoning through uncovered inputs."," Anything that changes the response without changing the key, such as a rewrite or override header, either goes into the key or gets stripped before it reaches the cache.",[567,570,571,573],{"tag":138,"children":568},[569],"Query strings and cookies missing from the key."," Nitro leaves both out by default, so a route that depends on a query parameter needs ",{"tag":240,"children":572},[338],", or two URLs will share one entry.",[575,578],{"tag":138,"children":576},[577],"Hits that still cost money."," On Workers, a cache hit saves CPU time but still counts as a request, so the request bill does not move with the hit ratio.",[580,583],{"tag":138,"children":581},[582],"Purge storms."," Purge calls are rate limited, and Workers Caching purges use the Free-tier limits, so a bulk import needs batching.",{"type":135,"content":585},[586],"The last footgun bites teams after a successful launch: stale content that nobody checks. Track the age of the oldest cached catalogue entry as well as the hit ratio. A staleness budget only helps if something reports when it is exceeded. The same applies to generated content. I run LLM features on my own Nuxt site and translated it with an LLM pipeline, so once generated text lands in a cached page, a stale translation is a correctness bug, not a cosmetic one.",{"type":146,"level":147,"id":128,"text":129},{"type":135,"content":589},[590],"The order matters. A purge path is useless if the responses carry no tags, and a hit ratio is misleading while the personalization boundary is still unclear. Work through the steps in sequence, and treat each one as a gate for the next.",{"type":493,"ordered":233,"items":592},[593,595,600,602,604,606,608],[594],"Classify every route into one of the classes in the table, and write the staleness budget next to it.",[596,597,599],"Set the route rule for each class, and make the platform behaviour of ",{"tag":240,"children":598},[253]," explicit.",[601],"Add tags to every catalogue response at write time, such as one per SKU and one per category.",[603],"Connect the PIM or ERP publish event to a signed webhook that batches purges by tag.",[605],"Expose the hit ratio per layer and the age of the oldest cached entry, using the headers described above.",[607],"Load-test one purge during peak traffic, and record how long the next requests take to refill the cache.",[609],"Run a leak test with two accounts, one on a different price list, against the deployed environment.",{"type":135,"content":611},[612,613,617,618,366],"If you are building or repairing a commerce front end on Nuxt and want the caching boundary to hold under load, see my ",{"tag":362,"to":614,"children":615},"\u002Fexpertise\u002Fvue-nuxt-developer",[616],"Vue and Nuxt work",". Rendering speed is a separate question with its own metrics, covered in ",{"tag":362,"to":619,"children":620},"\u002Fblog\u002Fnuxt-core-web-vitals-performance",[621],"Nuxt Core Web Vitals",{"type":146,"level":147,"id":131,"text":132},{"type":493,"ordered":233,"items":624},[625,629,632,635,638,641,644,647,650,653,656,659],[626],{"tag":627,"href":38,"children":628},"a",[37],[630],{"tag":627,"href":41,"children":631},[40],[633],{"tag":627,"href":44,"children":634},[43],[636],{"tag":627,"href":47,"children":637},[46],[639],{"tag":627,"href":50,"children":640},[49],[642],{"tag":627,"href":53,"children":643},[52],[645],{"tag":627,"href":56,"children":646},[55],[648],{"tag":627,"href":59,"children":649},[58],[651],{"tag":627,"href":62,"children":652},[61],[654],{"tag":627,"href":65,"children":655},[64],[657],{"tag":627,"href":68,"children":658},[67],[660],{"tag":627,"href":71,"children":661},[70],[663,737,793,865],{"slug":664,"published":665,"minutes":666,"category":7,"tags":667,"keywords":672,"about":684,"sources":693,"cover":730,"og":731,"expertise":732,"locales":733,"lang":76,"title":734,"description":735,"coverAlt":736},"schema-org-structured-data-b2b-shop","2026-10-17",10,[668,669,670,671,9],"Schema.org","Structured data","B2B e-commerce","Technical SEO",[673,674,675,676,677,678,679,680,681,682,683],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[685,687,690],{"name":668,"url":686},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":688,"url":689},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":691,"url":692},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[694,697,700,703,706,709,712,715,718,721,724,727],{"title":695,"url":696},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":698,"url":699},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":701,"url":702},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":704,"url":705},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":707,"url":708},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":710,"url":711},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":713,"url":714},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":716,"url":717},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":719,"url":720},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":722,"url":723},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":725,"url":726},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":728,"url":729},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg","b2b-ecommerce-developer",[76,77,78],"Schema.org structured data for B2B shops: prices behind logins, units and variants","Product markup for B2B shops: snippet or merchant listing, which price to publish, part numbers, unit pricing, variants, Organization and SSR in Nuxt.","A product rectangle with a framed outline and a small price slot hidden behind a low wall.",{"slug":738,"published":665,"minutes":739,"category":7,"tags":740,"keywords":745,"about":757,"sources":765,"cover":787,"og":788,"expertise":732,"locales":789,"lang":76,"title":790,"description":791,"coverAlt":792},"ai-assistant-b2b-shop-customer-support",9,[670,741,742,743,744],"AI assistant","Customer support","LLM agents","EU AI Act",[746,747,748,749,750,751,752,753,754,755,756],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[758,761,764],{"name":759,"url":760},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":762,"url":763},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":691,"url":692},[766,769,772,775,778,781,784],{"title":767,"url":768},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":770,"url":771},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":773,"url":774},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":776,"url":777},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":779,"url":780},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":782,"url":783},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":785,"url":786},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[76,77,78],"An AI assistant for B2B shop support: tools, permissions and the human handoff","How to build an AI support assistant for a B2B shop: which requests to automate, tools that run in the customer context, handoff, AI Act and metrics.","A tall wall with one narrow gap; an arrow passes through it into a boxed enclosure of three squares, and another leads down to a tray.",{"slug":794,"published":665,"minutes":6,"category":7,"tags":795,"keywords":800,"about":812,"sources":822,"cover":859,"og":860,"expertise":74,"locales":861,"lang":76,"title":862,"description":863,"coverAlt":864},"vue-ai-chat-ui-streaming-accessibility",[796,9,797,798,799],"Vue","Accessibility","Streaming","AI chat",[801,802,803,804,805,806,807,808,809,810,811],"vue ai chat ui","nuxt llm streaming","sse vs fetch streaming","aria-live streaming text","accessible chat interface","abort fetch stream vue","dompurify markdown llm","role log chat","reduced motion chat ui","wcag 2.2 chat","screen reader streaming",[813,816,819],{"name":814,"url":815},"Web accessibility","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWeb_accessibility",{"name":817,"url":818},"Server-sent events","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",{"name":820,"url":821},"Vue.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVue.js",[823,826,829,832,835,838,841,844,847,850,853,856],{"title":824,"url":825},"MDN: Using server-sent events","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events\u002FUsing_server-sent_events",{"title":827,"url":828},"MDN: AbortController","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",{"title":830,"url":831},"MDN: ReadableStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FReadableStream",{"title":833,"url":834},"MDN: TextDecoderStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoderStream",{"title":836,"url":837},"MDN: ARIA log role","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FReference\u002FRoles\u002Flog_role",{"title":839,"url":840},"W3C: Understanding SC 4.1.3 Status Messages","https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG22\u002FUnderstanding\u002Fstatus-messages.html",{"title":842,"url":843},"W3C WAI: What's new in WCAG 2.2","https:\u002F\u002Fwww.w3.org\u002FWAI\u002Fstandards-guidelines\u002Fwcag\u002Fnew-in-22\u002F",{"title":845,"url":846},"MDN: prefers-reduced-motion","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-reduced-motion",{"title":848,"url":849},"DOMPurify README","https:\u002F\u002Fgithub.com\u002Fcure53\u002FDOMPurify",{"title":851,"url":852},"markdown-it README","https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",{"title":854,"url":855},"AI SDK docs: useChat","https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-ui\u002Fchatbot",{"title":857,"url":858},"Vue docs: nextTick","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fgeneral.html#nexttick","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fog.jpg",[76,77,78],"Streaming AI chat in Vue and Nuxt: accessible by design","How to build a Vue or Nuxt AI chat UI: fetch streams, abort and retry, safe markdown rendering, aria-live announcements, keyboard support and reduced motion.","A message card with a careful caret, a dotted line opening into ripples and a check.",{"slug":866,"published":665,"minutes":739,"category":7,"tags":867,"keywords":872,"about":884,"sources":894,"cover":927,"og":928,"expertise":74,"locales":929,"lang":76,"title":930,"description":931,"coverAlt":932},"llm-translation-pipeline-i18n",[868,869,870,796,871],"Translation","i18n","LLM","Localization",[873,874,875,876,877,878,879,880,881,882,883],"llm translation pipeline","icu messageformat placeholders","vue i18n llm translation","translation memory llm","glossary enforcement translation","german informal sie du","hungarian translation quality llm","structured output json translation","machine translation post-editing","locale file validation","cldr plural categories",[885,888,891],{"name":886,"url":887},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",{"name":889,"url":890},"ICU MessageFormat","https:\u002F\u002Funicode-org.github.io\u002Ficu\u002Fuserguide\u002Fformat_parse\u002Fmessages\u002F",{"name":892,"url":893},"Vue I18n","https:\u002F\u002Fvue-i18n.intlify.dev\u002F",[895,897,900,903,906,909,912,915,918,921,924],{"title":896,"url":890},"Unicode ICU User Guide: Formatting Messages",{"title":898,"url":899},"Vue I18n: Message Format Syntax","https:\u002F\u002Fvue-i18n.intlify.dev\u002Fguide\u002Fessentials\u002Fsyntax",{"title":901,"url":902},"Nuxt i18n: Getting started","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fgetting-started",{"title":904,"url":905},"Unicode CLDR: Plural rules","https:\u002F\u002Fcldr.unicode.org\u002Findex\u002Fcldr-spec\u002Fplural-rules",{"title":907,"url":908},"OpenAI: Structured outputs","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",{"title":910,"url":911},"DeepL API: Glossaries","https:\u002F\u002Fdevelopers.deepl.com\u002Fdocs\u002Fapi-reference\u002Fglossaries",{"title":913,"url":914},"W3C: Localization vs. internationalization","https:\u002F\u002Fwww.w3.org\u002FInternational\u002Fquestions\u002Fqa-i18n",{"title":916,"url":917},"GNU gettext manual","https:\u002F\u002Fwww.gnu.org\u002Fsoftware\u002Fgettext\u002Fmanual\u002Fgettext.html",{"title":919,"url":920},"RFC 5646: Tags for Identifying Languages","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5646",{"title":922,"url":923},"Rat für deutsche Rechtschreibung","https:\u002F\u002Fwww.rechtschreibrat.com\u002F",{"title":925,"url":926},"Microsoft Writing Style Guide","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fstyle-guide\u002Fwelcome\u002F","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fog.jpg",[76,77,78],"An LLM translation pipeline for i18n: glossary, placeholders and review","How to translate product and website strings with LLMs without breaking placeholders, plurals or tone: glossary, ICU messages, QA checks and review gates.","Three source cards, one translated card with a glossary ribbon, then a checked clipboard.",1792337027665]