Fetching Sanity content
The hydrogen-sanity package contains a number of useful functions to simplify querying and displaying content from Sanity.
hydrogen-sanity and other packages# in the /web directorynpm install hydrogen-sanity @sanity/client @portabletext/react- hydrogen-sanity is a multi-purpose toolkit for integrating Sanity with Hydrogen
- @sanity/client is the primary way for JavaScript applications to interact with Sanity content and API's
- @portabletext/react is a React component for rendering block content which is authored in Sanity Studio
You may encounter a Vite error in the next few steps, to get ahead of that we'll add the following to our vite.config.ts file:
import {defineConfig} from 'vite'import {hydrogen} from '@shopify/hydrogen/vite'import {sanity} from 'hydrogen-sanity/vite'
export default defineConfig({ plugins: [hydrogen(), sanity() /** ... */], // ... other config})The Sanity Client allows you to query (and mutate) content.
You’ll need a Sanity Client configured with your Project ID and dataset name available to any route.
Add your Sanity project details to the Hydrogen project's .env file. You can find these either in sanity.io/manage or in the sanity.config.ts file of your Studio.
web/.env file to include Sanity project details.# Project IDSANITY_PROJECT_ID=""# Dataset nameSANITY_DATASET=""# (Optional) Sanity API versionSANITY_API_VERSION=""// ...all other type settings
declare global { interface Env extends HydrogenEnv { SANITY_PROJECT_ID: string; SANITY_DATASET: string; SANITY_API_VERSION: string; SANITY_API_TOKEN: string; }}The code example below contains new lines to paste into your Hydrogen project’s context file, this was previously handled in server.ts.
web/lib/context.ts and update it to include Sanity Clientimport {createHydrogenContext} from '@shopify/hydrogen';import {createSanityContext, type SanityContext} from 'hydrogen-sanity';import {AppSession} from '~/lib/session';import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
// ...other types and imports
declare global { interface HydrogenAdditionalContext extends AdditionalContextType { // Augment `HydrogenAdditionalContext` with the Sanity context sanity: SanityContext; }}
/** * Creates Hydrogen context for React Router 7.9.x * Returns HydrogenRouterContextProvider with hybrid access patterns * */export async function createHydrogenRouterContext( request: Request, env: Env, executionContext: ExecutionContext,) { // ...other functions
// 1. Add `sanity` configuration 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 || 'v2025-11-01', useCdn: process.env.NODE_ENV === 'production', }, });
// 2. Make `sanity` available to loaders and actions in the request context const hydrogenContext = createHydrogenContext( { env, request, cache, waitUntil, session, // Or detect from URL path based on locale subpath, cookies, or any other strategy i18n: {language: 'EN', country: 'US'}, cart: { queryFragment: CART_QUERY_FRAGMENT, }, }, {...additionalContext, sanity}, );
return hydrogenContext;}Now a configured Sanity React Loader is available inside the Hydrogen “context” as sanity, just like the preconfigured storefront.
You'll use loadQuery to retrieve content from Sanity. It's configured in a way to make setting up Visual Editing simple. For now, you'll just use the initial data in the default export to render content.
import {Link, useLoaderData, type LoaderFunctionArgs} from 'react-router';import {type Product} from '@shopify/hydrogen/storefront-api-types';import {PortableText, type PortableTextBlock} from '@portabletext/react';
export async function loader({ params, context: {storefront, sanity},}: LoaderFunctionArgs) { const {product} = await storefront.query<{product: Product}>( `#graphql query Product($handle: String!) { product(handle: $handle) { id title } } `, {variables: params}, );
const PRODUCT_QUERY = `*[_type == "product" && store.slug.current == $handle][0]{ body, "image": store.previewImageUrl }`;
const initial = await sanity.fetch<{ body: PortableTextBlock[] | null; image: string | null; } | null>(PRODUCT_QUERY, params, { tag: 'homepage', hydrogen: {debug: {displayName: 'query Homepage'}}, });
if (!initial) { throw new Response('Product not found', {status: 404}); }
return {product, initial};}
export default function Product() { const {product, initial} = useLoaderData<typeof loader>();
return ( <div className="mx-auto p-12 grid grid-cols-1 gap-4"> <h1 className="text-3xl font-bold">{product.title}</h1> {initial?.image ? ( <img alt={product.title} src={initial.image} className="size-32 mb-6 mr-6 object-cover float-left rounded-xl" /> ) : null} {Array.isArray(initial?.body) ? ( <PortableText value={initial.body} /> ) : null} <Link className="text-blue-500" to="/collections/all"> ← Back to All Products </Link> </div> );}Visit any product page now, and you should see both the product title from Shopify and the contents of the Portable Text field in the Sanity document for that same product.
If there's no extra text showing, edit the LULU Mini Pot product in your Studio and add the following text:
All of our products are made from 100% recycled materials and are finished with a waterproof sealant. We recommend cleaning your LULU Pot regularly with hot soapy water. Please do not leave the pot to soak. Alternatively our products are dishwasher safe.Your page should now look like this.
If your CSS looks slightly off, try emptying the contents of reset.css as it conflicts with some Tailwind Typography styles.
Your Hydrogen app now queries content from both Sanity and Shopify independently, displaying in a consistent front-end.
Sanity content is more than just a paragraph of text – let’s embellish this with some rich content blocks!