@storesynk/astro server-renders real Shopify data into the same Storesynk tags you write on a plain HTML page. The client runtime adopts that output without refetching or flashing. Static builds and SSR with any adapter.
Supports Astro 5, 6, and 7. With @astrojs/node, match majors: ^9 for Astro 5, ^10 for Astro 6, ^11 for Astro 7 (Astro 6+ needs Node.js 22.12+).
Install with the @beta tag (required while pre-1.0). This is a build-time integration, not a browser bundle: no CDN script tag on this path.
npm install @storesynk/astro@betanpx @storesynk/astro initinit writes an annotated storesynk.config.ts. Spread it into the integration:
import { defineConfig } from 'astro/config';import storesynk from '@storesynk/astro';import storesynkConfig from './storesynk.config';
export default defineConfig({ integrations: [storesynk(storesynkConfig)],});Or configure inline with the same shape:
storesynk({ domain: 'your-store.myshopify.com', token: 'your-public-storefront-token', // public token only - never an Admin token cart: { openOnAdd: true }, pixels: { enabled: true, server: ['meta', 'ga'] },});import.meta.env is not populated inside astro.config.mjs, so load .env explicitly:
import { loadEnv } from 'vite';const env = loadEnv(process.env.NODE_ENV ?? 'production', process.cwd(), '');// …then: storesynk({ ...storesynkConfig, domain: env.STORESYNK_STORE_DOMAIN, token: env.STORESYNK_PUBLIC_TOKEN })The options are the shared StoresynkConfig, plus injectRuntime (set false to load the elements runtime yourself) and serverCart (below). The integration injects the runtime and carries credentials into the components, so you never hand-write <storesynk-store domain token> or a bundle script.
Write pages with the components
Section titled “Write pages with the components”---import { StoresynkStore, StoresynkProduct, StoresynkSEO } from '@storesynk/astro/components';import { getProductHandles } from '@storesynk/astro/runtime';
export const getStaticPaths = async () => (await getProductHandles()).map((handle) => ({ params: { handle } }));
const { handle } = Astro.params;---<html lang="en"> <head> <StoresynkSEO handle={handle} siteName="My Store" /> </head> <body> <StoresynkStore> <StoresynkProduct handle={handle} revalidate> <h1><show-title></show-title></h1> <div class="price-row"> <show-price></show-price> <show-compare-price></show-compare-price> </div> <show-media main><img alt="" /></show-media> <show-description></show-description>
{/* A [handle] route serves the whole catalog - use positional group=, never a hardcoded name="Color". Extra groups self-hide. */} <change-option group="1"><show-option-label></show-option-label><option-value><show-option-title></show-option-title></option-value></change-option> <change-option group="2"><show-option-label></show-option-label><option-value><show-option-title></show-option-title></option-value></change-option> <change-option group="3"><show-option-label></show-option-label><option-value><show-option-title></show-option-title></option-value></change-option>
<add-to-cart><button type="button">Add to cart</button></add-to-cart> </StoresynkProduct> </StoresynkStore> </body></html>@storesynk/astro/components exports:
<StoresynkStore country? language? trackEvents? class?>:<storesynk-store>with credentials and an embedded shop payload.<StoresynkProduct handle revalidate? class?>: fetches once per handle per build, fills every displayer and control, and embeds the product payload.revalidatemakes the client refetch in the background and repaint only on drift. A missing product leaves the slot untouched and warns in the build log.<StoresynkList collection limit? class?>: a collection grid from the slot’s first-child template, with crawlable links.<StoresynkCollection>: the URL-state-aware filterable listing.<StoresynkLocalePicker>: the Markets<change-country>/<change-language>pickers (select mode).<StoresynkSEO handle title? description? url? siteName?>: in<head>: title, meta description, Open Graph tags, Product JSON-LD.<StoresynkProductJsonLd handle url?>: the JSON-LD alone, as a body-level script.
Every wrapper forwards class to its host tag. @storesynk/astro/runtime exposes the fetches: getProduct, getShop, getCollectionProducts, getCollectionListing, getProductMetafields, getLocalization, getAllProducts, getProductHandles.
Static or SSR? Start static.
Section titled “Static or SSR? Start static.”Both modes produce the same HTML and a live cart. You are only choosing when pages render.
| Your situation | Mode |
|---|---|
| Catalog fits a build and prices change occasionally | Static (+ revalidate on PDPs) |
| Prices/stock must be correct in the HTML itself at all times | SSR for those routes (prerender = false) |
| Catalog too large or volatile to rebuild | SSR |
| Per-request context: geo pricing, personalization, cookies | SSR |
| Marketing pages plus a few hot product pages | Hybrid: static site, prerender = false on hot routes |
Static default: revalidate on PDPs plus a Shopify webhook (products/update, collections/update) pointed at your host’s build hook. Cache-tag invalidation is Next.js-only.
How adoption works
Section titled “How adoption works”Each wrapper embeds its data as a direct-child JSON payload:
<storesynk-product handle="classic-snowboard"> <script type="application/json" data-storesynk-product>{…}</script> <show-title>The Classic Snowboard</show-title> …server-filled markup…</storesynk-product>The runtime hydrates from the payload with no queries and no repaint. The same protocol covers data-storesynk-shop, data-storesynk-listing, and data-storesynk-localization. A missing or malformed payload falls back to a client fetch. See SSR & adoption.
Per-request market from a cookie (SSR)
Section titled “Per-request market from a cookie (SSR)”The Markets pickers write a storesynk_locale=<COUNTRY>/<LANGUAGE> cookie (persistLocale: 'cookie', the default). SSR routes read it and render in that market, so the client adopts with no currency flash.
---// src/pages/live/[handle].astro - rendered per requestexport const prerender = false;---Precedence: component prop, cookie, integration config. Prerendered pages ignore the cookie and repaint client-side.
Server-owned cart
Section titled “Server-owned cart”By default the cart id lives in localStorage. serverCart: true moves it into an HttpOnly shopify_cartId cookie and routes every cart operation through an injected /_storesynk/cart endpoint (prerender = false). The window.Shopify.actions / shopify:cart:* protocol is unchanged.
For custom setups: @storesynk/astro/cart-client, @storesynk/astro/cart-endpoint, and @storesynk/astro/cart-server (with CART_COOKIE). The package also exports createGatesMiddleware for SSR enforcement of the app’s page gates.
Freshness
Section titled “Freshness”- Static builds freeze product data. Use
revalidate, rebuild on webhooks, or move hot routes to SSR. - SSR routes fetch live per request; components on one page share one fetch (
freshoverrides in helper calls). astro devfetches once per process for prerendered pages. Restart to see fresh data.- Client-only by design:
money/file/list.*metafields, predictive search, and the cart.
Gotchas
Section titled “Gotchas”trackEventsis not an integration option. It is a<StoresynkStore trackEvents>prop. Site-wide analytics ispixels: { enabled: true }.- Hardcoded option names on
[handle]routes silently hide the picker on products without that option. Usegroup="1"/"2"/"3". See options & variants. - Previewing through a tunnel (ngrok, cloudflared) hits Vite’s host allowlist. Add
vite: { preview: { allowedHosts: ['my-site.ngrok.io'] } }(server.allowedHostsfor dev). npm installERESOLVE conflicts are version pairing. Install matching Astro and adapter majors; don’t use--legacy-peer-deps.- A flash on load means a payload wasn’t adopted. Check the
data-storesynk-*script is a direct child and valid JSON. See Troubleshooting: SSR.