Release January 20, 2026 - #2582
Open
github-actions[bot] wants to merge 297 commits into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
force-pushed
the
changeset-release/from-canary
branch
from
January 23, 2026 15:25
0fc4cb5 to
6629180
Compare
…ing & widen RowRenderer content type - useStoryblokState now narrows to StoryblokPage; global-config uses the upstream hook directly via the typed GlobalConfigStory returned by fetchGlobalConfig - RowRenderer accepts a wider content type so typed StoryblokPage body unions ([k: string]: unknown) are assignable
- fetchGlobalConfig and useStoryblokState throw a descriptive error in dev when content type doesn't match, return null in production - fetchStory/fetchStories log non-404 errors in dev so real failures (network, auth, malformed) are distinguishable from missing stories
to make sure all pages get fetched when passing the 100 story per fetch API limit - Fetch page 1, then derive remaining pages from response.total and fetch them in parallel chunks of 3 so large slug prefixes aren't capped at 100 stories - Add fetchWithRetry: retries 429s up to 3 times honoring the Retry-After header, lets all other errors propagate to the existing logFetchError path
Wrap the single-story fetch in fetchWithRetry so 429s during builds with many slugs are retried with Retry-After, matching fetchStories' behavior.
…pages - RowProduct accepts an optional items prop that bypasses the SKU resolve query, used when the parent already has products to surface - Category page wires a row_product renderer override that passes the first 8 category products, mirroring the Hygraph pattern
…Paths or sitemap etc) and fetchStories for manually pagination pages (blog)
…ckage and wrap non-generic stuff with a thin wrapper function in local lib/storyblok helper file
…ypes
The upstream storyblokEditable expects SbBlokData, but auto-generated
component types widen props to `[k: string]: unknown`, which isn't
assignable. The wrapper accepts the wider shape (plus null/undefined)
and returns {} when the blok is missing.
Removes the need for type casting per call location.
The 'global/config' slug and 'global_config' component name are project-specific, so keeping the function in the package forced those constants into generic code. Export FetchStoryOpts from the package instead so the local wrapper can type its own signature.
Added better default renderers (like in Hygraph) Allow for custom renderers
…refer-resolved-full-slug fix(storyblok-ui): resolve story multilinks to the target's current slug
…omer-account-urls fix(next-config): redirect Magento account URLs to the GraphCommerce routes
Review feedback: remembering individual 404s does not help against the case it
most needs to — crawlers and vulnerability scanners walk an unbounded number of
made-up URLs, and a catch-all route asks the CMS about every one of them. Every
made-up URL is a fresh slug, so a per-slug negative cache never gets a second
chance to help and each one still costs a CDN request.
Replace it with a slug index: one `cdn/links` request per cache-version lists
every published slug in the space, and anything outside that set is answered as
"not found" without a request. Measured against a live space: 300 random URLs
now cost 0 requests instead of 300, while known slugs are unaffected.
- Deliberately unscoped by language. `cdn/links?language=x` returns only the
stories translated into that language (25 of 63 on the space I tested, for a
language the space does not even define), while `cdn/stories/<slug>?language=x`
falls back to the default language and resolves all of them — so a
language-scoped index would report existing pages as missing.
- Folders are skipped. A folder with a start page contributes that start page as
its own entry (`clubkleding/`), which is what makes `fetchStory('clubkleding')`
resolve while a folder without one correctly reports as missing.
- Memoized per cache-version in module scope rather than through the client's
response cache, because that cache is opt-in and off by default.
- Never claims absence when it does not know: a failed index request, or a space
larger than 10 000 stories, falls back to the previous per-slug behaviour.
Draft/preview reads and development skip the index entirely.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Google Places address autocomplete
`requestStoryblokCacheVersionRefresh()` — what a cache-notify webhook calls when a story is published — only flipped a flag in the process that happened to receive the webhook. Nothing told the other replicas anything had changed, so the only thing keeping them fresh was `cacheVersionTtl`: one `cdn/spaces/me` per server per interval, forever, whether or not anything is ever published. At the previous 60 second default that is ~450 000 requests a month across a ten-pod deployment, spent entirely on discovering that nothing happened. The refresh now fans out over a file instead. The webhook writes a millisecond timestamp to `renew-all-pages-query.txt` and every server compares it against the last value it acted on before a published read — a few bytes off the filesystem, throttled to once a second, never an API request. A server that sees a newer signal fetches the current cache-version once and pins it, so the cost of a publish is one request per server that actually serves traffic, instead of one request per server per minute forever. - The file name is not new: it is the one the SSR Apollo client convention already reads (`examples/*/lib/graphql/graphqlSsrClient.ts` drops its per-locale client when the timestamp moves past its own), so one publish can invalidate both caches and the contract stays "this file contains a number". - `storyblok.cacheVersionSignalDir` says where that file lives. It defaults to the `./tmp` of the existing convention, which resolves *inside each container* — a multi-replica deployment has to point it at a shared volume or the signal never leaves the pod that wrote it. `renewSignalPath()` is exported so a project's `graphqlSsrClient` can read the same file without hardcoding the path twice. - `cacheVersionTtl` now defaults to 3600 rather than 60. It is no longer what keeps content fresh, only what bounds staleness where the signal cannot be delivered: serverless, local development, an unwritable directory. It stays applied rather than disabled on purpose — a misconfigured webhook has to degrade to stale content, never to frozen content. - The real cache-version is still fetched rather than derived from the signal's timestamp. Storyblok answers a request carrying an unknown `cv` with a 301 to the canonical one, so a made-up value would work, but it costs every replica a redirect round-trip on its next read and keys the slug index on a version that is about to be replaced. - `node:fs` is loaded through a `webpackIgnore`d dynamic import. This module is re-exported from the package's single barrel entry point and Next.js does not tree-shake in development, so a static import would break every client component importing from the package. Draft, preview and development reads are untouched — they already send a fresh `cv` per request. Verified with ten real OS processes against one shared directory: before, a publish reached 1 of 10 processes and the other nine kept serving the pre-publish version; after, all 10 picked it up within a second, at one `cdn/spaces/me` each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`useClearCurrentCartId()` returned a bare arrow function and `useCartLock()` defined `lock`/`unlock` inline, so all three got a new identity on every render. Consumers that correctly list them in a `useEffect` dependency array therefore re-ran that effect on every render. `useClearCurrentCartId` now uses `useCallback` with `[cache]`, matching its neighbour `useAssignCurrentCartId`. `lock`/`unlock` use `useEventCallback` because they read values that legitimately change (`currentCartId`, the router query state, the Apollo client); that keeps the identity stable without introducing a stale closure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…identities fix(magento-cart): stable identities for cart clear and cart lock hooks
The renew signal was a file in a configurable directory, which only fans out when every replica happens to mount the same volume — the constraint that makes the `./tmp` convention in `examples/*/lib/graphql/graphqlSsrClient.ts` unreliable in the first place. `storyblok.cacheVersionSignalDir` made that a supported option rather than fixing it, so both the config key and the filesystem access are gone. The signal now goes through `globalThis.__incrementalCache`, so it reaches every server that shares a `cacheHandler` — the same requirement a deployment already has to meet to serve consistent ISR, and nothing extra to configure. Validated on a two-pod cluster: pod A writes, pod B reads the value within 9 ms. This leans on two Next.js internals. The global is set in `handleRequestImpl` (`next/dist/server/base-server.js`) on every incoming request, ahead of API routes and ISR regeneration, under the same name and shape in Next 13 through 16; middleware and the edge runtime build their own instance and are not covered. Writing under `kind: 'FETCH'` with `fetchCache: true` is the one path that stores a key verbatim rather than through `normalizePagePath()`, which is what allows a namespaced key. Both are unstable: if either changes, reads and writes fail closed and freshness falls back to `cacheVersionTtl`. The call shape has drifted before — `revalidate` became `cacheControl` between Next 14 and 15 — so it needs re-testing per major. Why the push is worth having at all: `cdn/spaces/me` polls are billed per request against the Storyblok quota, so the polling interval is a cost, not just a staleness knob. `cacheVersionTtl` stays at 3600 as the failsafe. Also drops the justification prose from the comments and rewrites both changesets to describe what changes rather than argue for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…blok Two mechanisms were solving the same problem in different ways: Storyblok pushed a renew signal through the incremental cache, while `graphqlSsrClient()` read `./tmp/renew-all-pages-query.txt`. That path resolves inside each container, so on a multi-replica deployment it silently invalidated nothing — the assumption that made the file approach wrong for Storyblok in the first place. The signal moves to `@graphcommerce/graphql` as `publishRenewSignal()` / `renewSignal()` / `refreshRenewSignal()`. That package is the right home by dependency direction: `storyblok-ui` peer-depends on it, it does not depend on `storyblok-ui`, and all three examples already import from it. The key is neutral (`gc:signal:content-renew`) because the fact being published is "content changed", not anything Storyblok-specific. `graphqlSsrClient()` is synchronous and called as `const client = graphqlSsrClient(context)` throughout `getStaticProps`, so it cannot await the read; making it async would break every consumer in every project. It reads the last known value and schedules the next refresh, which bounds staleness at one poll interval plus one call. That makes the cold start explicit rather than accidental. `renewSignal()` returns `undefined` until the first read completes, which is distinct from "read, and nothing was published" — every pod is in that state right after a deploy, and treating it as 0 would silently skip the first invalidation. Callers create the client without invalidating (a client made now cannot predate a publish) and let the scheduled read decide the next call. Concurrent callers share one in-flight read, so a fresh pod taking simultaneous requests does not open one per `getStaticProps`. Also sets the `cacheVersionTtl` schema default to 3600 to match the repo convention. The `?? 3600` fallback stays: schema defaults are applied by the zod parse but not carried into the generated config values, which is what the runtime imports — `configurableVariantForSimple` has a `= false` default and still generates `undefined`. Verified against a stand-in incremental cache: 25 concurrent refreshes cost one read, a second process picks up a publish, and the client is recreated once per publish and stable in between. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er-api-calls fix(storyblok-ui): cut redundant Storyblok CDN requests
…ignment feat!: GraphCommerce 11 — drop Node 20, require Node 22+
fix(date): parse d/m/Y format and avoid mangling ISO offset strings in toDate
OrderInformationInput.lastname (2.4.8) and .postcode (2.4.9) were emitted with @deprecated, but the GraphQL spec forbids deprecating a required input field — and postcode oscillates (String! in 2.4.7, removed in 2.4.8, re-added nullable in 2.4.9), so mergeTypeDefs unions our marker onto the earlier non-null declaration. Both broke `buildSchema`/mesh ("Required input field X cannot be deprecated"). Regenerated without the marker on those fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Magento 2.4.9 renamed the guestOrder argument type OrderInformationInput -> GuestOrderInformationInput and swapped the postcode lookup field for lastname. The GuestOrder query and guest order form now use the new type/field, so codegen validates against a 2.4.9 backend. schema-249 re-declares guestOrder(input: GuestOrderInformationInput!) (generated) so operations keep validating on older backends via the mesh version shim — mergeTypeDefs is last-wins for a field's arguments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schema-<v> folders are layered onto an OLDER Magento backend, which does not have the added fields and can never produce a value for them. Declaring them non-null made codegen emit required properties — breaking algolia-products' getStoreConfig on a 2.4.7 backend with "Type 'StoreConfig' is missing the following properties ... and 15 more" once schema-248/249 contributed 19 non-null StoreConfig fields — and was semantically wrong: a non-null field that resolves to null propagates the error up and nullifies its parent, so one backfilled Boolean! would blow up the whole storeConfig object at runtime. Brand-new types keep their nullability (only reachable via an already-nullable field), as do redeclared existing fields such as guestOrder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ions Schema defintions for 246,247 and 248
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automated release PR from canary branch