Watch a live product demo đź‘€ See how Sanity powers richer commerce experiences
November 22, 2021 (Updated February 08, 2023)

How to build a Remix website with Sanity.io and live preview

By Simeon Griggs

To give your content creators the best possible experience, let them see what their content looks like before they press publish. In this guide, you’ll add @sanity/preview-kit to a Remix application and make that possible.

Protip

The final code created in this Guide is available as a repository on GitHub.

Notes on this guide

Following this guide, you'll create a new Remix application and a new Sanity project with a preconfigured Sanity Studio. You may be able to follow along with an existing project to add this functionality.

  • Looking for a complete example project? This complete Remix and Sanity template can be installed from the command line and is fully configured with an embedded Sanity Studio.
  • TypeScript is optional. All the code examples here are authored in TypeScript, but using it is not necessary for this functionality. You can still use JavaScript but may need to remove the typings from the example code.

Assumptions

  • You already have an account with Sanity
  • You’re somewhat familiar with Sanity Studio and Remix
  • You’re comfortable JavaScript, React, and the command line.

Create a new Remix application

Using the below, you can initialize a new Remix application from the command line and follow the prompts. This command will install Remix in a new directory, prefer TypeScript and install dependencies.

# from the command line
npx create-remix@latest remix-live-preview --typescript --install --template remix

# enter the Remix application's directory
cd remix-live-preview

# run the development server
npm run dev

If you’re stuck with the installation process, see the Remix documentation for further instructions on how to get started.

Visit http://localhost:3000 in your web browser, and you should see this landing screen to show it’s been installed correctly.

A new Remix application as seen in the browser

Install Tailwind Play CDN

To make styling simpler, we’ll install Tailwind CSS’s Play CDN to the Remix application.

Open root.tsx and insert this script tag inside the head tag.

// ./app/root.tsx

// ... all other code

<head>
  <Meta />
  <Links />
  {/* 👇 add this line */}
<script src="https://cdn.tailwindcss.com?plugins=typography" />
</head>

While this installs Tailwind CSS quickly, it’s not recommended for production. If you end up deploying this app, follow the guide on their documentation to install it properly.

To test that this is working, replace the contents of app/routes/index.tsx with the code below:

// ./app/routes/index.ts

export default function Index() {
  return (
    <main className="flex items-center justify-center min-h-screen">
      Soon you'll replace me with Sanity Content
    </main>
  )
}

Your localhost:3000 should now look like this:

If your Remix application looks like this, Tailwind CSS from the CDN is working!

Create a new Sanity Studio project

Next, you’ll create a new Sanity Studio for a new Sanity project in its own folder. Using the preconfigured schema from the blog template.

# from the command line
npm create sanity@latest -- --template blog --create-project "Sanity Live Preview" --dataset production

# follow prompts during install
# this tutorial uses TypeScript

# enter the new project's directory
cd sanity-live-preview

# run the development server
npm run dev

For more complete instructions and troubleshooting, our documentation covers how to create a Sanity project.

Open localhost:3333 in your browser, and you should see a new Studio with the Blog template schema already created.

There are currently no documents, though; try creating and publishing a few new post type documents.

Create and publish some content to load into the Remix application

Before we go further…

You should now have two folders:

/remix-live-preview  -> contains our Remix application
/sanity-live-preview -> contains our Sanity Studio

From which you can separately run both application’s development servers:

Linking Remix and Sanity

To query and display Sanity content inside the Remix application, you’ll need to install a few packages first.

# in /remix-live-preview
npm install @sanity/client @sanity/preview-kit @sanity/image-url @portabletext/react

This command installs:

  • @sanity/client: A package to simplify querying from (and sending mutations to) the Content Lake
  • @sanity/preview-kit: A collection of helpful functions to make live preview simple from inside a React application
  • @sanity/image-url: A library for creating complete image URLs from just the ID of a Sanity image asset.
  • @portabletext/react: Renders block content from a Portable Text field in the Studio with props to add configuration options.

With these packages installed, you’ll now need to create a new file to setup your Sanity Client:

// ./app/lib/sanity.ts

import { createClient } from "@sanity/client";
import { definePreview } from "@sanity/preview-kit";

// copy these from your Studio's sanity.config.ts
export const projectId = "";
export const dataset = "production";
export const apiVersion = "2023-01-01";

export const client = createClient({ projectId, dataset, apiVersion, useCdn: true });
export const usePreview = definePreview({ projectId, dataset });

Protip

Why not use Environment Variables?

In other guides, you may use a .env file to load the Project ID and Dataset name, but this is mainly a convenience to store global values or change them between deployment targets. None of these values above are considered sensitive and are safe to be stored publicly. If later you need a token so that your Sanity Client can read private content or write mutations, you should set up environment variables as recommended in the Remix documentation

Fetching data from Sanity

You’ll now confirm that you can query published documents from Sanity first before setting up live preview to display draft content.

Change the index route to include a loader that will use the Sanity Client to query for all post documents with a slug.

// ./app/routes/index.tsx

import { useLoaderData } from "@remix-run/react";
import Posts from "~/components/Posts";

import { client } from "~/lib/sanity";

export const loader = async () => {
  const query = `*[_type == "post" && defined(slug.current)]`;
  const posts = await client.fetch(query);

  return { posts };
};

export default function Index() {
  const { posts } = useLoaderData();

  return <Posts posts={posts} />;
}

Also, create this new component to display the returned data.

// ./app/components/Posts.tsx

import { Link } from "@remix-run/react";
import type { SanityDocument } from "@sanity/client";

export default function Posts({ posts }: { posts: SanityDocument[] }) {
  return (
    <main className="container mx-auto grid grid-cols-1 divide-y divide-blue-100">
      {posts?.length > 0 ? (
        posts.map((post) => (
          <Link
            key={post._id}
            to={post.slug.current}
            className="p-4 hover:bg-blue-50"
          >
            <h2>{post.title}</h2>
          </Link>
        ))
      ) : (
        <div className="p-4 text-red-500">No posts found</div>
      )}
    </main>
  );
}

Your home page at localhost:3000 should now look like the image below. If not, your Studio likely has no posts yet! Open up your Sanity Studio, create and publish a few new post type documents and refresh your Remix application.

The Remix Application is now querying our Sanity data

You’ll get a "404 Not Found" page if you click one of these links. To fix this, you’ll need to create another route:

// ./app/routes/$slug.tsx

import type { LoaderArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import Post from "~/components/Post";
import { client } from "~/lib/sanity";

export const loader = async ({params}: LoaderArgs) => {
  const query = `*[_type == "post" && slug.current == $slug][0]`;
  const post = await client.fetch(query, params);

  return { post };
};

export default function PostRoute() {
  const { post } = useLoaderData();

  return <Post post={post} />;
}

As well as a new Post component to display the data.

// ./components/Post.tsx

import { PortableText } from "@portabletext/react";
import imageUrlBuilder from "@sanity/image-url";
import type { SanityDocument } from "@sanity/client";
import { projectId, dataset } from "~/lib/sanity";

const builder = imageUrlBuilder({ projectId, dataset });

export default function Post({ post }: { post: SanityDocument }) {
  const { title, mainImage, body } = post;

  return (
    <main className="container mx-auto prose prose-lg p-4">
      {title ? <h1>{title}</h1> : null}
      {mainImage ? (
        <img
          className="float-left m-0 w-1/3 mr-4 rounded-lg"
          src={builder.image(mainImage).width(300).height(300).quality(80).url()}
          width={300}
          height={300}
          alt={title}
        />
      ) : null}
      {body ? <PortableText value={body} /> : null}
    </main>
  );
}

Notice how the code in this component checks first if a value exists before displaying any element? This is necessary when working later with live preview, where you cannot guarantee the existence of any value.

Now when you click a link on the home page, you should be taken to a page just like this:

Every published post document with a slug can now display as a unique page

What have we achieved so far?

You now have successfully:

  • Created a new Sanity Studio with some placeholder content
  • A Remix application with a home page that lists published blog posts with links to a unique page for each post – displaying rich text and an image.

The next step is to make the Remix application query for drafts and preview content before it is published!

Setup preview-kit

Add a CORS origin for the Remix application

Because our Remix application will make authenticated requests to the Sanity Project, its URL will need to be added as a valid CORS origin.

This can be done inside sanity.io/manage; navigate to the API tab and enter http://localhost:3000

Ensure you check ”Allow credentials.”

Important:

  • Only setup CORS origins for URLs where you control the code.
  • You will need to repeat this when you deploy your application to a hosting provider with its URL.

If you see the error “Failed to fetch” from your front end while setting up Live Preview, likely, this step wasn’t performed.

Add a new CORS origin for everywhere Sanity content will be queried with authentication

Create a session cookie in Remix

For the best user experience, you’ll want to ensure that anyone browsing the Remix application in “preview mode” stays in that mode as they navigate between pages. For this, we’ll create a session cookie. For more details, see the Remix documentation.

Create this new file:

// ./app/sessions.tsx

import {createCookieSessionStorage} from '@remix-run/node'

const {getSession, commitSession, destroySession} = createCookieSessionStorage({
  cookie: {
    name: '__session',
    sameSite: 'lax',
    secrets: [],
  },
})

export {getSession, commitSession, destroySession}

The session cookie will be written by accessing a specific route in the application. The example below will put the user into “preview mode” by writing a session and then redirecting them to the home page.

While there’s no authentication layer here, only a user that is also logged into the Sanity Studio will see draft content.

Add this extra route:

// ./app/routes/resource/preview.tsx

import type {ActionFunction, LoaderArgs} from '@remix-run/node'
import {json, redirect} from '@remix-run/node'
import {getSession, commitSession, destroySession} from '~/sessions'

// A `POST` request to this route will exit preview mode
export const action: ActionFunction = async ({request}) => {
  if (request.method !== 'POST') {
    return json({message: 'Method not allowed'}, 405)
  }

  const session = await getSession(request.headers.get('Cookie'))

  return redirect('/', {
    headers: {
      'Set-Cookie': await destroySession(session),
    },
  })
}

// A `GET` request to this route will enter preview mode
export const loader = async ({request}: LoaderArgs) => {
  const session = await getSession(request.headers.get('Cookie'))
  // For a more advanced use case, you could use this 
  // to store a read token from sanity.io/manage
  session.set(`preview`, `a-random-string`)

  return redirect(`/`, {
    headers: {
      'Set-Cookie': await commitSession(session),
    },
  })
}

Lastly, you’ll also want to give users an easy way to exit preview mode. For this, create a new component that will make a POST request to that same preview route, clearing the session cookie:

// ./app/components/ExitPreview.tsx

export default function ExitPreview() {
    return (
      <div className="pointer-events-none fixed inset-0 flex h-screen w-screen items-end justify-center">
        <form className="pointer-events-auto" action="/resource/preview" method="POST">
          <button className="bg-black p-4 font-bold text-white" type="submit">
            Exit Preview Mode
          </button>
        </form>
      </div>
    )
  }

Let’s step back and look at how this all works:

  1. Visitors to /resource/preview will receive a “session cookie” and be redirected to the home page.
  2. Any route's loader function can be configured to check for this cookie and, if found, will return the GROQ query and params to the browser. If the cookie is not found, published content will be fetched server-side.
  3. A user in preview mode will see a brief “Loading…” screen while a fetch is made for draft content – if the user is also currently logged into Sanity Studio – and watch any further changes made in real time.

So to make this work, you’ll need to update your routes to check for this cookie and return a different component if found.

First, update the entire index route:

// ./app/routes/index.tsx

import type { LoaderArgs, LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import type { SanityDocument } from "@sanity/client";
import { PreviewSuspense } from "@sanity/preview-kit";
import { lazy } from "react";
import Posts from "~/components/Posts";
import { client } from "~/lib/sanity";
import { getSession } from "~/sessions";

const PostsPreview = lazy(() => import("../components/PostsPreview"));

export const loader: LoaderFunction = async ({request}: LoaderArgs) => {
  const query = `*[_type == "post" && defined(slug.current)]`;
  const session = await getSession(request.headers.get('Cookie'))
  const preview = session.get('preview')

  // Preview session cookie found, return early and query client-side!
  if (preview) {
    return { preview: true, query };
  }
  
  const posts: SanityDocument[] = await client.fetch(query);

  return { preview: false, posts };
};

export default function PostRoute() {
  const { preview, query, posts } = useLoaderData();

  return preview ? (
    <PreviewSuspense fallback="Loading...">
      <PostsPreview query={query} />
    </PreviewSuspense>
  ) : (
    <Posts posts={posts} />
  );
}

Then create a new component PostsPreview, to perform the client-side query:

// ./app/components/PostsPreview.tsx

import { usePreview } from "~/lib/sanity";
import Posts from "~/components/Posts";
import ExitPreview from "~/components/ExitPreview";

export default function PostsPreview({ query }: { query: string }) {
  const posts = usePreview(null, query);

  return (
    <>
      <Posts posts={posts} />
      <ExitPreview />
    </>
  );
}

Now if you visit the home page, nothing will have changed. However, if you visit /resource/preview you’ll be redirected to the home page and see the “Exit Preview Mode” button.

You’ll also see draft versions of any of the post titles. Try modifying one in the Studio!

With the Studio and Remix side-by-side, you can see draft content in preview mode

Clicking any of these links will still take you to the published version of the content. The $slug.tsx route needs replacing to enable preview mode:

// ./app/routes/$slug.tsx

import type { LoaderArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import type { SanityDocument } from "@sanity/client";
import { PreviewSuspense } from "@sanity/preview-kit";
import Post from "~/components/Post";
import PostPreview from "~/components/PostPreview";
import { client } from "~/lib/sanity";
import { getSession } from "~/sessions";

export const loader = async ({ params, request }: LoaderArgs) => {
  const query = `*[_type == "post" && slug.current == $slug][0]`;
  const session = await getSession(request.headers.get('Cookie'))
  const preview = session.get('preview')

  if (preview) {
    return {
      preview: true,
      query,
      params,
    };
  }

  const post: SanityDocument[] = await client.fetch(query, params);

  return { preview: false, post };
};

export default function PostRoute() {
  const { preview, query, params, post } = useLoaderData();

  return preview ? (
    <PreviewSuspense fallback="Loading...">
      <PostPreview query={query} params={params} />
    </PreviewSuspense>
  ) : (
    <Post post={post} />
  );
}

As well as a new component to display a single updated Post in preview mode:

// ./app/components/PostPreview.tsx

import type { QueryParams } from "@sanity/client";
import { usePreview } from "~/lib/sanity";
import Post from "~/components/Post";
import ExitPreview from "~/components/ExitPreview";

export default function PostPreview({
  query,
  params,
}: {
  query: string;
  params: QueryParams;
}) {
  const post = usePreview(null, query, params);

  return (
    <>
      <Post post={post} />
      <ExitPreview />
    </>
  );
}

Now when you're in preview mode, you should be able to navigate through the site, always seeing the latest draft version of any post type document.

Draft content can also be displayed on individual post pages

Put the Remix preview inside the Studio

You can preview draft content on the website, but that’s only convenient if you have two separate browser windows open.

To give your content creators a better experience, you can embed the website inside a View Pane in the Studio.

Install the Iframe Pane plugin package into your Sanity Studio project:

npm install sanity-plugin-iframe-pane

Currently, your Studio is using the default settings for the Desk Tool plugin. To display the Iframe Pane by customising the defaultDocumentNode in the Desk Tool’s configuration.

Create a new file in your Studio:

// ./src/defaultDocumentNode.ts

import {DefaultDocumentNodeResolver} from 'sanity/desk'
import Iframe from 'sanity-plugin-iframe-pane'

export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
  switch (schemaType) {
    case `post`:
      return S.document().views([
        S.view.form(),
        S.view
          .component(Iframe)
          .options({
            url: `http://localhost:3000/resource/preview`,
          })
          .title('Preview'),
      ])
    default:
      return S.document().views([S.view.form()])
  }
}

Next, import this into the sanity.config.ts and load it into the Desk Tool’s configuration:

// ./sanity.config.ts

// ...other imports
import { defaultDocumentNode } from './src/defaultDocumentNode'
export default defineConfig({ // ...other config settings plugins: [
deskTool({ defaultDocumentNode }),
// ...other plugins ], })

Now with any Post document open, you can place the Remix application side-by-side with the document editor and see content changes as you write them.

With the Iframe Pane plugin installed, the Remix application is displayed inside the Studio

In summary, an excellent user experience for your content creators as they can now publish with complete confidence!

Where to next?

Some ideas to improve and extend the experience.

  • Make the preview route redirect to the current document by appending a ?slug= param.
  • Secure the preview route, making it check if the slug currently exists in the dataset.
  • Write a view-only token to the cookie so guests not logged into the Sanity Studio can preview content on the website.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is a platform that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when the need arises. With the hosted document store you query content freely, and easily integrate with any framework or data source distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

→ Watch a demo

Other guides by author

Create richer array item previews

Object types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.

Simeon Griggs
Go to Create richer array item previews