Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/storyblok-refresh-cache-version.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions examples/magento-storyblok/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions packages/storyblok-ui/Config.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions packages/storyblok-ui/lib/fetch.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<void> {
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()
Expand Down Expand Up @@ -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 ?? [],
Expand All @@ -159,6 +191,7 @@ export async function fetchStories(
export async function fetchAllStories(params: FetchStoriesParams): Promise<StoryblokStory[]> {
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)
Expand Down Expand Up @@ -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)),
)
Expand Down
1 change: 1 addition & 0 deletions packages/storyblok-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading