--- title: "Sanity + Shopify + Hydrogen Rules" description: Integration guide for Sanity with Shopify using the Hydrogen framework (React Router 7). --- # Sanity + Shopify + Hydrogen Rules **Package:** [`hydrogen-sanity`](https://github.com/sanity-io/hydrogen-sanity) — requires `@shopify/hydrogen >= 2025.5.0` ## 1. Architecture Overview | Component | Purpose | |-----------|---------| | **Shopify** | Product catalog, inventory, checkout (source of truth for commerce) | | **Sanity Connect** | Syncs Shopify data to Sanity in real-time | | **Sanity Studio** | Editorial content, rich descriptions, media (enhances Shopify data) | | **Hydrogen** | React Router 7 front-end optimized for Shopify | **Project Structure:** ``` ./ ├── /studio # Sanity Studio └── /web # Hydrogen front-end ``` ## 2. Environment Variables ```bash # web/.env PUBLIC_STOREFRONT_API_TOKEN="your-public-storefront-token" PRIVATE_STOREFRONT_API_TOKEN="your-private-storefront-token" PUBLIC_STORE_DOMAIN="your-store.myshopify.com" SESSION_SECRET="your-random-session-secret" # Sanity SANITY_PROJECT_ID="your-project-id" SANITY_DATASET="production" SANITY_API_VERSION="2026-02-01" SANITY_PREVIEW_TOKEN="your-sanity-viewer-token" # Viewer token for previews ``` ## 3. Sanity Client Setup ### Vite Config ```typescript // web/vite.config.ts import {hydrogen} from '@shopify/hydrogen/vite' import {sanity} from 'hydrogen-sanity/vite' export default defineConfig({ plugins: [hydrogen(), sanity()], }) ``` ### Context Setup ```typescript // web/app/lib/context.ts import {createSanityContext, type SanityContext} from 'hydrogen-sanity' import {PreviewSession} from 'hydrogen-sanity/preview/session' import {isPreviewEnabled} from 'hydrogen-sanity/preview' const sanity = await createSanityContext({ request, cache, waitUntil, client: { projectId: env.SANITY_PROJECT_ID, dataset: env.SANITY_DATASET, apiVersion: env.SANITY_API_VERSION || '2026-02-01', useCdn: true, stega: { enabled: isPreviewEnabled(env.SANITY_PROJECT_ID, previewSession), studioUrl: 'http://localhost:3333', } }, preview: { token: env.SANITY_PREVIEW_TOKEN, session: previewSession, } }) ``` ### Provider Setup (entry.server.tsx) ```typescript const {SanityProvider} = context.sanity const body = await renderToReadableStream( , ) ``` ### Root Layout (root.tsx) ```typescript import {Sanity} from 'hydrogen-sanity' export function Layout({children}) { const nonce = useNonce() return ( {children} {/* Required for client-side */} ) } ``` ## 4. Data Fetching Fetch from **both** Shopify (GraphQL) and Sanity (GROQ). Use `defineQuery` for TypeGen support. ### Recommended: `query` + `Query` component ```typescript import {defineQuery} from 'groq' import {Query} from 'hydrogen-sanity' const PRODUCT_QUERY = defineQuery(`*[_type == "product" && store.slug.current == $handle][0]{ body }`) // Loader export async function loader({params, context: {sanity}}: LoaderFunctionArgs) { const initial = await context.sanity.query(PRODUCT_QUERY, params) return {initial} } // Component - auto-enables live preview when active export default function ProductPage({loaderData}) { return ( {(data) =>
{data?.body}
}
) } ``` ### Alternative methods | Method | Use Case | |--------|----------| | `sanity.query()` + `Query` | Recommended - auto preview mode | | `sanity.loadQuery()` | Manual loader integration | | `sanity.fetch()` | No preview needed, lightweight | | `sanity.client` | Mutations in actions | ### Images ```typescript import {useImageUrl} from 'hydrogen-sanity' function Hero({image}) { const imageUrl = useImageUrl(image) return } ``` **Key Insight:** Shopify fields synced via Sanity Connect are `readOnly`. Use Sanity for editorial enhancements only. ## 5. Visual Editing Setup ### Root Layout ```typescript // web/app/root.tsx import {usePreviewMode} from 'hydrogen-sanity/preview' import {VisualEditing} from 'hydrogen-sanity/visual-editing' export function Layout({children}: {children?: React.ReactNode}) { const previewMode = usePreviewMode() return ( {children} {previewMode ? : null} ) } ``` ### Preview Route ```typescript // web/app/routes/api.preview.ts export {action, loader} from 'hydrogen-sanity/preview/route' ``` ### Content Security Policy ```typescript // web/entry.server.tsx const {nonce, header, NonceProvider} = createContentSecurityPolicy({ frameAncestors: isPreviewEnabled ? [studioHostname] : [], connectSrc: [ `https://${projectId}.api.sanity.io`, `wss://${projectId}.api.sanity.io`, ], }) ``` ## 6. Studio: Presentation Tool ```typescript // studio/sanity.config.ts import {presentationTool} from 'sanity/presentation' export default defineConfig({ plugins: [ presentationTool({ resolve: { locations: { product: defineLocations({ select: { title: 'store.title', slug: 'store.slug.current' }, resolve: (doc) => ({ locations: [ { title: doc?.title || 'Untitled', href: `/products/${doc?.slug}` }, { title: 'Products', href: `/collections/all` }, ], }), }), }, }, previewUrl: { origin: 'http://localhost:3000', previewMode: { enable: '/api/preview' }, }, }), ], }) ``` ## 7. Commands ```bash # Install dependencies pnpm add hydrogen-sanity @sanity/client @portabletext/react # Development (run in separate terminals) cd studio && pnpm dev # Studio at localhost:3333 cd web && pnpm dev # Hydrogen at localhost:3000 # Sanity Manage (CORS, tokens): https://www.sanity.io/manage pnpm dlx sanity manage ``` ## 8. Boundaries - Always: - Query Shopify for commerce data (price, inventory, variants) - Query Sanity for editorial content (rich text, custom fields) - Use `hydrogen-sanity` package for Visual Editing - Add Hydrogen URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage) - Ask First: - Before modifying Sanity Connect sync settings - Before changing CSP configuration - Never: - Edit Shopify-synced fields in Sanity (they're `readOnly`) - Expose `SANITY_API_TOKEN` to client-side code - Query Sanity for commerce data that should come from Shopify