Skip to content

sanity-io/hydrogen-sanity

Repository files navigation

hydrogen-sanity

Sanity.io toolkit for Hydrogen. Requires @shopify/hydrogen >= 2023.7.0.

Features:

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.

Installation

npm install hydrogen-sanity
yarn add hydrogen-sanity
pnpm install hydrogen-sanity

Usage

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=""

Satisfy TypeScript

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
  }
}

Interacting with Sanity data

Preferred: Cached queries using loadQuery

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})
}

Additional loadQuery options

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.

Alternatively: 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 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 Visual Editing

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.

Enabling preview mode

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'

Setup CORS for front-end domains

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.

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 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
}

Setup 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 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.

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 instead of hydrogen-sanity

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})
}

Migration Guides

License

MIT © Sanity.io [email protected]

Develop & test

This plugin uses @sanity/pkg-utils with default configuration for build & watch scripts.

Release new version

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.