UNPKG

hydrogen-sanity

Version:
510 lines (386 loc) 18 kB
# hydrogen-sanity [Sanity.io](https://www.sanity.io) toolkit for [Hydrogen](https://hydrogen.shopify.dev/). Requires `@shopify/hydrogen >= 2023.7.0`. - [Installation](#installation) - [Usage](#usage) - [Satisfy TypeScript](#satisfy-typescript) - [Interacting with Sanity data](#interacting-with-sanity-data) - [Preferred: Cached queries using `loadQuery`](#preferred-cached-queries-using-loadquery) - [Additional `loadQuery` options](#additional-loadquery-options) - [Alternatively: Using `client` directly](#alternatively-using-client-directly) - [Enable Visual Editing](#enable-visual-editing) - [Enabling preview mode](#enabling-preview-mode) - [Setup CORS for front-end domains](#setup-cors-for-front-end-domains) - [Modify storefront's Content Security Policy (CSP)](#modify-storefronts-content-security-policy-csp) - [Setup Presentation tool](#setup-presentation-tool) - [Troubleshooting](#troubleshooting) - [Using `@sanity/client` instead of `hydrogen-sanity`](#using-sanityclient-instead-of-hydrogen-sanity) - [Migration Guides](#migration-guides) - [License](#license) - [Develop \& test](#develop--test) - [Release new version](#release-new-version) **Features:** - Cacheable queries to [Sanity API CDN](https://www.sanity.io/docs/api-cdn) - Interactive live preview with [Visual Editing](https://www.sanity.io/docs/loaders-and-overlays) > [!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). > [!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](#using-sanityclient-directly) below. ## Installation ```sh npm install hydrogen-sanity ``` ```sh yarn add hydrogen-sanity ``` ```sh 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/hydrogen@2024.7.9` ```ts // ./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](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_API_TOKEN="" ``` ### Satisfy TypeScript Update the environment variables in `Env` to include the ones you created above: ```ts // ./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](https://www.sanity.io/docs/sanity-typegen) to generate TypeScript definitions for your GROQ queries. Learn more about configuring [caching in Hydrogen on the Shopify documentation](https://shopify.dev/docs/custom-storefronts/hydrogen/caching). Sanity queries will appear in Hydrogen's [Subrequest Profiler](https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler). By default, they're titled `Sanity query`; however, 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 json({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] > You can learn more about request tagging in [the documentation](https://www.sanity.io/docs/reference-api-request-tags). ### 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: ```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'), }) } ``` ## 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](https://www.sanity.io/docs/introduction-to-visual-editing). First set up your root route to enable preview mode across the entire application, if the preview session is active: ```tsx // ./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](https://github.com/sanity-io/visual-editing/blob/main/packages/visual-editing/src/remix/VisualEditing.tsx) 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](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 {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](https://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](https://shopify.dev/docs/storefronts/headless/hydrogen/content-security-policy) to ensure your storefront is secure. ```ts // ./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](https://www.sanity.io/docs/configuring-the-presentation-tool). ```ts // ./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` 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. ```ts // ./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: ```ts 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](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 json({homepage}) } ``` ## Migration Guides - [From `v3` to `v4`](https://github.com/sanity-io/hydrogen-sanity/blob/main/package/MIGRATE-v3-to-v4.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.