hydrogen-sanity
Version:
Sanity.io toolkit for Hydrogen
988 lines (773 loc) • 33.4 kB
Markdown
# hydrogen-sanity
> [!TIP]
> **Upgrading from v4?** See the [migration guide](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v4-to-v5.md) for breaking changes and new features. 🎉
[Sanity.io](https://www.sanity.io) toolkit for [Hydrogen](https://hydrogen.shopify.dev/). Requires `@shopify/hydrogen >= 2025.5.0`.
Learn more about [getting started with Sanity](https://www.sanity.io/docs/getting-started).
**Features:**
- Drop-in preview mode handling with pre-built route and session management.
- Opinionated data fetching that automatically adapts to preview mode.
- Interactive live preview with automatic loader detection for [Visual Editing](https://www.sanity.io/docs/loaders-and-overlays) in Sanity's Presentation tool.
- Optimized image URL generation hooks with Sanity's image URL builder.
- TypeScript support with [Sanity TypeGen](https://www.sanity.io/docs/sanity-typegen).
> [!TIP]
>
> If you'd prefer a self-paced course on how to use Sanity and Hydrogen, check out the [Sanity and Shopify with Hydrogen on Sanity Learn](https://www.sanity.io/learn/course/sanity-and-shopify-with-hydrogen).
- [Installation](#installation)
- [Add Vite plugin](#add-vite-plugin)
- [Usage](#usage)
- [Satisfy TypeScript](#satisfy-typescript)
- [Set up the Sanity provider](#set-up-the-sanity-provider)
- [Interacting with Sanity data](#interacting-with-sanity-data)
- [Recommended: Using `query` and `Query` together](#recommended-using-query-and-query-together)
- [Alternative: Cached queries using `loadQuery`](#alternative-cached-queries-using-loadquery)
- [Additional `loadQuery` options](#additional-loadquery-options)
- [Alternative: Direct queries using `fetch`](#alternative-direct-queries-using-fetch)
- [Alternative: Using `client` directly](#alternative-using-client-directly)
- [Using Sanity TypeGen](#using-sanity-typegen)
- [Working with images](#working-with-images)
- [Enable preview mode](#enable-preview-mode)
- [Configure preview mode](#configure-preview-mode)
- [Add Visual Editing component](#add-visual-editing-component)
- [Visual Editing configuration options](#visual-editing-configuration-options)
- [Visual Editing components](#visual-editing-components)
- [Preview mode route](#preview-mode-route)
- [Set up CORS for front-end domains](#set-up-cors-for-front-end-domains)
- [Modify storefront's Content Security Policy (CSP)](#modify-storefronts-content-security-policy-csp)
- [Set up Presentation tool](#set-up-presentation-tool)
- [Troubleshooting](#troubleshooting)
- [Using `@sanity/client` directly](#using-sanityclient-directly)
- [Migration Guides](#migration-guides)
- [License](#license)
- [Develop & test](#develop--test)
- [Release new version](#release-new-version)
> [!NOTE]
> This package builds on [`@sanity/react-loader`](https://github.com/sanity-io/visual-editing/tree/main/packages/react-loader) and [`@sanity/visual-editing`](https://github.com/sanity-io/visual-editing/tree/main/packages/visual-editing), providing Hydrogen-specific optimizations and caching integration. For non-Hydrogen React applications, consider using these packages directly.
>
> Using this package isn't required for working with Sanity in a Hydrogen storefront. If you'd prefer to use `@sanity/react-loader` and/or `@sanity/client` directly, see [Using `@sanity/client` directly](#using-sanityclient-directly) below.
## Installation
```sh
npm install hydrogen-sanity @sanity/client
```
```sh
yarn add hydrogen-sanity @sanity/client
```
```sh
pnpm install hydrogen-sanity @sanity/client
```
### Add Vite plugin
Add the Vite plugin to your `vite.config.ts`:
```ts
import {defineConfig} from 'vite'
import {hydrogen} from '@shopify/hydrogen/vite'
import {sanity} from 'hydrogen-sanity/vite'
export default defineConfig({
plugins: [hydrogen(), sanity() /** ... */],
// ... other config
})
```
## Usage
Create the Sanity context and pass it through to your application, and optionally, configure the preview mode if you plan to setup Visual Editing
> [!NOTE]
> The examples below are up-to-date as of `npm create @shopify/hydrogen@2025.7.0`
```diff
// ./lib/context.ts
// ...all other imports
+ import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
// Define the additional context object
const additionalContext = {
// Additional context for custom properties, CMS clients, 3P SDKs, etc.
// These will be available as both context.propertyName and context.get(propertyContext)
// Example of complex objects that could be added:
// cms: await createCMSClient(env),
// reviews: await createReviewsClient(env),
} as const;
// Automatically augment HydrogenAdditionalContext with the additional context type
type AdditionalContextType = typeof additionalContext;
declare global {
interface HydrogenAdditionalContext extends AdditionalContextType {
+
+ // Augment `HydrogenAdditionalContext` with the Sanity context
+ sanity: SanityContext;
}
}
export async function createHydrogenRouterContext(
request: Request,
env: Env,
executionContext: ExecutionContext,
) {
// ... Leave all other functions as-is
const waitUntil = executionContext.waitUntil.bind(executionContext)
const [cache, session] = await Promise.all([
caches.open('hydrogen'),
AppSession.init(request, [env.SESSION_SECRET]),
])
+ // Initialize the Sanity context
+ const sanity = await createSanityContext({
+ request,
+
+ // To use the Hydrogen cache for queries
+ cache,
+ waitUntil,
+
+ // Sanity client configuration
+ client: {
+ projectId: env.SANITY_PROJECT_ID,
+ dataset: env.SANITY_DATASET || 'production',
+ apiVersion: env.SANITY_API_VERSION || 'v2024-08-08',
+ useCdn: process.env.NODE_ENV === 'production',
+ },
+
+ // You can also initialize a client and pass it instead
+ // client: createClient({
+ // projectId: env.SANITY_PROJECT_ID,
+ // dataset: env.SANITY_DATASET,
+ // apiVersion: env.SANITY_API_VERSION,
+ // useCdn: process.env.NODE_ENV === 'production',
+ // }),
+
+ // Optionally, set a default cache strategy, defaults to `CacheLong`
+ // strategy: CacheShort() | null,
+ })
+ // Make `sanity` available to loaders and actions in the request context
const hydrogenContext = createHydrogenContext(
{
env,
request,
cache,
waitUntil,
session,
i18n: {language: 'EN', country: 'US'},
cart: {
queryFragment: CART_QUERY_FRAGMENT,
},
},
+ {
+ ...additionalContext
+ sanity,
+ } as const,
+ )
+
return hydrogenContext
}
```
Learn more about [Sanity's JavaScript client configuration](https://www.sanity.io/docs/js-client).
Update your environment variables with settings from your Sanity project.
- Copy these from [sanity.io/manage](https://sanity.io/manage).
- Or run `npx sanity@latest init --env` to fill the minimum required values from a new or existing project.
```sh
# Project ID
SANITY_PROJECT_ID=""
# Dataset name
SANITY_DATASET=""
# (Optional) Sanity API version
SANITY_API_VERSION=""
# Sanity token to authenticate requests in "preview" mode
# must have `viewer` role or higher access
# Create in sanity.io/manage
SANITY_PREVIEW_TOKEN=""
```
### Satisfy TypeScript
Update the environment variables in `Env` to include the ones you created above:
> [!NOTE]
> If you plan to reference any environment variables in the client bundle, say for your embedded Studio configuration, you must prefix them with either `PUBLIC_` or `SANITY_STUDIO_`
```diff
// ./env.d.ts
declare global {
// ...other types
interface Env extends HydrogenEnv {
// ...other environment variables
+ SANITY_PROJECT_ID: string
+ SANITY_DATASET?: string
+ SANITY_API_VERSION?: string
+ SANITY_PREVIEW_TOKEN: string
}
}
```
### Set up the Sanity provider
You now need to wrap your app with the Sanity provider to make Sanity context available to client-side hooks and components like `useImageUrl` and `Query`.
**Update entry.server.tsx**
Wrap your app with the Sanity provider in your server entry point:
```diff
// ./app/entry.server.tsx
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
- context: AppLoadContext,
+ context: HydrogenRouterContextProvider,
) {
+ const {SanityProvider} = context.sanity
// ... CSP setup etc ...
const body = await renderToReadableStream(
<NonceProvider>
+ <SanityProvider>
<ServerRouter context={reactRouterContext} url={request.url} nonce={nonce} />
+ </SanityProvider>
</NonceProvider>,
// ... render options
)
}
```
**Update root.tsx**
Add the `Sanity` component to your root layout:
```diff
// ./app/root.tsx
+ import {Sanity} from 'hydrogen-sanity'
export function Layout({children}: {children?: React.ReactNode}) {
const nonce = useNonce()
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
+ {/* Add Sanity client-side script */}
+ <Sanity nonce={nonce} />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}
```
## Interacting with Sanity data
### Recommended: Using `query` and `Query` together
The `query` method and `Query` component work together to provide an optimized data fetching and rendering experience that automatically adapts based on whether Sanity preview mode is active:
**Best for**: Most applications wanting opinionated best practices with automatic optimization
- **Opinionated approach**: Curated patterns that codify Hydrogen + Sanity best practices
- Automatic preview mode detection and switching
- Built-in Visual Editing with click-to-edit overlays
- **Bundle optimization**: Preview-related code is conditionally loaded only when needed
- **Configuration decisions made for you**: Cache strategies, session management, and Visual Editing setup follow recommended approaches
**How it works**:
- **When preview mode is active**: Dynamically imports `@sanity/react-loader` with `loadQuery` for loader integration and client-side re-rendering with `useQuery` for real-time Studio updates
- **When preview mode is inactive**: Uses lightweight direct client `fetch` for optimal performance with static rendering
- **Bundle efficiency**: The `Query` component uses React's `lazy()` to conditionally load client-side preview components only when needed, while server-side `loadQuery` imports happen at runtime only when preview mode is detected
**Step 1: Fetch data in your loader with `query`**
```ts
import {defineQuery} from 'groq'
const HOMEPAGE_QUERY = defineQuery(`*[_id == "home"][0]{
_id,
title,
hero {
title,
description,
image {
asset->{
_id,
url
},
alt
}
},
modules[] {
_type,
_type == "productShowcase" => {
products[]-> {
_id,
store {
title,
slug,
previewImageUrl
}
}
}
}
}`)
export async function loader({context}: LoaderFunctionArgs) {
const initial = await context.sanity.query(HOMEPAGE_QUERY, undefined, {
tag: 'homepage',
hydrogen: {debug: {displayName: 'query Homepage'}},
})
return {initial}
}
```
**Step 2: Render with the `Query` component**
```tsx
import {Query} from 'hydrogen-sanity'
export default function HomePage({loaderData}: {loaderData: {initial: any}}) {
const {initial} = loaderData
return (
<Query query={HOMEPAGE_QUERY} options={{initial}}>
{(homepage, encodeDataAttribute) => (
<div>
<h1>{homepage?.hero?.title}</h1>
<p>{homepage?.hero?.description}</p>
{homepage?.modules?.map((module) => {
switch (module._type) {
case 'productShowcase':
return (
<div
key={module._key}
data-sanity={encodeDataAttribute(['modules', {_key: module._key}, '_type'])}
>
<div className="products">
{module.products?.map((product) => (
<div key={product._id}>
<h3>{product.store?.title}</h3>
{product.store?.slug && (
<Link to={`/products/${product.store.slug}`}>View Product</Link>
)}
</div>
))}
</div>
</div>
)
default:
return null
}
})}
</div>
)}
</Query>
)
}
```
> [!NOTE]
> The `encodeDataAttribute` function enables click-to-edit functionality in Sanity Studio's Presentation tool. It's only available when Sanity preview mode is active (managed via preview session) and returns `undefined` otherwise.
**Advanced Options**
Both methods accept the same options as their underlying implementations:
```ts
// In your loader
const page = await context.sanity.query<HomePage>(queryString, params, {
// Hydrogen caching options
hydrogen: {
cache: CacheShort(),
debug: {displayName: 'query Homepage'},
},
// Sanity request options
tag: 'home',
})
// In your component
<Query
query={queryString}
params={params}
options={initial}
fallback={<div>Loading...</div>} // React Suspense props
>
{(data) => <YourComponent data={data} />}
</Query>
```
### Alternative: Cached queries using `loadQuery`
**Best for**: Full control over preview mode behavior and loader integration (alternative to `query`/`Query`)
- Manual loader integration with real-time Studio updates when preview mode is active
- Uses `useQuery` hooks for client-side re-rendering with Sanity Studio
Query Sanity's API and use Hydrogen's cache to store the response (defaults to `CacheLong` caching strategy). When Sanity preview mode is active, `loadQuery` automatically bypasses the cache.
Learn more about configuring [caching in Hydrogen](https://shopify.dev/docs/custom-storefronts/hydrogen/caching).
Sanity queries appear in Hydrogen's [Subrequest Profiler](https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler). By default, they're titled `Sanity query`. You can give your queries more descriptive titles by using the request option below.
```ts
export async function loader({context, params}: LoaderFunctionArgs) {
const query = `*[_type == "page" && _id == $id][0]`
const params = {id: 'home'}
const initial = await context.sanity.loadQuery(query, params)
return {initial}
}
```
#### Additional `loadQuery` options
If you need to pass any additional options to the request provide `queryOptions` like so:
```ts
const page = await context.sanity.loadQuery<HomePage>(query, params, {
// Optionally customize the cache strategy for this request
hydrogen: {
cache: CacheShort(),
// Or disable caching for this request
// cache: CacheNone(),
// If you'd like to add a custom display title that will
// display in the Subrequest Profiler, you can pass that here:
// debug: {
// displayName: 'query Homepage'
// },
// You can also pass a function do determine whether or not to cache the response
// shouldCacheResult(value){
// return true
// },
},
// ...as well as other request options
// tag: 'home',
// headers: {
// 'Accept-Encoding': 'br, gzip, *',
// },
})
```
> [!TIP]
> Learn more about [request tagging](https://www.sanity.io/docs/reference-api-request-tags).
### Alternative: Direct queries using `fetch`
**Best for**: When preview mode is not needed and bundle optimization is priority
- Lightweight with direct client results
- No preview or loader integration
For Sanity queries that don't need loader integration, there is a `fetch` method that also integrates with Hydrogen's cache:
```ts
export async function loader({context, params}: LoaderFunctionArgs) {
const query = `*[_type == "page" && _id == $id][0]`
const params = {id: 'home'}
const page = await context.sanity.fetch<HomePage>(query, params, {
hydrogen: {
cache: CacheShort(),
debug: {displayName: 'fetch Homepage'},
},
tag: 'home',
})
return {page}
}
```
### Alternative: Using `client` directly
The Sanity client (either instantiated from your configuration or passed through directly) is also available in your app's context. It is recommended to use `query` for data fetching; but the Sanity client can be used for mutations within actions, for example:
```ts
export async function action({context, request}: ActionFunctionArgs) {
if (!isAuthenticated(request)) {
return redirect('/login')
}
return context.sanity
.withConfig({
token: context.env.SANITY_WRITE_TOKEN,
})
.client.create({
_type: 'comment',
text: request.body.get('text'),
})
}
```
### Using Sanity TypeGen
[Sanity TypeGen](https://www.sanity.io/docs/sanity-typegen) generates TypeScript definitions for your [GROQ](https://www.sanity.io/docs/groq) queries. To use TypeGen with `hydrogen-sanity`, install `groq` as a dependency:
```sh
npm install groq
```
> [!TIP]
> Refer to TypeGen steps covered in the [Sanity TypeGen documentation](https://www.sanity.io/docs/sanity-typegen). TypeGen with `overloadClientMethods: true` uses [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) to automatically generate types for your GROQ queries.
Now your queries will have automatic type inference:
```ts
import {defineQuery} from 'groq'
const HOMEPAGE_QUERY = defineQuery(`*[_id == "home"][0]`)
export async function loader({context, params}: LoaderFunctionArgs) {
const params = {id: 'home'}
const initial = await context.sanity.loadQuery(HOMEPAGE_QUERY, params)
return {initial}
}
```
## Working with images
The `useImageUrl` hook provides a convenient way to generate optimized image URLs from Sanity image assets with the [image URL builder](https://www.sanity.io/docs/image-url).
```tsx
import {useImageUrl} from 'hydrogen-sanity'
function HeroBanner({hero}: {hero: {image: SanityImageSource}}) {
const imageUrl = useImageUrl(hero.image)
return (
<div className="hero-banner">
<img
src={imageUrl.width(1200).height(600).format('auto').url()}
alt="Hero banner"
width={1200}
height={600}
/>
</div>
)
}
```
## Enable preview mode
Enabling preview mode provides real-time content editing with visual overlays inside the [Presentation tool](https://www.sanity.io/docs/presentation). `hydrogen-sanity` includes everything needed to make your storefront Presentation-aware.
> [!NOTE]
>
> These instructions assume some familiarity with Sanity's Visual Editing concepts, like loaders and overlays. To learn more, please visit the [Visual Editing documentation](https://www.sanity.io/docs/introduction-to-visual-editing).
### Configure preview mode
For Visual Editing to work, you need to configure Sanity preview mode in your context. Preview mode is a session-based state that gets activated when users visit your storefront through Sanity Studio's Presentation tool or through a preview link shared from Studio. First, initialize the preview session:
```diff
// ./lib/context.ts
// ...all other imports
import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
+ import {PreviewSession} from 'hydrogen-sanity/preview/session'
+ import {isPreviewEnabled} from 'hydrogen-sanity/preview'
export async function createHydrogenRouterContext(
request: Request,
env: Env,
executionContext: ExecutionContext,
) {
// ... Leave all other functions as-is
const waitUntil = executionContext.waitUntil.bind(executionContext)
- const [cache, session] = await Promise.all([
+ const [cache, session, previewSession] = await Promise.all([
caches.open('hydrogen'),
AppSession.init(request, [env.SESSION_SECRET]),
+ // Initialize the preview session
+ PreviewSession.init(request, [env.SESSION_SECRET]),
])
const sanity = await createSanityContext({
request,
cache,
waitUntil,
client: {
projectId: env.SANITY_PROJECT_ID,
dataset: env.SANITY_DATASET || 'production',
apiVersion: env.SANITY_API_VERSION || 'v2024-08-08',
useCdn: process.env.NODE_ENV === 'production',
+ // Enable stega encoding only when in preview mode
+ stega: {
+ enabled: isPreviewEnabled(env.SANITY_PROJECT_ID, previewSession),
+ },
},
+ // Preview configuration
+ preview: {
+ token: env.SANITY_PREVIEW_TOKEN,
+ session: previewSession,
+ },
})
}
```
> [!NOTE]
> **Stega encoding** enables click-to-edit functionality by encoding content source information directly into strings. Learn more about [Content Source Maps and stega](https://www.sanity.io/docs/visual-editing/stega).
### Add Visual Editing component
Set up your root route to enable Visual Editing across the entire application when preview mode is active:
```diff
// ./app/root.tsx
// ...other imports
+ import {usePreviewMode} from 'hydrogen-sanity/preview'
+ import {VisualEditing} from 'hydrogen-sanity/visual-editing'
export function Layout({children}: {children?: React.ReactNode}) {
const nonce = useNonce()
const data = useRouteLoaderData<RootLoader>('root')
+ const previewMode = usePreviewMode()
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{/* ...rest of the root layout */}
+ {/* Conditionally render `VisualEditing` component only when in preview mode */}
+ {previewMode ? <VisualEditing action="/api/preview" /> : null}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}
```
#### Visual Editing configuration options
The `VisualEditing` component provides flexible configuration for different data loading patterns:
**Server-Only Setup (default):**
Best when using only server-side data fetching (direct `fetch` or `loadQuery` without client components).
```tsx
<VisualEditing /> // Overlays only with server revalidation
```
**With Client-Side Loaders (recommended for `Query` component or `useQuery` hooks):**
The component automatically detects when you're using client-side loaders (`Query` components or `useQuery` hooks) and enables live mode accordingly:
```tsx
// Default usage - automatically enables live mode when needed
<VisualEditing action="/api/preview" />
```
> [!NOTE]
> **Automatic Detection**: Live mode automatically activates when:
>
> - `Query` components are rendered
> - `useQuery` hooks are called
#### Visual Editing components
For advanced use cases, you can use the individual components:
```tsx
import {Overlays, LiveMode} from 'hydrogen-sanity/visual-editing'
// Overlays only (server-only setups)
<Overlays action="/api/preview" />
// Live mode only (client-side data sync)
<LiveMode />
// Both (hybrid setups)
<Overlays action="/api/preview" />
<LiveMode />
```
This Visual Editing component provides a complete Visual Editing experience, including:
- **Context-aware behavior**: Auto-detects Studio vs standalone preview contexts
- **Real-time preview**: Updates content as you edit in Studio
- **Visual overlays**: Click-to-edit functionality with element highlighting
- **Perspective switching**: Draft/published content switching
- **Server revalidation**: Smart refresh logic for server-side data
- **Custom revalidation**: Customizable refresh logic for more control
### Preview mode route
For Sanity's Presentation tool to activate preview mode, you need to set up a route that handles authentication and session management. When users work in Sanity Studio's Presentation tool, it automatically calls this endpoint to enable preview mode.
`hydrogen-sanity` comes with a preconfigured route for this purpose. When Sanity's Presentation tool loads your storefront, it automatically makes requests to this route with a secret token. If the secret is valid, the route activates preview mode by writing the `projectId` to the preview session.
> [!NOTE]
>
> For Visual Editing overlays and click-to-edit functionality to work, you must configure `stega.enabled: true` in your Sanity client configuration.
>
> You can learn more about [Content Source Maps and working with stega-encoded strings](https://www.sanity.io/docs/stega).
Add this route to your project like below, or view the source to copy and modify it in your project.
```tsx
// ./app/routes/api.preview.ts
export {action, loader} from 'hydrogen-sanity/preview/route'
```
### Set up CORS for front-end domains
If your Sanity Studio is not embedded in your Hydrogen App, you will need to add a Cross-Origin Resource Sharing (CORS) origin to your project for every URL where your app is hosted or running in development.
Add `http://localhost:3000` to the CORS origins in your Sanity project settings at [sanity.io/manage](https://sanity.io/manage). Learn more about [CORS configuration in Sanity](https://www.sanity.io/docs/front-ends-and-cors).
### Modify storefront's Content Security Policy (CSP)
Since Sanity Studio's Presentation tool displays the storefront inside an iframe, you will need to adjust the Content Security Policy (CSP) in `entry.server.tsx`.
> [!TIP]
>
> Review Hydrogen's [content security policy documentation](https://shopify.dev/docs/storefronts/headless/hydrogen/content-security-policy) to ensure your storefront is secure.
```diff
// ./app/entry.server.tsx
// ...all other imports
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
- context: AppLoadContext,
+ context: HydrogenRouterContextProvider,
) {
+ const {env, sanity} = context
+ const projectId = env.SANITY_PROJECT_ID
+ const studioHostname = env.SANITY_STUDIO_HOSTNAME || 'http://localhost:3333'
+ const isPreviewEnabled = sanity.preview?.enabled
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
// If your storefront and Studio are on separate domains...
// ...allow Sanity assets loaded from the CDN to be loaded in your storefront
+ defaultSrc: ['https://cdn.sanity.io'],
// ...allow Studio to load your storefront in Presentation's iframe
+ frameAncestors: isPreviewEnabled ? [studioHostname] : [],
// If you've embedded your Studio in your storefront...
// ...allow Sanity assets to be loaded in your storefront and allow user avatars in Studio
+ defaultSrc: ['https://cdn.sanity.io', 'https://lh3.googleusercontent.com'],
// ...allow client-side requests for Studio to do realtime collaboration
+ connectSrc: [`https://${projectId}.api.sanity.io`, `wss://${projectId}.api.sanity.io`],
// ...allow embedded Studio to load storefront
+ frameAncestors: [`'self'`],
})
// ...and the rest
}
```
### Set up Presentation tool
Now in your Sanity Studio config, import the Presentation Tool with the preview URL set to the preview route you created.
> [!TIP]
>
> Consult the [Visual Editing documentation](https://www.sanity.io/docs/introduction-to-visual-editing) for how to [configure the Presentation tool](https://www.sanity.io/docs/configuring-the-presentation-tool).
```diff
// ./sanity.config.ts
// Add this import
+ import {presentationTool} from 'sanity/presentation'
export default defineConfig({
// ...all other settings
plugins: [
+ presentationTool({
+ previewUrl: {
+ // If you're hosting your storefront on a separate domain, you'll need to provide an `origin`:
+ // origin: process.env.SANITY_STUDIO_STOREFRONT_ORIGIN
+ previewMode: {
+ // This path is relative to the origin above and should match the route in your storefront that you've setup above
+ enable: '/api/preview',
+ },
+ },
+ }),
// ..all other plugins
],
})
```
You should now be able to view your Hydrogen app in the Presentation Tool, click to edit any Sanity content and see live updates as you make changes.
> [!NOTE]
>
> If you're able to see Presentation working locally, but not when you've deployed your application, check that your session cookie is using `sameSite: 'none'` and `secure: true`.
>
> Since Presentation displays your site in an iframe, the session cookie by default won't be sent through. You can learn more about session cookie configuation in [MDN's documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value).
### Troubleshooting
Are you getting the following error when trying to load your storefront in the Presentation Tool?
> Unable to connect to Visual Editing. Make sure you've setup '@sanity/visual-editing' correctly
Presentation will throw this error if it can't establish a connection to your storefront. Here are a few things to double-check in your setup to try and troubleshoot:
1. Confirm that you've provided `preview` configuration to the Sanity context.
2. Confirm that you've added the `VisualEditing` component to your root layout.
3. If you've followed the instructions above, the `VisualEditing` component will be conditionally rendered if the app has been successfully put into preview mode.
4. If you're using a session cookie, check your browser devtools and confirm that the cookie has been set as expected.
5. Since Presentation loads your storefront in an `iframe`, double check your cookie and CSP configuration.
## Using `@sanity/client` directly
If you prefer not to use `hydrogen-sanity`, you can configure [`@sanity/client`](https://www.sanity.io/docs/js-client) directly in your Hydrogen storefront.
The following example configures Sanity Client and provides it in the request context.
```diff
// ./lib/context.ts
// ...all other imports
+ import {createClient} from '@sanity/client'
+ import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
// Define the additional context object
const additionalContext = {
// Additional context for custom properties, CMS clients, 3P SDKs, etc.
// These will be available as both context.propertyName and context.get(propertyContext)
// Example of complex objects that could be added:
// cms: await createCMSClient(env),
// reviews: await createReviewsClient(env),
} as const;
// Automatically augment HydrogenAdditionalContext with the additional context type
type AdditionalContextType = typeof additionalContext;
declare global {
interface HydrogenAdditionalContext extends AdditionalContextType {
+
+ // Augment `HydrogenAdditionalContext` with the Sanity context
+ sanity: SanityContext;
+ withCache: WithCache;
}
}
export async function createHydrogenRouterContext(
request: Request,
env: Env,
executionContext: ExecutionContext,
) {
// ... all other functions
+ const withCache = createWithCache({cache, waitUntil, request})
+ // Create the Sanity Client
+ const sanity = createClient({
+ projectId: env.SANITY_PROJECT_ID,
+ dataset: env.SANITY_DATASET,
+ apiVersion: env.SANITY_API_VERSION ?? 'v2025-02-19',
+ useCdn: process.env.NODE_ENV === 'production',
+ })
const hydrogenContext = createHydrogenContext(
{
env,
request,
cache,
waitUntil,
session,
i18n: {language: 'EN', country: 'US'},
cart: {
queryFragment: CART_QUERY_FRAGMENT,
},
},
+ {
+ ...additionalContext,
+ sanity,
+ withCache,
+ } as const,
+ )
+
+ return hydrogenContext
}
```
Then, in your loaders and actions you'll have access to Sanity Client in context:
```ts
export async function loader({context, params}: LoaderArgs) {
const {sanity} = context
const homepage = await sanity.fetch(`*[_type == "page" && _id == $id][0]`, {id: 'home'})
return {homepage}
}
```
To cache your query responses in Hydrogen, use the [`withCache` utility](https://shopify.dev/docs/custom-storefronts/hydrogen/caching/third-party#hydrogen-s-built-in-withcache-utility):
```ts
export async function loader({context, params}: LoaderArgs) {
const {withCache, sanity} = context
const homepage = await withCache('home', CacheLong(), () =>
sanity.fetch(`*[_type == "page" && _id == $id][0]`, {id: 'home'}),
)
return {homepage}
}
```
## Migration Guides
- [From `v3` to `v4`](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v3-to-v4.md)
- [From `v4` to `v5`](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v4-to-v5.md)
## License
[MIT](LICENSE) © Sanity.io <hello@sanity.io>
## Develop & test
This plugin uses [@sanity/pkg-utils](https://github.com/sanity-io/pkg-utils)
with default configuration for build & watch scripts.
### Release new version
Run ["CI & Release" workflow](https://github.com/sanity-io/hydrogen-sanity/actions/workflows/main.yml).
Make sure to select the main branch and check "Release new version".
Semantic release will only release on configured branches, so it is safe to run release on any branch.