(Applies only to pages built with @storesynk/astro, @storesynk/next, @storesynk/react, or
@storesynk/core/server. A plain client-rendered page never does any of this. All three wrappers
share one engine and one adoption protocol, so every entry below applies to each - <StoresynkProduct revalidate>, the data-storesynk-* payloads, i18n, and the client-only metafields are identical.
@storesynk/react is the classic-SSR React sibling (TanStack Start / Remix / Vite SSR); the
package-specific entries below note which wrapper they apply to.)
npm i @storesynk/… fails (E404 / ETARGET / “No matching version found”)
Section titled “npm i @storesynk/… fails (E404 / ETARGET / “No matching version found”)”Almost always a missing @beta dist-tag on an older release line. The @storesynk
packages are published on the public npm registry with @beta as their explicit release
channel; latest currently points at the same beta, so a bare npm i @storesynk/next may
happen to work - never rely on that. Install with the tag: npm i @storesynk/next@beta (same for
@storesynk/astro@beta / @storesynk/react@beta / @storesynk/elements@beta). Transitive deps (@storesynk/core,
@storesynk/elements under a wrapper) resolve normally - dist-tags don’t affect semver
ranges - so one wrapper install is enough. Do not fall back to .tgz files or file:
dependencies, and don’t write a made-up version into package.json. If the tagged install
still 404s, check for a misconfigured registry/proxy (npm config get registry) or a
typo’d package name. Full install notes in build-patterns §4, “Install”.
npm install fails with an ERESOLVE peer conflict (astro / @astrojs/node)
Section titled “npm install fails with an ERESOLVE peer conflict (astro / @astrojs/node)”Version pairing, two variants. (1) @storesynk/astro vs the project’s Astro: the package
peer-supports Astro 5–7 - on an older/newer Astro the install fails with ERESOLVE (npm) or
unmet-peer warnings (pnpm/yarn); check npm ls astro and upgrade the project or the package.
(2) The @astrojs/node adapter vs Astro: adapter majors pair with Astro majors -
@astrojs/node@^9 ↔ Astro 5, ^10 ↔ Astro 6, ^11 ↔ Astro 7. A bare npm i @astrojs/node
installs the newest major and conflicts with any older Astro; install the matching major
explicitly. Do not reach for --legacy-peer-deps - it silences the conflict without
fixing the incompatibility. (@storesynk/next is not affected this way: its peer range is
open-ended, next >=15 / react >=19, and Next 16 Turbopack builds need no configuration.)
My server-rendered prices/titles are wrong or stale
Section titled “My server-rendered prices/titles are wrong or stale”Static astro build freezes product data at build time. Titles/descriptions are fine, but prices and stock go stale between builds. Add revalidate to <StoresynkProduct>/<storesynk-product> so the client background-refreshes after adoption, rebuild on Shopify webhooks, or use an Astro SSR adapter for per-request freshness. (The cart is always client-side/live regardless.) See references/build-patterns.md §4d. If you already run SSR and prices are still stale: in Astro the wrappers fetch live per request on prerender = false routes automatically - stale SSR usually means the route is actually prerendered (check prerender) or an old package version (< 0.1.0-beta.2) whose helpers were module-cached with no TTL. In Next per-request freshness is explicit: dynamic routes must pass the fresh prop (see build-patterns §4 SSR freshness) - without it the route serves the first request’s snapshot for the process lifetime.
I added revalidate but nothing updates (or the user’s selection got reset)
Section titled “I added revalidate but nothing updates (or the user’s selection got reset)”revalidate only does something after a payload was adopted - it’s a no-op on a normal client-fetched product (there’s nothing frozen to refresh). It background-refetches and re-provides context only when the data actually changed, and it preserves the current selection - so if the data is identical you’ll correctly see no change, and switching a variant mid-refresh is not clobbered. If you expected it to also refresh a money/file/list metafield: those aren’t in the payload path (see below).
A product page shows the wrong currency or market pricing (or it flips on load)
Section titled “A product page shows the wrong currency or market pricing (or it flips on load)”Set country/language consistently - on the integration config and they flow to <storesynk-store>, which drives Shopify @inContext. The client and server localize identically (both fill $country/$language), so a mismatch usually means one side wasn’t set. If the SSR HTML shows one market and the page flips to another after hydration, the store attributes disagree with the build-time config.
How do I put my grid class on the rendered host (<StoresynkList> → <storesynk-list>)?
Section titled “How do I put my grid class on the rendered host (<StoresynkList> → <storesynk-list>)?”Every SSR wrapper forwards a class prop to its rendered host tag: Astro - class on <StoresynkStore> / <StoresynkProduct> / <StoresynkList>; Next.js - className on the same three plus <StoresynkStatic>, serialized as class on the host. Put grid/layout classes there; a tag selector in global CSS (storesynk-list { display: grid; … }) works identically. Classes on the tags inside the slot template pass through to the rendered items untouched in both frameworks.
A build fails to render a product / a product route is missing
Section titled “A build fails to render a product / a product route is missing”getProductHandles(max) feeds your getStaticPaths, and getProduct warns loudly in the build log on a missing/invalid handle - check the build output for that warning (a typo’d handle or an unpublished product won’t 404 silently; it logs and the slot renders untouched).
Server-rendered content flashes / repaints on load
Section titled “Server-rendered content flashes / repaints on load”The client is supposed to adopt the server output without repainting, so a visible flash means the payload wasn’t adopted. Check that the <script type="application/json" data-storesynk-product> is a direct child of <storesynk-product> (and data-storesynk-shop a direct child of <storesynk-store>) - the runtime only reads a :scope > payload; nested deeper, it’s ignored and the element falls back to a network fetch (that fetch overwriting the HTML is the flash). Also confirm the payload is valid JSON: a malformed payload is silently caught and the runtime fetches normally.
The client re-fetches even though I server-rendered the product
Section titled “The client re-fetches even though I server-rendered the product”Adoption is keyed on that direct-child JSON payload. If it’s missing, empty, or malformed, <storesynk-product> runs its normal handle/product-id fetch (and <storesynk-store> runs SHOP_QUERY). This isn’t an error - it’s the designed fallback - but you lose the no-network/no-flash benefit. Verify the wrappers emitted the payloads (the built HTML should show both data-storesynk-* scripts).
A metafield is blank in the built HTML (but fine in the browser)
Section titled “A metafield is blank in the built HTML (but fine in the browser)”Most tags now do server-render - displayers, media (incl. Video/ExternalVideo/Model3d), change-option (all three modes), show-thumbnail, selling-plan initial state, and portable metafields (scalar, rich_text_field, dimension/volume/weight, url/link). But three metafield types are deliberately client-only: money (needs the viewer’s locale), file_reference, and any list.* type (needs the client list engine). Those stay blank in the built HTML and fill on adoption - expected, not a bug. If a tag that should server-render is blank even in the browser, it’s an ordinary client-side problem (missing ancestor/template), not an SSR one - debug it as on a plain page.
An SSR page shows the default currency even though the buyer picked another market
Section titled “An SSR page shows the default currency even though the buyer picked another market”This is the per-request market from a cookie flow (astro @storesynk/astro + next @storesynk/next). The client picker writes storesynk_locale=<COUNTRY>/<LANGUAGE> (whenever persist-locale is cookie - the default this version onward; only localStorage/none disable it); an SSR request reads it and renders in that market (foreign-presentment amounts format server-side through the same context-aware formatter as the client), emitting matching country/language store attributes so the client adopts with zero refetch. If the server keeps rendering the default currency, check, in order: (1) persist-locale wasn’t overridden to localStorage/none (those never write a server-readable cookie; the default cookie does); (2) the route is actually dynamic - a prerendered/static page can’t read per-request cookies, so it renders once at build time in the default market (make it dynamic: Astro export const prerender = false / a node-or-similar adapter; Next export const dynamic = 'force-dynamic' and pass getRequestLocale() down). This is by design: static pages ignore the cookie and instead let the client revalidate to the buyer’s market after load - only opt a route into dynamic rendering when you need the market baked into the server HTML (SEO, no post-load flip). In Next, getRequestLocale() is explicit and never automatic (calling cookies() opts the route into dynamic rendering), so a page that forgot to call/pass it stays in the default market - and you must thread it as locale into <StoresynkCollection> / <StoresynkProduct> / <StoresynkLocalePicker> (and their runtime helpers) for the market to reach the server render.
An option control (color/size) renders but doesn’t reflect the selected variant after load
Section titled “An option control (color/size) renders but doesn’t reflect the selected variant after load”The server fills change-option with ss-active/ss-unavailable, role="radiogroup", roving tabindex, and aria-checked, and the client _populate reuses those values in place - so it adopts without a repaint. If selection looks wrong, confirm the payload carried the variant/options (a malformed payload → client refetch → brief mismatch until it resolves).
Option selector wrappers appear briefly then disappear on single-variant products (SSR)
Section titled “Option selector wrappers appear briefly then disappear on single-variant products (SSR)”On a server-rendered PDP using positional group="1/2/3" pickers (the multi-product-route idiom), a single-variant product would show the extra <change-option> wrappers for a beat before hydration hid them. The cause: a single-variant product has no real options (Shopify’s synthetic “Title” option is dropped → options: []), and the old server fill bailed on the empty array before hiding, so the authored groups shipped visible and only the client hid them. Fixed engines ≥ this version: the server now mirrors the client exactly - an empty options array falls through so every authored group’s lookup misses and gets stamped display:none in the SSR markup, so an optionless PDP renders with the groups hidden from the first byte. On older engines the extra groups only self-hide client-side (the flash) - upgrade, or accept the brief flash. (This is SSR-only; on a pure client-rendered page there’s no server markup to flash.)
A nested product card in a server-rendered list shows placeholder text
Section titled “A nested product card in a server-rendered list shows placeholder text”A nested <storesynk-product> (a list/search card template) is deliberately left untouched by the server renderer - it’s a template the client clones and fills per item. Its literal placeholder text in the source is normal; the client replaces it on adoption.
(Next.js) A Storesynk tag inside a client component gets clobbered, disappears, or React warns about it
Section titled “(Next.js) A Storesynk tag inside a client component gets clobbered, disappears, or React warns about it”@storesynk/next renders wrapper children to an HTML string on the server and injects it via dangerouslySetInnerHTML so React treats that subtree as opaque and never reconciles the DOM the engine mutates. So children of <StoresynkProduct> / <StoresynkList> / <StoresynkStatic> must be static host elements - plain Storesynk/HTML tags, no client components, no React state, no event handlers (interactivity comes from the elements themselves: <add-to-cart>, <change-option>, …). A standalone Storesynk tag (e.g. a nav <open-cart> with <show-cart-count>) is fine as plain JSX in a Server Component, but inside a re-rendering client component React will fight the engine’s mutations - wrap it in <StoresynkStatic>. Anything whose structure the engine rewrites (the <storesynk-cart> drawer, whose template child is removed and cloned per line) must always go through <StoresynkStatic>. The runtime loads after hydration, so this is never a hydration-mismatch bug - it is React reconciling DOM it shouldn’t own. Also: a missing SHOPIFY_STORE_DOMAIN/SHOPIFY_STOREFRONT_ACCESS_TOKEN (the canonical env names - STORESYNK_STORE_DOMAIN/STORESYNK_PUBLIC_TOKEN are still accepted as a fallback), with no configureStoresynk override, throws a loud build-time error, not a silent blank page.
(Next.js) A Storesynk tag inside a dialog/drawer portaled to document.body renders nothing
Section titled “(Next.js) A Storesynk tag inside a dialog/drawer portaled to document.body renders nothing”You portaled a modal/drawer to document.body (the usual React portal pattern) and its Storesynk tags - <add-to-cart>, <show-price>, a <storesynk-cart> drawer - render empty, with no console error. The cause: every Storesynk element reads its data from the storeContext/productContext provided by an ancestor <storesynk-store> / <storesynk-product> in the live DOM tree. A portal to document.body moves those tags outside <storesynk-store>, so they receive no context and silently render nothing. The validator can’t see this - it happens at runtime, after React relocates the node, so the source tree still looks correct. Fix: portal into the <storesynk-store> subtree instead of document.body - give the store (or an element inside it) a ref/container and portal there. Keep every Storesynk tag under a live <storesynk-store> ancestor at all times.
(Next.js) Dev logs “encountered runtime data” or “uncached data during prerendering”
Section titled “(Next.js) Dev logs “encountered runtime data” or “uncached data during prerendering””Two distinct Next 15 prerender-guard warnings the buyer-market SSR recipe trips, with two distinct fixes:
- “…encountered runtime data…” / a blocking-prerender error - the route awaits per-request input top-level (
await searchParams,await params,getRequestLocale()/cookies()), so it can’t be statically prerendered. That’s the deliberate trade for a complete, no-JS-readable document (a filtered/market-specific server render) over a streamed shell. Opt into it explicitly withexport const instant = falseon that route; without it, dev logs the error on every such request. Use it on any collection/PDP route that reads the buyer market or the listing’ssearchParamsup front. - “…uncached data during prerendering…” - a region fetches (a Storesynk runtime helper:
getCollectionListing,getLocalization,getShop, …) outside a"use cache"scope. Every fetching region must sit in a"use cache"scope (cacheTag(...)+cacheLife(...)), including a<StoresynkLocalePicker>in a layout/nav (theCachedStoresynkStore/CachedCountryPickerpattern - wrap the picker in its own cached component). Uncached data in a prerender fails the guard on every route.
(Next.js) My server-rendered collection/market content never updates (frozen until redeploy)
Section titled “(Next.js) My server-rendered collection/market content never updates (frozen until redeploy)”A "use cache" region with cacheLife("max") (the recommended setting for the shop grid) revalidates only via the Shopify webhook → revalidateTag(...) path - so in production SHOPIFY_WEBHOOK_SECRET must be set (and the webhook wired to the revalidate route), or the cached content is frozen until the next redeploy. If prices/inventory/collections look stale in prod, verify the webhook secret is present and the products/collections webhooks are firing; in dev, cacheLife TTLs are shorter so you may not notice. Belt-and-braces: add revalidate on <StoresynkCollection> (attribute revalidate on a bare <storesynk-collection>) - after adopting the SSR listing it silently re-runs the query in the background and repaints only on drift (no ss-loading, no dim), so the grid self-heals to fresh data client-side even while the webhook path is stale or mis-wired. It’s a backstop, not a replacement - the webhook is still what keeps the server HTML (SEO, first paint) current.
(React / classic-SSR) A <StoresynkProduct> / <StoresynkStore> renders nothing (blank region)
Section titled “(React / classic-SSR) A <StoresynkProduct> / <StoresynkStore> renders nothing (blank region)”@storesynk/react’s components are synchronous and get their filled markup + adoption payload from the prepared prop - the object a render* helper (renderStore / renderProduct / renderList / renderCollection / renderLocalePicker) returns from your data layer (TanStack createServerFn, Remix/React-Router loader). A component rendered without prepared (or with an empty/undefined one) has nothing to inject and renders blank - like an orphaned tag, with no error. Fix: run the matching render* helper in the loader and thread its result through to the component’s prepared prop; never hand the component raw markup or a bare fetch result (only the helper embeds the payload and stringifies the template). Templates handed to the helper must be static host elements (plain Storesynk/HTML tags - no client components, no React state, no event handlers), exactly like @storesynk/next wrapper children. See build-patterns §4g.
(React / classic-SSR) Dynamic routes leak data between requests, or react-dom/server bloats the client bundle
Section titled “(React / classic-SSR) Dynamic routes leak data between requests, or react-dom/server bloats the client bundle”Two @storesynk/react specifics. (1) Stale/leaked data on dynamic routes: fetches are module-cached per process (build-time economy), so a per-request route that needs fresh data must pass fresh: true plus a shared createRequestScope() (const scope = createRequestScope(); { locale, fresh: true, scope }) to every render*/fetch helper in that request - the scope dedupes within the request without leaking across requests. Omit it and you either re-fetch redundantly or serve a cached snapshot. (2) react-dom/server in the client bundle: only <StoresynkStatic> pulls react-dom/server client-side (it re-stringifies its children at hydration); the prepared-driven components don’t. Reserve <StoresynkStatic> for the no-data markup that needs it (the <storesynk-cart> drawer, client-only templates) - don’t wrap ordinary prepared regions in it. Also note the JSX tag typings ship in @storesynk/elements/jsx - @storesynk/react’s barrels side-effect import it, so you get them just by importing from the package - and they want React 19’s @types/react; on React 18 keep your own JSX.IntrinsicElements declarations for the tags (build-patterns §4, “Plain React/JSX”).
(Cart) A line’s +/−/remove buttons briefly stop responding right after a change
Section titled “(Cart) A line’s +/−/remove buttons briefly stop responding right after a change”By design. When you change a line’s quantity or remove it, the initiating action (increase-line-quantity / decrease-line-quantity / remove-cart-line) stamps ss-loading on that <cart-line> and sets aria-disabled="true" on the line’s controls, ignoring further clicks until the cart mutation settles against the server (the cart repaints from server truth, not optimistically). Both clear on completion. This prevents double-submits and racing mutations on the same line. Give the shopper visible feedback by styling cart-line[ss-loading] (e.g. cart-line[ss-loading] { opacity: .5; pointer-events: none; }). If a line seems permanently stuck loading, the mutation failed/hung - check the network for the shopify:cart:* transport, not the markup.
(Astro) “Blocked request. This host (…) is not allowed” when previewing through a tunnel (ngrok, cloudflared, LAN IP)
Section titled “(Astro) “Blocked request. This host (…) is not allowed” when previewing through a tunnel (ngrok, cloudflared, LAN IP)”Not a Storesynk issue - astro dev and astro preview are Vite servers, and Vite validates the request’s Host header against a localhost-only allowlist (DNS-rebinding protection, default since Vite 5.4.12/6.0.9). A tunnel forwards requests with the tunnel’s hostname, so the preview server rejects them. Fix in the user’s own astro.config.mjs - never from the integration (loosening someone’s dev-server security default is not the package’s call):
export default defineConfig({ vite: { preview: { allowedHosts: ['my-site.ngrok.io'] } }, // and/or server.allowedHosts for `astro dev` // …});This can only ever happen on the two local servers. Deployed output is unaffected: a static build is plain files on a CDN and an SSR adapter runs its own runtime server - no Vite, no allowlist, in production.
Imported from the Storesynk skill v0.1.0 (references/known-issues.md). To change this page, change it there.