Skip to content

Astro

Server-render Shopify data in Storesynk tags with @storesynk/astro, static or SSR.

@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.

Terminal window
npm install @storesynk/astro@beta
npx @storesynk/astro init

init writes an annotated storesynk.config.ts. Spread it into the integration:

astro.config.mjs
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:

astro.config.mjs
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.

src/pages/products/[handle].astro
---
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. revalidate makes 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.

Both modes produce the same HTML and a live cart. You are only choosing when pages render.

Your situationMode
Catalog fits a build and prices change occasionallyStatic (+ revalidate on PDPs)
Prices/stock must be correct in the HTML itself at all timesSSR for those routes (prerender = false)
Catalog too large or volatile to rebuildSSR
Per-request context: geo pricing, personalization, cookiesSSR
Marketing pages plus a few hot product pagesHybrid: 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.

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.

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 request
export const prerender = false;
---

Precedence: component prop, cookie, integration config. Prerendered pages ignore the cookie and repaint client-side.

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.

  • 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 (fresh overrides in helper calls).
  • astro dev fetches once per process for prerendered pages. Restart to see fresh data.
  • Client-only by design: money/file/list.* metafields, predictive search, and the cart.
  • trackEvents is not an integration option. It is a <StoresynkStore trackEvents> prop. Site-wide analytics is pixels: { enabled: true }.
  • Hardcoded option names on [handle] routes silently hide the picker on products without that option. Use group="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.allowedHosts for dev).
  • npm install ERESOLVE 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.