Shopify Integration
Shopify provides the commerce engine for Tune Me In through its Storefront API, delivering real-time product data, inventory, and checkout functionality.
Overview
The Shopify integration provides:
Product Data - Real-time pricing, availability, and variants
Inventory Management - Live stock levels and availability
Product Media - Images, videos, and 3D models
Metafields - Custom product metadata
Cart & Checkout - Shopping cart and secure checkout flow
Configuration
Shopify is configured in shopify.config.js:3-9:
export default {
graphqlApiVersion: 'unstable' ,
locale: 'en-us' ,
storeDomain: 'sanity-dev-store.myshopify.com' ,
storefrontToken: '791dbd01268e4a7129288e24b1012710' ,
sanity: sanityConfig ,
} ;
Configuration Options
graphqlApiVersion - Storefront API version ('unstable' uses the latest features)
locale - Default locale for the storefront
storeDomain - Your Shopify store domain (.myshopify.com)
storefrontToken - Public access token for the Storefront API
sanity - Embedded Sanity configuration for unified setup
Never commit your production Storefront Token to version control. Use environment variables for production deployments.
Storefront API
The Shopify Storefront API is a GraphQL API that provides read access to your store’s product catalog.
API Capabilities
Products & Variants - Query product data with all variants
Collections - Product groupings and filters
Media - Images, videos, and product media
Metafields - Custom fields on products and variants
Cart Operations - Create, update, and manage carts
Checkout - Customer checkout and payment processing
Authentication
The Storefront API uses a public access token that’s safe to use in client-side code. The token is configured via storefrontToken and used by Hydrogen’s ShopifyServerProvider.
From App.server.jsx:15:
< ShopifyServerProvider shopifyConfig = { shopifyConfig } { ... serverState } >
{ /* Your app */ }
</ ShopifyServerProvider >
GraphQL Queries
Product Queries
The useSanityQuery hook automatically fetches products from Shopify using GraphQL fragments.
Default Fragment
By default, products are fetched using Hydrogen’s ProductProviderFragment, which includes all fields needed for <ProductProvider>:
fragment ProductProviderFragment on Product {
id
title
handle
descriptionHtml
media ( first : 10 ) {
edges {
node {
... on MediaImage {
id
image {
altText
url
}
}
}
}
}
variants ( first : 250 ) {
edges {
node {
id
title
availableForSale
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
selectedOptions {
name
value
}
}
}
}
}
Custom Fragments
You can customize which fields are fetched using getProductGraphQLFragment.
From pages/Index.server.jsx:27-47:
const { sanityData , shopifyProducts } = useSanityQuery ({
query: QUERY ,
getProductGraphQLFragment : () => {
return `
...ProductProviderFragment
mf:metafields(namespace:"tunemein", first:1){
edges {
node {
key
value
}
}
}
images(first: 10) {
edges {
node {
altText
url
}
}
}
` ;
},
});
This extends the default fragment to include:
Custom metafields from the tunemein namespace
Additional product images beyond the default
Custom fragments can improve performance by fetching only the data you need. For product listings, you might exclude variant data to reduce payload size.
Metafields allow you to attach custom data to products:
mf : metafields ( namespace : "tunemein" , first :2){
edges {
node {
key
value
}
}
}
Accessing metafields in code:
const metafields = product . mf ?. edges || [];
const customField = metafields . find ( e => e . node . key === 'custom_key' );
const value = customField ?. node ?. value ;
Product Data Structure
Products returned by useSanityQuery are normalized into an object map:
const { shopifyProducts } = useSanityQuery ({ query: QUERY });
// shopifyProducts structure:
{
'shopifyProduct-7349334187288' : {
id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzY2Mzk2Mjk5MjY0ODc=' ,
handle: 'red-tshirt' ,
title: 'Red T-shirt' ,
descriptionHtml: '<p>A comfortable red t-shirt</p>' ,
priceRange: {
minVariantPrice: { amount: '29.99' , currencyCode: 'USD' },
maxVariantPrice: { amount: '29.99' , currencyCode: 'USD' }
},
variants: {
edges: [
{
node: {
id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8...' ,
title: 'Small' ,
availableForSale: true ,
priceV2: { amount: '29.99' , currencyCode: 'USD' },
selectedOptions: [
{ name: 'Size' , value: 'Small' }
]
}
}
]
},
media: {
edges: [ /* ... */ ]
}
},
'shopifyProduct-7342335787245' : { /* ... */ }
}
Accessing Product Data
Products are accessed by their Sanity ID:
const sanityProductId = sanityProduct . _id ; // e.g., 'shopifyProduct-123'
const shopifyData = shopifyProducts [ sanityProductId ];
Merging Sanity and Shopify data:
const product = {
... sanityProduct , // Sanity fields (custom, editorial)
storefront: shopifyProducts [ sanityProduct . _id ] // Live Shopify data
};
From pages/Index.server.jsx:76-81:
< FeaturedCollection
title = { featuredCollection1 . title }
products = { featuredCollection1 . products . map (( product ) => {
return {
... product . productData ,
storefront: shopifyProducts ?.[ product ?. productData . _id ],
};
}) }
/>
Product Variants
Variant Selection
Variants are stored in Sanity references and resolved from Shopify:
From fragments/productWithVariant.js:3-11:
export const PRODUCT_WITH_VARIANT = groq `
product->{
_id,
"available": !store.isDeleted && store.status == 'active',
"slug": store.slug.current,
store,
"variantId": coalesce(^.variant->store.id, store.variants[0]->store.id)
}
` ;
This fragment:
References the product document
Gets the selected variant (or defaults to first variant)
Includes availability status from Sanity
Working with Variants
From pages/products/[handle].server.jsx:66-79:
const params = new URLSearchParams ( props . search );
const variantId = props ?. variantId || params ?. get ( 'variant' );
const encodedVariantId = encode ( 'ProductVariant' , variantId );
const flattenedVariants = flattenConnection ( product . storefront . variants );
const productVariantIndex = flattenedVariants . findIndex (
( variant ) => variant . id === encodedVariantId ,
);
const productVariant =
product . storefront ?. variants ?. edges [
productVariantIndex >= 0 ? productVariantIndex : 0
];
This code:
Gets variant ID from server state or URL params
Encodes it to Shopify’s Global ID format
Finds the matching variant in the edges array
Falls back to the first variant if not found
Shopify uses Global IDs (base64-encoded strings like Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8xMjM=). The encode utility converts numeric IDs to this format.
Variant URLs
Variants can be accessed via URL parameters:
/products/red-tshirt?variant=123456789
The LinkProduct component handles this automatically:
< LinkProduct
productId = { product . _id }
variantId = { variant . id }
>
View Product
</ LinkProduct >
Product Provider Pattern
Hydrogen’s <ProductProvider> component provides product context to child components.
From pages/products/[handle].server.jsx:86-91:
< ProductProvider
product = { product ?. storefront }
initialVariantId = { productVariant ?. node ?. id }
>
< ProductDetails product = { product } />
</ ProductProvider >
Child components can then use Hydrogen’s product hooks:
import { useProduct } from '@shopify/hydrogen/client' ;
function ProductPrice () {
const { selectedVariant } = useProduct ();
return (
< div >
{ selectedVariant . priceV2 . amount } { selectedVariant . priceV2 . currencyCode }
</ div >
);
}
Collections
Collections in Tune Me In are managed primarily in Sanity, but can reference Shopify collections.
Collection Structure
From pages/collections/[handle].server.jsx:24-43:
const { sanityData : sanityCollection , shopifyProducts } = useSanityQuery ({
query: QUERY ,
params: { slug: handle , start , end },
getProductGraphQLFragment : () => {
return `
...ProductProviderFragment
images(first: 10) {
edges {
node {
altText
url
}
}
}
` ;
},
});
Collections support server-side pagination:
const pageSize = 6 ;
const page = currentPage || 0 ;
const start = page * pageSize ;
const end = start + ( pageSize - 1 );
The GROQ query uses array slicing:
products[available][$start..$end]
Product Context Provider
To avoid prop drilling, products are provided via React Context.
From pages/products/[handle].server.jsx:84:
< ProductsProvider value = { shopifyProducts } >
< Layout >
{ /* Products available to all children */ }
</ Layout >
</ ProductsProvider >
Accessing products in nested components:
import { useProductsContext } from '../contexts/ProductsContext.client' ;
function NestedProductCard ({ productId }) {
const product = useProductsContext ( productId );
return (
< div >
< h3 > { product . title } </ h3 >
< p > { product . priceRange . minVariantPrice . amount } </ p >
</ div >
);
}
Cart Integration
While not extensively covered in the core concepts, Tune Me In uses Hydrogen’s cart functionality:
import { CartProvider } from './contexts/CartProvider.client' ;
< CartProvider >
{ /* App content */ }
</ CartProvider >
Components can add products to cart using Hydrogen’s cart hooks:
import { useCart } from '@shopify/hydrogen/client' ;
function AddToCartButton ({ variantId }) {
const { linesAdd } = useCart ();
const handleClick = () => {
linesAdd ([{
merchandiseId: variantId ,
quantity: 1
}]);
};
return < button onClick = { handleClick } > Add to Cart </ button > ;
}
Best Practices
Fetch only needed fields - Use custom fragments to reduce payload size
Paginate collections - Don’t load all products at once
Cache strategically - Consider caching product data client-side
Use variants wisely - Limit variant queries when showing many products
Data Freshness
Shopify data is always fresh - Each request fetches live data from Shopify
Sanity uses CDN - Content may be slightly delayed (seconds)
Handle out-of-stock - Always check availableForSale before checkout
Validate variants - Ensure selected variants still exist
Error Handling
Check product existence - Products can be deleted in Shopify
Handle missing variants - Default to first variant gracefully
Validate availability - Products can become unavailable
Provide fallbacks - Show alternative products when unavailable
Security
Use environment variables - Never commit tokens to source control
Rotate tokens periodically - Regenerate Storefront tokens regularly
Scope appropriately - Use read-only tokens where possible
Monitor usage - Track API usage to detect issues
Shopify Global IDs
Shopify uses base64-encoded Global IDs for all resources:
gid://shopify/Product/7349334187288
Encoded:
Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzczNDkzMzQxODcyODg=
Encoding Helper
From utils/shopifyGid.js:
export function encode ( type , id ) {
return btoa ( `gid://shopify/ ${ type } / ${ id } ` );
}
export function decode ( gid ) {
const decoded = atob ( gid );
const match = decoded . match ( /gid: \/\/ shopify \/ ( \w + ) \/ ( \d + ) / );
return match ? { type: match [ 1 ], id: match [ 2 ] } : null ;
}
Next Steps
Data Fetching Learn how useSanityQuery unifies Sanity and Shopify data
Sanity Integration Understand how Sanity content references Shopify products
Architecture See how all the pieces fit together