Installation
npm install @commerce-blocks/sdk
Loading from a CDN
The SDK ships as a pure ES module with no Node-only dependencies, so it works in the browser with no build step. Use this when you’re integrating directly into a Shopify theme, a static HTML page, or any environment where you don’t run npm install.
unpkg (recommended)
unpkg serves the published npm package directly. Add the ?module query string and unpkg rewrites the SDK’s @preact/signals-core import into a sibling unpkg URL, so the browser resolves the entire module graph without a bundler.
<script type="module">
import { createClient } from 'https://unpkg.com/@commerce-blocks/sdk?module'
const { data: client, error } = createClient({
token: 'your-token',
sorts: [{ name: 'Featured', code: 'featured' }],
facets: [{ name: 'Color', code: 'options.color' }],
})
if (error) {
console.error('Layers init failed:', error.message)
} else {
window.layers = client
}
</script>
Pin the version explicitly in production so a new release never silently changes behavior on your storefront. unpkg accepts a major (@2), minor (@2.0), or exact (@2.0.3) tag — for example, https://unpkg.com/@commerce-blocks/sdk@2?module. See the SDK changelog for the latest version.
esm.sh
esm.sh is an alternative ESM CDN that pre-bundles the SDK and its dependency into a single file.
<script type="module">
import { createClient } from 'https://esm.sh/@commerce-blocks/sdk@2'
</script>
jsDelivr
jsDelivr mirrors npm with its own ESM transform. Use the +esm suffix to get a browser-compatible bundle.
<script type="module">
import { createClient } from 'https://cdn.jsdelivr.net/npm/@commerce-blocks/sdk@2/+esm'
</script>
Import maps
If you load multiple modules from a CDN and want to keep imports short, declare an import map once and use bare specifiers everywhere else:
<script type="importmap">
{
"imports": {
"@commerce-blocks/sdk": "https://unpkg.com/@commerce-blocks/sdk@2?module"
}
}
</script>
<script type="module">
import { createClient, getClient } from '@commerce-blocks/sdk'
// …
</script>
Reusing the client across modules
createClient registers the client in a singleton, so any later <script type="module"> block can pull it back with getClient. This is the cleanest pattern for theme integrations where one snippet boots the SDK and many other sections consume it.
<!-- Boot once in theme.liquid -->
<script type="module">
import { createClient } from 'https://unpkg.com/@commerce-blocks/sdk@2?module'
createClient({
token: '{{ shop.metafields.layers.embed_settings.value.storefrontApiToken }}',
sorts: [{ name: 'Featured', code: 'featured' }],
facets: [{ name: 'Color', code: 'options.color' }],
})
</script>
<!-- Consume anywhere -->
<script type="module">
import { getClient } from 'https://unpkg.com/@commerce-blocks/sdk@2?module'
const { data: client } = getClient()
const collection = client.collection({ handle: 'shirts' })
await collection.execute()
</script>
When pulling the SDK from a CDN, also add <link rel="modulepreload" href="https://unpkg.com/@commerce-blocks/sdk@2?module" /> to your <head> so the browser starts fetching it during HTML parse instead of waiting for the first <script type="module"> to execute.
Configuration
Required configuration
| Option | Type | Required | Description |
|---|
token | string | Yes | Layers API public token |
sorts | Sort[] | Yes | Sort options (see below) |
facets | Facet[] | Yes | Facet fields (see below) |
Sort:
| Property | Type | Required | Description |
|---|
name | string | Yes | Display name shown to users |
code | string | Yes | Sort order code configured in Layers |
scope | ('search' | 'collection')[] | No | Restrict to specific controllers. Omit to allow everywhere. |
order | number | No | Display order (lower numbers appear first) |
Facet:
| Property | Type | Required | Description |
|---|
name | string | Yes | Display name shown to users |
code | string | Yes | Attribute code in Layers (e.g. options.color, vendor, variants.price) |
Optional configuration
| Option | Type | Description |
|---|
attributes | string[] | Product attributes to fetch |
baseUrl | string | Custom API URL |
fetch | CustomFetch | Custom fetch implementation (SSR, testing) |
context | Context | Default market and shopper context applied to every controller (see Context) |
Context
Pass market and shopper context to personalize results across all controllers. Set it globally on the client config, per-query on execute(), or both — per-query context shallow-merges with and overrides the global context.
import { createClient } from '@commerce-blocks/sdk'
const { data: client } = createClient({
token: 'your-token',
sorts: [{ name: 'Featured', code: 'featured' }],
facets: [{ name: 'Color', code: 'options.color' }],
context: {
market: 'US',
geo: { country: 'US' },
shoppingChannel: 'web',
},
})
// Per-query override — merges with global context
const collection = client.collection({ handle: 'shirts' })
await collection.execute({
context: { geo: { country: 'CA', province: 'ON' } },
})
// Effective context: { market: 'US', geo: { country: 'CA', province: 'ON' }, shoppingChannel: 'web' }
Context fields:
| Field | Type | Description |
|---|
geo | GeoLocation | Geographic location: { country, province, city } |
market | string | Market identifier |
productsInCart | CartProduct[] | Products currently in the shopper’s cart |
productsPurchased | CartProduct[] | Previously purchased products |
priorSearches | PriorSearch[] | Recent searches: { searchQuery, hadClick, hasResults } |
marketing | Marketing | UTM-style attribution: { source, medium, campaign, term } |
customer | CustomerContext | Customer profile: { signedIn, returning, numberOfOrders, ... } |
shoppingChannel | 'web' | 'app' | Shopping channel the request originates from |
custom | Record<string, unknown> | Custom key-value pairs forwarded to merchandising strategies |
CartProduct:
| Property | Type | Required | Description |
|---|
title | string | Yes | Product title |
price | number | No | Unit price |
type | string | No | Product type |
productId | string | No | Product ID |
variantId | string | No | Variant ID |
quantity | number | No | Quantity in cart |
options | Record<string, string> | No | Selected options (e.g. { Size: 'L' }) |
CustomerContext:
| Property | Type | Description |
|---|
signedIn | boolean | Whether the shopper is signed in |
returning | boolean | Whether the shopper has visited before |
numberOfOrders | number | Lifetime order count |
averageOrderValue | number | Average order value |
daysBetweenOrders | number | Average days between orders |
daysSinceLastOrder | number | Days since the most recent order |
daysSinceOldestOrder | number | Days since the first order |
totalSpent | number | Lifetime spend |
Use the global context for values that rarely change within a session (market, channel, customer profile). Use per-query context for values that vary by page or interaction (current cart, geo override).
Product configuration
| Option | Type | Description |
|---|
currency | string | Currency for price formatting |
formatPrice | (amount, currency) => string | Custom price formatter |
swatches | Swatch[] | Color swatch definitions (see below) |
includeMeta | boolean | Include _meta in results (applied rules, variant breakouts) |
Swatch:
| Property | Type | Required | Description |
|---|
name | string | Yes | Option name this swatch belongs to (e.g. Color) |
value | string | Yes | Option value to match (e.g. Red) |
color | string | null | Yes | CSS color value (e.g. #ff0000) |
imageUrl | string | null | Yes | URL to a swatch image. Use when a color alone is not sufficient. |
Transforms post-process results before caching. Filter aliases map URL-friendly keys to API property names.
| Option | Type | Description |
|---|
transforms.product | ({ base, raw }) => object | Extend products with custom fields |
transforms.collection | (result, raw) => result | Transform collection results |
transforms.search | (result, raw) => result | Transform search results |
transforms.block | (result, raw) => result | Transform block results |
transforms.searchContent | (result, raw) => result | Transform content search results |
transforms.filters | (filters) => FilterGroup | Custom filter transformation |
filterAliases | FilterAliases | URL-friendly filter key mapping |
Once configured, transforms and aliases are applied automatically:
import { createClient } from '@commerce-blocks/sdk'
const { data: client } = createClient({
token: 'your-token',
sorts: [{ name: 'Featured', code: 'featured' }],
facets: [{ name: 'Color', code: 'options.color' }],
attributes: ['body_html'],
transforms: {
product: ({ raw }) => ({
description: raw.body_html ?? '',
rating: raw.calculated?.average_rating ?? 0,
}),
},
filterAliases: {
color: 'options.color',
size: 'options.size',
brand: { property: 'vendor', values: { nike: 'Nike', adidas: 'Adidas' } },
},
})
// Aliases resolve automatically
const collection = client.collection({ handle: 'shirts' })
await collection.execute({ filters: { color: 'Red', brand: 'nike' } })
// Products now include description and rating from the product transform
Cache configuration
| Option | Type | Description |
|---|
cacheLimit | number | Max entries in cache |
cacheLifetime | number | TTL in milliseconds |
storage | StorageAdapter | Custom storage adapter for cache persistence (defaults to localStorage in browser) |
initialData | CacheData | Pre-populate cache at initialization |
restoreCache | boolean | Auto-restore from storage on init (default: true) |
Singleton access
After initialization, access the client anywhere:
import { getClient, isInitialized } from '@commerce-blocks/sdk'
if (isInitialized()) {
const { data: client } = getClient()
if (client) {
// Use client
}
}
Important notes
All SDK methods return a Result type instead of throwing exceptions. Always check for result.error before accessing result.data.