npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@storesynk/next

v0.1.0-beta.16

Published

Next.js (App Router) bindings for Storesynk — server-rendered Shopify data inside Storesynk tags, adopted flash-free by the client runtime.

Readme

@storesynk/next

Next.js (App Router) bindings for Storesynk: your product pages are server-rendered with real Shopify data — titles, prices, images, and descriptions are in the built HTML (SEO- and no-JS-complete) — and the client runtime adopts that output without re-fetching or flashing.

Works with static generation (generateStaticParams) and per-request SSR. The markup you write inside the wrappers is exactly the same Storesynk tags you'd use on a plain HTML page.

Requires Next 15+ / React 19+ and the App Router. Next 16 is supported, including its default Turbopack builds — the published package ships compiled output, which Turbopack resolves without configuration.

Building with webpack (Next 15's default, or next build --webpack)? Add one externals rule. @shopify/hydrogen, which @storesynk/core builds on, lazily import()s Shopify's standard-events module straight from cdn.shopify.com; Turbopack treats an https: specifier as an external, but webpack rejects the scheme unless told to leave it as a native import:

// next.config.ts
webpack: (config) => {
  config.externals = [
    ({ request }, callback) =>
      request?.startsWith('https://') ? callback(null, `import ${request}`) : callback(),
    ...(Array.isArray(config.externals) ? config.externals : [config.externals].filter(Boolean)),
  ];
  return config;
},

Install

npm install @storesynk/next

Setup

Scaffold the config file with the init command (explicit — not a postinstall, so it works under pnpm/npm/yarn and CI):

npx @storesynk/next init   # writes an annotated storesynk.config.ts and prints the next steps

Then set the two store credentials as environment variables (they're read server-side; the token is the browser-safe public one and ends up in the HTML anyway):

# .env.local
SHOPIFY_STORE_DOMAIN="your-shop.myshopify.com"
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-public-storefront-token"  # public token only — never an Admin token
# (STORESYNK_STORE_DOMAIN / STORESYNK_PUBLIC_TOKEN are accepted as explicit overrides)

Only these two live in env. Everything else — locale (country/language), customer accounts, and the pixels / cart behavior groups — goes in the storesynk.config.ts the init command created (the Next counterpart to the storesynk({...}) block in an Astro project's astro.config):

// storesynk.config.ts
import { configureStoresynk } from '@storesynk/next';

configureStoresynk({
  // domain/token are inherited from env — this file is just behavior:
  country: 'DE',
  // Optional: print every price in this Shopify-style template instead of the
  // shop's admin setting (which checkout and emails use).
  moneyFormat: '{{amount_with_comma_separator}} €',
  cart: { openOnAdd: true, closeOnOutside: true },
  pixels: { enabled: true, server: ['meta', 'ga'] },
});

Import it once, for its side effect, at the very top of app/layout.tsx — above your other imports — so it runs at module scope before any Storesynk component renders:

// app/layout.tsx
import '../storesynk.config';
// …the rest of your layout

(Put credentials in the config call instead of env by passing domain/token explicitly — they override env when present. The scaffolded file lists every option, commented, with its default.)

<StoresynkStore> automatically loads the @storesynk/elements runtime in the browser (client-only, post-hydration). Pass loadRuntime={false} and place <StoresynkRuntime /> (from @storesynk/next/client) yourself if you need control over where it mounts.

Usage

// app/products/[handle]/page.tsx
import { StoresynkStore, StoresynkProduct, StoresynkProductJsonLd, productMetadata } from '@storesynk/next';
import { getProductHandles } from '@storesynk/next/runtime';
import type { Metadata } from 'next';

export async function generateStaticParams() {
  return (await getProductHandles()).map((handle) => ({ handle }));
}

export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }): Promise<Metadata> {
  const { handle } = await params;
  return productMetadata(decodeURIComponent(handle), { siteName: 'Acme' });
}

export default async function Page({ params }: { params: Promise<{ handle: string }> }) {
  const { handle } = await params;
  const decoded = decodeURIComponent(handle);
  return (
    <StoresynkStore>
      <StoresynkProductJsonLd handle={decoded} />
      <StoresynkProduct handle={decoded}>
        <show-title>Product Title</show-title>
        <div className="price-row">
          <show-price></show-price>
          <show-compare-price></show-compare-price>
        </div>
        <show-media main=""><img alt="" /></show-media>
        <show-description></show-description>
        <add-to-cart><button type="button">Add to cart</button></add-to-cart>
      </StoresynkProduct>
    </StoresynkStore>
  );
}

At build/SSR time <StoresynkProduct> fetches the product (one request per handle per build, shared across pages), fills every displayer tag, image, and sale badge with the exact output the client runtime would produce, and embeds the raw product as a JSON payload:

<storesynk-product handle="classic-snowboard">
  <script type="application/json" data-storesynk-product>{…}</script>
  <show-title>The Classic Snowboard</show-title>
  …

In the browser, the runtime detects the payload and hydrates its state from it — no product/shop queries, no repaint — while everything interactive (options, quantity, cart, checkout) works exactly as on a static Storesynk page. <StoresynkStore> does the same for the shop (money formatting), so prices are formatted on the very first client update. Soft navigations re-insert the payloads with the new page, so adoption stays refetch-free there too.

How this coexists with React

Storesynk's engine mutates the DOM it owns (displayers rewrite text, lists clone templates) — DOM that React must therefore never reconcile. The wrappers make that safe:

  1. Children of <StoresynkProduct> / <StoresynkList> / <StoresynkStatic> must be static host elements — plain tags, no client components, no React state or event handlers. They are rendered once to an HTML string on the server and injected via dangerouslySetInnerHTML, so React treats the whole subtree as an opaque string. Interactivity comes from the Storesynk elements themselves (<add-to-cart>, <change-option>, …), not from React.
  2. Standalone Storesynk tags as plain JSX (a nav <open-cart> with <show-cart-count>, say) are fine in Server Components, which render once and never re-render on the client. Don't put them inside a re-rendering client component — if you must, wrap them in <StoresynkStatic>.
  3. Markup whose structure the engine rewrites (the <storesynk-cart> drawer: its template child is removed and cloned per line item) should always go through <StoresynkStatic>.

The elements runtime loads after hydration, so engine updates can never cause hydration mismatches.

Components & helpers

  • <StoresynkStore country? language? trackEvents? loadRuntime? serverCart?> — renders <storesynk-store> with the configured credentials + an embedded shop payload. Children are ordinary React content; put it in your root layout so the cart survives navigation. serverCart switches the cart to server-owned: the cart id lives in an HttpOnly shopify_cartId cookie (the headless convention, so server code written against templates like vercel-shop shares the same cart) and every mutation runs as a server action with the configured credentials — the browser never learns the id, and your own server code (agents, RSC cart rendering) can read the same cart. The standard window.Shopify.actions / shopify:cart:* protocol is unchanged; only the I/O moves. Mutations invalidate a cart cache tag (updateTag on Next 16+, revalidateTag fallback) — tag your own cart reads with it to get read-your-writes. Default: the client-side Storefront-API cart (id in localStorage), same as every other surface.
  • <StoresynkProduct handle revalidate? selectedOptions?> — renders <storesynk-product> with the filled children + an embedded product payload (including any metafields the markup displays). revalidate makes the client refetch in the background and repaint only if the live data differs. selectedOptions (usually await searchParams) makes a shared variant URL server-render that variant — keys match option names case-insensitively, non-option params are ignored, garbage falls back to the default variant; the reserved variant param carries a numeric variant id (?variant=41820371452004, the Liquid/Hydrogen shape) and wins over option params — and stamps sync-url on the host, so the client adopts the same selection from the URL and mirrors later option clicks back into it (?color=red, history.replaceState; put sync-url="variant" on a hand-authored host to write ?variant=<id> links instead). Under Cache Components, awaiting searchParams moves the region out of the static shell — for static/PPR pages pass syncUrl alone instead: the shell keeps the default variant and the client corrects a shared variant URL at adoption. If the product isn't found, the children render untouched (and the build log warns).
  • <StoresynkList collection limit?> — server-renders a collection grid: the first element child is the item template (the same contract as <storesynk-list>), one filled clone per product with crawlable /products/<handle> links and per-item payloads; the client engine adopts the items with zero refetch.
  • <StoresynkProductJsonLd handle url?> — schema.org Product JSON-LD (price, availability, images) as a body-level script.
  • productMetadata(handle, { title?, description?, url?, siteName? }) — a Metadata object for generateMetadata: <title>, meta description, Open Graph tags. (og:type/og:price:* render via metadata.other as name= meta tags — a Next Metadata limitation; the JSON-LD carries the structured data rich results consume.)
  • <StoresynkStatic> — stringify-and-inject wrapper for Storesynk markup that must live where React re-renders (see above).
  • @storesynk/next/runtime — the data helpers (getProduct, getShop, getCollectionProducts, getProductMetafields, getProductHandles) if you need the same cached fetches in your own server code.
  • installServerCartTransport() / <StoresynkServerCart /> — the pieces behind serverCart, exported for loadRuntime={false}-style custom setups. Registration must happen before the elements runtime boots (module scope of a client module is the safe spot). Built on core's CartTransport seam (registerCartTransportFactory), which you can also implement yourself to back the standard actions with any cart system — e.g. an existing app's own server-actions cart.

Localization

configureStoresynk({ country, language }) applies Shopify's @inContext to every server-side fetch (and the client runtime uses the same context via the store attributes), so SSR'd prices match the configured market.

Caching & invalidation (Next 16 cacheComponents: true)

Caching is package-owned: every data helper behind the wrappers runs in its own "use cache" scope with data-derived cache tags — you write no cache scopes for correctness, and Shopify webhooks invalidate exactly the surfaces that rendered the changed entity. The tag taxonomy:

| Operation | Tags | | --- | --- | | getProduct(handle) | products, product-<handle>, product-<numericId> | | getProductMetafields / volume / customer-pricing | products, metaobjects, product-<numericId> | | getCollectionProducts / getCollectionListing(handle) | products, collections, collection-<handle>, + product-<id> per listed product | | getAllProducts / getCollectionListing(null) | products, products-index, + product-<id> per product | | getShop / getLocalization / getAccountUrl | shop (+ localization) | | getRecommendationRoots (related / upsell picks) | products, shop, product-<numericId> |

List operations tag every returned product's numeric id, so a product update refreshes each grid it appears in without purging the catalog. Wire the invalidation side with the bundled handler:

// app/api/webhooks/shopify/route.ts
import { createStoresynkWebhookHandler } from '@storesynk/next';
export const POST = createStoresynkWebhookHandler();

Register the products/*, collections/*, metaobjects/*, and shop/update topics in Shopify pointing at that route, with SHOPIFY_WEBHOOK_SECRET set. Entries live under the cacheLife profile from storesynk.config.ts. The default 'max' is webhook-driven only: without the webhook route a cached read never expires, so products and offers edited in the admin after the first render never show up. Until the webhook route is wired, set cacheLife: 'minutes' or longer (npx @storesynk/next init scaffolds 'minutes', which picks up admin edits within about a minute). Do not go under five minutes: Next treats caches that expire sooner (such as 'seconds') as runtime data, and next build then fails on prerendered routes with "encountered uncached or runtime data during prerendering". revalidate on <StoresynkProduct> remains a browser-side self-heal on top.

Related / upsell freshness has two lanes. Product picks live on the product ($app:recommendations), and Shopify fires products/update for a product-level metafield write, so the webhook route expires product-<handle> and the picks refresh with the product. The shop-level settings (related_settings / upsell_settings, including the upsell discount label, cart seeds and free-shipping goals) are shop metafields, for which Shopify fires no webhook at all; the package therefore reads them on their own 'minutes' lifetime (RECOMMENDATION_SETTINGS_CACHE_LIFE) and merges them over the long-lived shop entry, so a merchant's settings change shows within about a minute on every store, webhooks or not.

A <StoresynkProduct> slot that authors an offer widget (<storesynk-bundle>, <storesynk-mix-match>, <storesynk-addons>, <storesynk-volume-discount>, <storesynk-customer-pricing>) renders at request time: the widget fills compare offer schedules against the clock, which Next rejects during prerendering, so the wrapper awaits connection() before the fill. A fixed-handle page with a widget is therefore never prerendered (its data reads stay cached as above); a slot without a widget is unaffected.

A product that is merely REFERENCED by a BOGO offer (every buy-side product once an offer is live) never forces request-time rendering: the ss-offer="bogo" badge stamp on list cards, collection cards and the product host runs without a clock during a prerender. An active offer with no schedule is stamped in the built HTML (byte-identical to the client); a scheduled offer is stamped by the client after adoption, so <StoresynkList> and <StoresynkCollection> always prerender, whatever offers the store runs.

Wrapping a Storesynk region in your own "use cache" scope (fragment-output caching) is optional: the package's tags propagate to the enclosing scope, so webhook invalidation still reaches it. Leave offer widgets out of such a scope, since connection() cannot run inside "use cache". The fresh prop bypasses the persistent cache entirely (deduped per request) for draft/preview reads.

One sharp edge:

  • Dev-only hydration noise: dev restores cached trees with the dangerouslySetInnerHTML prop in serialized shape and warns on the second request of a cached route. The hosts ship suppressHydrationWarning (they are React-opaque by design); production is unaffected. A related React DevTools extension assertion ("The children should not have changed if we pass in the same set", installHook.js stack) can also appear — extension-internal, harmless, not suppressible from app code.

Per-request market from a cookie (dynamic routes)

With persistLocale="cookie" on <StoresynkStore>, the client's Markets pickers persist the buyer's choice as a storesynk_locale=<COUNTRY>/<LANGUAGE> cookie. A dynamic route opts into rendering in that market by resolving it explicitly and passing it down — getRequestLocale() never runs implicitly, because calling cookies() makes a route dynamic and an invisible default would silently un-static every page:

// app/layout.tsx — safe everywhere: empty (null) on force-static routes
const locale = await getRequestLocale();
<StoresynkStore locale={locale} persistLocale="cookie">…</StoresynkStore>

// app/live/[handle]/page.tsx
export const dynamic = 'force-dynamic';
const locale = await getRequestLocale();
return <ProductPdp handle={handle} locale={locale} />;   // → StoresynkProduct locale={…}

productMetadata(handle, { locale }) and <StoresynkProductJsonLd locale={…}> accept the same value. Prices, translations, and currency symbols render server-side in the buyer's market, and the emitted store attributes match the client's effective locale — adoption with zero refetch and no default-currency flash. Static routes keep the config defaults (the client repaints there via revalidate semantics instead). The server only ever reads the cookie; the client picker writes it, so first visits render the default market.

Server-rendered coverage

Displayers, images/media (including video, external video, and 3D models), sale badges, option controls (swatch/select/radio with full ARIA state), thumbnails, product links, selling-plan initial state, scalar/rich-text metafields, and collection grids. Remaining client-only: money/file/list-type metafields (locale- or engine-dependent), predictive search, and the cart (per-user by design).