From 6eb86fd5833adfac3cd39a99d1841a8b95520ed5 Mon Sep 17 00:00:00 2001 From: Bram van der Holst Date: Mon, 29 Jun 2026 12:26:18 +0200 Subject: [PATCH 1/2] feat(storyblok-ui): refresh pinned Storyblok cache-version on a TTL storyblok-js-client pins the space cache-version (cv) per process on the first published request and never refreshes it (cache.clear defaults to 'manual'), so published content stays frozen at the process-start cv until restart. On long-lived servers (multi-pod Kubernetes) published edits never become visible. Add refreshStoryblokCacheVersion(), which re-fetches cdn/spaces/me at most once per the new storyblok.cacheVersionTtl config (seconds, default 60; 0 refreshes on every read) to advance the pinned cv, and call it before published fetchStory/fetchStories/fetchAllStories reads. Skipped for preview/draft and in dev. Exported so on-demand revalidation can force an immediate refresh. --- .changeset/storyblok-refresh-cache-version.md | 7 ++++ packages/storyblok-ui/Config.graphqls | 12 +++++++ packages/storyblok-ui/lib/fetch.ts | 34 +++++++++++++++++++ packages/storyblok-ui/package.json | 1 + 4 files changed, 54 insertions(+) create mode 100644 .changeset/storyblok-refresh-cache-version.md diff --git a/.changeset/storyblok-refresh-cache-version.md b/.changeset/storyblok-refresh-cache-version.md new file mode 100644 index 0000000000..acb5325a3a --- /dev/null +++ b/.changeset/storyblok-refresh-cache-version.md @@ -0,0 +1,7 @@ +--- +'@graphcommerce/storyblok-ui': patch +--- + +Refresh the pinned Storyblok cache-version (`cv`) on a TTL so published content no longer stays frozen on long-lived servers. + +`storyblok-js-client` pins the space `cv` per process on the first published request and never refreshes it (its `cache.clear` defaults to `'manual'`), so published edits only became visible after a process restart — on a multi-pod deployment this could mean content not updating for a long time. `fetchStory`, `fetchStories` and `fetchAllStories` now call the new `refreshStoryblokCacheVersion()` before published reads, which re-fetches `cdn/spaces/me` at most once per the new `storyblok.cacheVersionTtl` config (seconds, default 60; 0 refreshes on every read) to advance the pinned `cv`. Skipped for preview/draft and in development. The helper is exported so on-demand revalidation (e.g. a cache-notify webhook) can force an immediate refresh. diff --git a/packages/storyblok-ui/Config.graphqls b/packages/storyblok-ui/Config.graphqls index 98c7226848..03f9485110 100644 --- a/packages/storyblok-ui/Config.graphqls +++ b/packages/storyblok-ui/Config.graphqls @@ -22,6 +22,18 @@ input StoryblokConfig { Override only if you maintain your own example/template space. """ sourceSpaceId: String + + """ + How often (in seconds) the server refreshes the Storyblok space cache-version (`cv`). + + `storyblok-js-client` pins the `cv` per process on the first published request and never + refreshes it, which freezes published content until the process restarts (a real problem on + long-lived, multi-pod deployments). GraphCommerce re-fetches the current `cv` at most once per + this interval before published reads, so edits become visible again. Defaults to `60`. Set to + `0` to refresh on every read (always fresh, more requests); raise it to reduce requests at the + cost of higher staleness. Does not apply in development, where a fresh `cv` is sent per request. + """ + cacheVersionTtl: Int } extend input GraphCommerceConfig { diff --git a/packages/storyblok-ui/lib/fetch.ts b/packages/storyblok-ui/lib/fetch.ts index 157eaec7aa..6667cf575e 100644 --- a/packages/storyblok-ui/lib/fetch.ts +++ b/packages/storyblok-ui/lib/fetch.ts @@ -1,4 +1,5 @@ import type { ApolloClient } from '@graphcommerce/graphql' +import { storyblok } from '@graphcommerce/next-config/config' import { storefrontConfig } from '@graphcommerce/next-ui' import { getStoryblokApi, @@ -35,6 +36,36 @@ const STORIES_PER_PAGE = 100 const MAX_PAGE_CONCURRENCY = 3 const MAX_RETRIES = 3 +/** + * `storyblok-js-client` pins the space cache-version (`cv`) per process on the first published + * request and—because `cache.clear` defaults to `'manual'`—never refreshes it. Published content + * therefore stays frozen at the process-start `cv` until the process restarts. On long-lived + * servers (e.g. multi-pod Kubernetes) this means published edits never become visible. + * + * We bound the staleness by periodically re-fetching `cdn/spaces/me`, which returns the current + * `cv` and updates the client's pinned value, so subsequent reads hit the fresh (CDN-cached) + * version. Skipped in dev, where `sbParams` already sends a fresh `cv` on every request. + * + * The interval is configurable via `storyblok.cacheVersionTtl` (seconds, default 60; 0 refreshes + * on every read). + */ +const CV_REFRESH_TTL_MS = (storyblok?.cacheVersionTtl ?? 60) * 1000 +let cvRefreshedAt = 0 + +export async function refreshStoryblokCacheVersion(force = false): Promise { + if (isDev) return + const now = Date.now() + if (!force && now - cvRefreshedAt < CV_REFRESH_TTL_MS) return + // Optimistically mark refreshed so concurrent callers don't stampede cdn/spaces/me. + cvRefreshedAt = now + try { + await getStoryblokApi().get('cdn/spaces/me') + } catch { + // A failed refresh keeps the previous cv; reset so the next call retries. + cvRefreshedAt = 0 + } +} + /** Extracts the language prefix from a locale string (e.g. `en_US` → `en`). */ function langPrefix(locale?: string) { return locale?.split(/[-_]/)[0].toLowerCase() @@ -139,6 +170,7 @@ export async function fetchStories( const perPage = params.per_page ?? STORIES_PER_PAGE const page = params.page ?? 1 try { + if (!params.preview) await refreshStoryblokCacheVersion() const response = await storiesRequest(params, page, perPage) return { stories: response.data?.stories ?? [], @@ -159,6 +191,7 @@ export async function fetchStories( export async function fetchAllStories(params: FetchStoriesParams): Promise { const perPage = params.per_page ?? STORIES_PER_PAGE try { + if (!params.preview) await refreshStoryblokCacheVersion() const first = await storiesRequest(params, 1, perPage) const stories: StoryblokStory[] = first.data?.stories ?? [] const totalPages = Math.ceil((first.total ?? stories.length) / perPage) @@ -187,6 +220,7 @@ export async function fetchStory( apolloClient?: ApolloClient, ): Promise<{ data: { story: StoryblokStory } | null }> { try { + if (!opts?.preview) await refreshStoryblokCacheVersion() const result = await fetchWithRetry(() => getStoryblokApi().get(`cdn/stories/${slug}`, sbParams(opts)), ) diff --git a/packages/storyblok-ui/package.json b/packages/storyblok-ui/package.json index 7563c2f248..c92cf01689 100644 --- a/packages/storyblok-ui/package.json +++ b/packages/storyblok-ui/package.json @@ -16,6 +16,7 @@ "@graphcommerce/graphql": "^10.1.0-canary.26", "@graphcommerce/image": "^10.1.0-canary.26", "@graphcommerce/magento-product": "^10.1.0-canary.26", + "@graphcommerce/next-config": "^10.1.0-canary.26", "@graphcommerce/next-ui": "^10.1.0-canary.26", "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.26", "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.26", From 780902ce9659ff9f896dc418ba21b29cf3fef019 Mon Sep 17 00:00:00 2001 From: Bram van der Holst Date: Mon, 29 Jun 2026 15:11:20 +0200 Subject: [PATCH 2/2] fix(magento-storyblok): add @floating-ui/dom for @tiptap/suggestion peer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @tiptap/suggestion (pulled in transitively via @storyblok/react → @storyblok/richtext) declares @floating-ui/dom as a peerDependency, which Yarn does not auto-install. Nothing provided it, so production builds failed with "Module not found: Can't resolve '@floating-ui/dom'". Declare it directly in the example so it ships with the seed project. Co-Authored-By: Claude Opus 4.8 --- examples/magento-storyblok/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/magento-storyblok/package.json b/examples/magento-storyblok/package.json index eb10ca197f..b0e13f9708 100644 --- a/examples/magento-storyblok/package.json +++ b/examples/magento-storyblok/package.json @@ -36,6 +36,7 @@ "@emotion/react": "^11.14.0", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.14.1", + "@floating-ui/dom": "^1.7.6", "@graphcommerce/cli": "10.1.0-canary.26", "@graphcommerce/ecommerce-ui": "10.1.0-canary.26", "@graphcommerce/framer-next-pages": "10.1.0-canary.26",