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/astro

v0.1.0-beta.16

Published

Astro integration for Storesynk — server-rendered Shopify data inside Storesynk tags, adopted flash-free by the client runtime.

Readme

@storesynk/astro

Astro integration 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 in static builds (astro build) and SSR (any adapter). The markup you write inside the wrappers is exactly the same Storesynk tags you'd use on a plain HTML page.

Supports Astro 5, 6, and 7. If you SSR with @astrojs/node, match the adapter major to your Astro major: @astrojs/node@^9 for Astro 5, ^10 for Astro 6, ^11 for Astro 7 (Astro 6+ needs Node.js ≥ 22.12).

Install

npm install @storesynk/astro

Setup

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

npx @storesynk/astro init   # writes storesynk.config.ts and prints the astro.config wiring

It creates a storesynk.config.ts that exports the integration options (every option listed, commented, with its default). 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 if you prefer — the options are the same shape:

storesynk({
  domain: 'your-shop.myshopify.com',
  token: 'your-public-storefront-token', // public token only — never an Admin token
  cart: { openOnAdd: true },
  pixels: { enabled: true, server: ['meta', 'ga'] },
});

The integration injects the @storesynk/elements runtime on every page (disable with injectRuntime: false if you load it yourself) and carries the config to the server-side components. The connection / locale / customer / pixels / cart options are the one shared StoresynkConfig (same shape as @storesynk/next and the no-build data-storesynk-config block); injectRuntime is Astro-specific. (The app metafield namespace is hard-coded in the package for now — not an integration option, same as @storesynk/next.)

Usage

---
import { StoresynkStore, StoresynkProduct } from '@storesynk/astro/components';
---
<StoresynkStore>
  <StoresynkProduct handle="classic-snowboard">
    <show-title>Product Title</show-title>
    <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>
    <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.

Components

  • <StoresynkStore country? language? trackEvents?> — renders <storesynk-store> with the integration credentials + an embedded shop payload.
  • <StoresynkProduct handle revalidate? selectedOptions? syncUrl?> — renders <storesynk-product> with the filled slot + an embedded product payload (including any metafields the slot displays). revalidate makes the client refetch in the background and repaint only if the live data differs. On SSR routes the variant selection rides Astro.url automatically (selectedOptions overrides it): option params match option names case-insensitively, the reserved variant param carries a numeric variant id (?variant=41820371452004, the Liquid/Hydrogen shape) and wins over option params, garbage falls back to the default variant; sync-url is stamped so the client adopts the same selection and mirrors option clicks back into the URL. syncUrl stamps it alone for prerendered pages. If the product isn't found, the slot renders untouched (and the build log warns).
  • <StoresynkList collection limit?> — server-renders a collection grid: the slot's 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.
  • <StoresynkSEO handle title? description? url? siteName?> — place inside <head>: <title>, meta description, Open Graph tags, and schema.org Product JSON-LD (price, availability, images) from the same cached fetches.

Dynamic routes

---
// src/pages/products/[handle].astro
import { getProductHandles } from '@storesynk/astro/runtime';

export async function getStaticPaths() {
  return (await getProductHandles()).map((handle) => ({ params: { handle } }));
}
---

Localization

country/language in the integration config apply Shopify's @inContext to every server-side fetch (and the client runtime uses the same context), so SSR'd prices match the configured market.

Previewing through a tunnel (ngrok, cloudflared, LAN)

astro dev/astro preview are Vite servers, and Vite rejects requests whose Host header isn't localhost (DNS-rebinding protection): Blocked request. This host (…) is not allowed. Allow your tunnel hostname in your own astro.config.mjs:

export default defineConfig({
  vite: { preview: { allowedHosts: ['my-site.ngrok.io'] } }, // server.allowedHosts for dev
});

Local-preview only — deployed static files or an SSR adapter never run Vite, so production is unaffected.

Static or SSR? Start static.

Both modes run the same Storesynk code and produce the same SEO-complete HTML — the only thing you're choosing is when pages render (once at build vs. per request) and therefore how fresh prices/stock are and what hosting you need. The cart is live in both modes, and crawlers see full HTML in both modes, so neither SEO nor checkout should drive this choice.

Default: static + revalidate + rebuild-on-webhook. Free CDN hosting, instant pages, self-healing prices. This covers most stores:

| Your situation | Mode | | --- | --- | | Catalog fits a build (≲ a few thousand pages) and prices change occasionally | Static (+ revalidate on PDPs) | | Prices/stock must be correct in the HTML itself at all times (not just after hydration) | SSR for those routes (prerender = false) | | Catalog too large or too volatile to rebuild (tens of thousands of products, constant updates) | SSR | | Per-request context: geo-based market pricing, personalization, cookies | SSR | | Mixed: marketing pages + a few hot product pages | Hybrid — static site, prerender = false on the hot routes |

Signals you chose wrong: rebuilds taking too long or firing constantly → move the hot routes to SSR; an SSR server sitting idle serving unchanged pages → go back to static.

SSR caveat (current version): the runtime's fetch helpers are module-cached with no TTL — designed for builds. On a long-running SSR server that means per-process freshness, not per-request; pair SSR with revalidate or restarts-on-deploy for now. Caches are keyed per market, so size is bounded by markets × handles.

Per-request market from a cookie (SSR)

With persistLocale: 'cookie' on the integration, the client's Markets pickers (<change-country> / <change-language>) persist the buyer's choice as a storesynk_locale=<COUNTRY>/<LANGUAGE> cookie — which the wrapper components read back on SSR requests (prerender = false routes, any adapter) and render everything in that market: @inContext prices, translations, correct currency symbols, and matching country/language attributes on <storesynk-store>, so the client adopts the payloads with zero refetch and no default-currency flash.

// astro.config.mjs
adapter: node({ mode: 'standalone' }),
integrations: [storesynk({ domain, token, persistLocale: 'cookie' })],
---
// src/pages/live/[handle].astro — rendered per request
export const prerender = false;
---

Precedence per field: component prop > cookie > integration config. Prerendered pages ignore the cookie (a build has no request) and keep the config defaults — a visitor with a persisted locale gets the client-side revalidate-style repaint there instead. The server only ever reads the cookie; writing it is the client picker's job, so the first visit renders the default market.

Freshness

  • Static builds freeze product data at build time. Titles and descriptions are fine; prices/stock go stale between builds — rebuild on Shopify webhooks, or use an SSR adapter for per-request freshness. The cart is always live (client-side, per-user).
  • astro dev fetches per dev-server process (module-cached).

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