Sanity.io toolkit for Hydrogen. Requires @shopify/hydrogen >= 2023.7.0
.
- Installation
- Usage
- Interacting with Sanity data
- Enable Visual Editing
- Using
@sanity/client
instead ofhydrogen-sanity
- Migration Guides
- License
- Develop & test
Features:
- Cacheable queries to Sanity API CDN
- Interactive live preview with Visual Editing
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.
Note
Using this package isn't strictly required for working with Sanity in a Hydrogen storefront. If you'd like to use @sanity/react-loader
and/or @sanity/client
directly, see Using @sanity/client
directly below.
npm install hydrogen-sanity
yarn add hydrogen-sanity
pnpm install hydrogen-sanity
Update the server file to include the Sanity Loader, 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/[email protected]
// ./lib/context.ts
// ...all other imports
import {createSanityContext} from 'hydrogen-sanity';
export async function createAppLoadContext(
request: Request,
env: Env,
executionContext: ExecutionContext,
) {
// ... Leave all other functions like the Hydrogen context as-is
const waitUntil = executionContext.waitUntil.bind(executionContext);
const [cache, session] = await Promise.all([
caches.open('hydrogen'),
AppSession.init(request, [env.SESSION_SECRET]),
]);
// 1. Configure the Sanity Loader and preview mode
const sanity = 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',
// In preview mode, `stega` will be enabled automatically
// If you need to configure the client's steganography settings,
// you can do so here
// stega: {
// logger: console
// }
}),
// 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 || '2023-03-30',
// useCdn: process.env.NODE_ENV === 'production',
// }),
// Optionally, set a default cache strategy, defaults to `CacheLong`
// strategy: CacheShort() | null,
// Optionally, enable Visual Editing
// See "Visual Editing" section below to setup the preview route
// preview: env.SANITY_API_TOKEN
// ? {
// enabled: session.get('projectId') === env.SANITY_PROJECT_ID,
// token: env.SANITY_API_TOKEN,
// studioUrl: 'http://localhost:3333',
// }
// : undefined,
})
// 2. Make Sanity available to loaders and actions in the request context
return {
...hydrogenContext,
sanity,
};
}
Update your environment variables with settings from your Sanity project.
- Copy these from sanity.io/manage
- or run
npx sanity@latest init --env
to fill the minimum required values from a new or existing project
# 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_API_TOKEN=""
Update the environment variables in Env
to include the ones you created above:
// ./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_API_TOKEN: string
}
}
Query Sanity's API and use Hydrogen's cache to store the response (defaults to CacheLong
caching strategy). While in preview mode, loadQuery
will use CacheNone
to prevent results from being cached.
Tip
You can use Sanity TypeGen tooling to generate TypeScript definitions for your GROQ queries.
Learn more about configuring caching in Hydrogen on the Shopify documentation.
Sanity queries will appear in Hydrogen's Subrequest Profiler. By default, they're titled Sanity query
; however, you can give your queries more descriptive titles by using the request option below.
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 json({initial})
}
If you need to pass any additional options to the request provide queryOptions
like so:
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'
// }
},
// ...as well as other request options
// tag: 'home',
// headers: {
// 'Accept-Encoding': 'br, gzip, *',
// },
})
Tip
You can learn more about request tagging in the documentation.
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 loadQuery
for data fetching; but the Sanity client can be used for mutations within actions, for example:
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'),
})
}
Enable real-time, interactive live preview inside the Presentation tool of your Sanity Studio. hydrogen-sanity
comes with a ready-to-use version of the VisualEditing
component that's compatible with Hydrogen and Oxygen.
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.
First set up your root route to enable preview mode across the entire application, if the preview session is active:
// ./app/root.tsx
// ...other imports
import {VisualEditing} from 'hydrogen-sanity/visual-editing'
export async function loader({context}: LoaderArgs) {
return json({
// ... other loader data
isPreviewEnabled: context.sanity.preview?.enabled,
})
}
export function Layout({children}: {children?: React.ReactNode}) {
const nonce = useNonce()
const data = useRouteLoaderData<RootLoader>('root')
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 */}
{data?.isPreviewEnabled ? <VisualEditing /> : null}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}
This Visual Editing component will trigger incremental updates to draft documents from the server for users with a valid preview session. Duplicate its source into your own project if you wish to customize its behavior.
For users to enter preview mode, they will need to visit a route that performs some authentication and then writes to the session.
hydrogen-sanity
comes with a preconfigured route for this purpose. It checks the value of a secret in the URL used by Presentation tool - and if valid - writes the projectId
to the Hydrogen session.
Note
By default, hydrogen-sanity
will enable stega-encoded Content Source Maps when preview mode is enabled.
You can learn more about Content Source Maps and working with stega-encoded strings in the documentation.
Add this route to your project like below, or view the source to copy and modify it in your project.
// ./app/routes/api.preview.ts
export {loader} from 'hydrogen-sanity/preview/route'
// Optionally, export the supplied action which will disable preview mode when POSTed to
// export {action, loader} from 'hydrogen-sanity/preview/route'
If your Sanity Studio is not embedded in your Hydrogen App, you will need to add a 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.
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 to ensure your storefront is secure.
// ./app/entry.server.tsx
// ...all other imports
import type {AppLoadContext, EntryContext} from '@shopify/remix-oxygen'
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
context: AppLoadContext,
) {
const projectId = context.env.SANITY_PROJECT_ID
const studioHostname = context.env.SANITY_STUDIO_HOSTNAME || 'http://localhost:3333'
const isPreviewEnabled = context.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] : undefined,
// 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
}
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 for how to configure the Presentation tool.
// ./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.
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:
- Confirm that you've provided
preview
configuration to the Sanity context. - Confirm that you've added the
VisualEditing
component to your root layout. - If you've followed the instructions above, the
VisualEditing
component will be conditionally rendered if the app has been successfully put into preview mode. - If you're using a session cookie, check your browser devtools and confirm that the cookie has been set as expected.
- Since Presentation loads your storefront in an
iframe
, double check your cookie and CSP configuration.
For whatever reason, if you choose not to use hydrogen-sanity
you could still configure @sanity/react-loader
or @sanity/client
to get Sanity content into your Hydrogen storefront.
The following example configures Sanity Client and provides it in the request context.
// ./server.ts
// ...all other imports
import {createClient} from '@sanity/client'
export async function createAppLoadContext(
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 ?? 'v2024-08-08',
useCdn: process.env.NODE_ENV === 'production',
})
// Pass it along to every request by
// adding it to `handleRequest`
return {
...hydrogenContext,
sanity,
withCache,
}
}
Then, in your loaders and actions you'll have access to Sanity Client in context:
export async function loader({context, params}: LoaderArgs) {
const {sanity} = context
const homepage = await sanity.fetch(`*[_type == "page" && _id == $id][0]`, {id: 'home'})
return json({homepage})
}
If you want to cache your query responses in Hydrogen, you can use the withCache
utility:
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 json({homepage})
}
MIT © Sanity.io [email protected]
This plugin uses @sanity/pkg-utils with default configuration for build & watch scripts.
Run "CI & Release" workflow. 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.