Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive

Visual Editing with Remix and Sanity Studio

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 setup Presentation in the Studio to get interactive live previews of your Remix front end.

You'll setup a basic blog, with visual editing and live preview inside Presentation

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.

  • The final code created in this Guide is available as a repository on GitHub.
  • Want a fresh start? See the official Remix + Sanity clean template that implements the same patterns.
  • 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 command, initialize a new Remix application from the command line and follow the prompts.

This command will install Remix in a new directory named remix-live-preview. With a template that is just plain Remix with Typescript and Tailwind CSS already configured, and will immediately install dependencies.

# from the command line
npx create-remix@latest remix-live-preview --template SimeonGriggs/remix-tailwind-typography --install

# 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:5173 in your web browser, and you should see this landing screen to show it’s been installed correctly.

The default start page of a new Remix application

Create a new Sanity Studio project

Next, you’ll create a new Sanity Studio for a new Sanity project in a separate folder.

The command below uses the preconfigured schema from the blog template.

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

# follow the prompts to install dependencies

# 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 http://localhost:3333 in your browser, and after logging in, you should see a new Studio with the Blog template schema already created.

There are currently no documents!

Create and publish a few new post type documents.

Create and publish some content to load into the Remix application
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
} from "@remix-run/react";
import { VisualEditing } from "@sanity/visual-editing/remix";
import type { LinksFunction, LoaderFunction } from "@remix-run/node"; import stylesheet from "~/tailwind.css?url"; export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, ];
export const loader: LoaderFunction = async () => {
return {
preview: process.env.NODE_ENV === "development",
ENV: {
SANITY_STUDIO_PROJECT_ID: process.env.SANITY_STUDIO_PROJECT_ID,
SANITY_STUDIO_DATASET: process.env.SANITY_STUDIO_DATASET,
SANITY_STUDIO_URL: process.env.SANITY_STUDIO_URL,
SANITY_STUDIO_API_VERSION: process.env.SANITY_STUDIO_API_VERSION,
},
};
}; export function Layout({ children }: { children: React.ReactNode }) {
const { preview, ENV } = useLoaderData<typeof loader>();
return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> {children}
{preview ? <VisualEditing /> : null}
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(ENV)}`,
}}
/>
<ScrollRestoration /> <Scripts /> </body> </html> ); } export default function App() { return <Outlet />; }

Gotcha

For now you'll just enable preview mode in development, later in this guide you'll setup a session cookie to activate preview mode dynamically.

// ./app/sanity/client.ts

import {createClient} from '@sanity/client'

import {apiVersion, dataset, projectId} from '~/sanity/projectDetails'

export const client = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: true,
  perspective: 'published',
})

Sanity Client can be used for a basic content fetch, however when working with live preview, a mix of server/client side fetching and stega encoding it is recommended to use the React loader package.

Create a new server-only file to export a loadQuery function. See the Remix documentation on .server.ts files.

// ./app/sanity/loader.server.ts

import * as queryStore from "@sanity/react-loader";

import { client } from "~/sanity/client";
import { studioUrl } from "~/sanity/projectDetails";

queryStore.setServerClient(client);

export const { loadQuery } = queryStore;

This loader takes the basic client setup in the previous file, but extends it with a read token and enables "stega" to return information required for visual editing along with your content.

Create one more file to store and reuse your GROQ queries:

// ./app/sanity/queries.ts

import groq from "groq"

export const POSTS_QUERY = groq`*[_type == "post" && defined(slug.current)] | order(_createdAt desc)`
export const POST_QUERY = groq`*[_type == "post" && slug.current == $slug][0]`

Preflight check

Check you have the following files set in your Remix project:

app/
└─ sanity/
   ├─ client.ts
   ├─ loader.server.ts
   ├─ projectDetails.ts
   └─ queries.ts

Loading data from Sanity in Remix

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

Create a new component to display a list of Posts:

// ./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}
          >
            <h2 className="p-4 hover:bg-blue-50">{post.title}</h2>
          </Link>
        ))
      ) : (
        <div className="p-4 text-red-500">No posts found</div>
      )}
    </main>
  );
}

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

// ./app/routes/_index.tsx

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

import Posts from "~/components/Posts";
import { loadQuery } from "~/sanity/loader.server";
import { POSTS_QUERY } from "~/sanity/queries";

export const loader = async () => {
  const {data} = await loadQuery<SanityDocument[]>(POSTS_QUERY);

  return { data };
};

export default function Index() {
  const { data } = useLoaderData<typeof loader>();

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

Visit your home page at http://localhost:3000; it should now look like the image below. If not, your Studio likely has no published 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 loading our Sanity 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 "~/sanity/projectDetails";

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-right m-0 w-1/3 ml-8 mt-2 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>
  );
}

Protip

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

Create a new route to fix the 404's:

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

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

import Post from "~/components/Post";
import { loadQuery } from "~/sanity/loader.server";
import { POST_QUERY } from "~/sanity/queries";

export const loader = async ({ params }: LoaderFunctionArgs) => {
  const {data} = await loadQuery<SanityDocument>(POST_QUERY, params)

  return { data };
};

export default function PostRoute() {
  const { data } = useLoaderData<typeof loader>();

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

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 new 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 Presentation-ready to render interactive live previews!

Preparing for Presentation

The connection between Sanity and Remix is currently simple requests for published (and so publicly queryable) documents. You'll need to configure two items in Sanity Manage, which can easily be accessed from this menu in the Studio:

Open sanity.io/manage for your project by clicking this link in the top right of your Studio

Add a CORS origin

Because our Remix application will make client-side requests to the Sanity Studio across domains, its URL must be added as a valid CORS origin.

This can be done inside sanity.io/manage

  1. Navigate to the API tab and enter http://localhost:5173
  2. Check ”Allow credentials.”
  3. Save

Important:

  • Only set up CORS origins for URLs where you control the code.
  • You must repeat this when you deploy your application to a hosting provider with its URL.
Add a new CORS origin for everywhere Sanity content will be queried with authentication

Add a token with Viewer permissions

In the same section of Manage:

  1. Create a token with Viewer permissions
  2. Copy it to your .env file inside your Remix application
Management panel listing project tokens

Gotcha

This token is can query for draft documents and considered secret and should not be committed to your repository or shared!

Your .env file should now contain the following values:

# .env
SANITY_STUDIO_PROJECT_ID="REPLACE_WITH_YOUR_PROEJCT_ID"
SANITY_STUDIO_DATASET="production"
SANITY_STUDIO_URL="http://localhost:3333"
SANITY_READ_TOKEN="sk...."

You may need to restart your Remix application's development server in order to use the token.

Configure loader to use your token

Update the loader configuration with the code below, which will allow it to:

  • Use a token to query for draft content
  • Query with the previewDrafts perspective to return draft content over published content
  • Return stega encoding as invisible characters inside of the content to power the Visual Editing overlays
import * as queryStore from "@sanity/react-loader";

import { client } from "~/sanity/client";
import { studioUrl } from "~/sanity/projectDetails";

const token = process.env.SANITY_READ_TOKEN

if (!token) {
  throw new Error('Missing SANITY_READ_TOKEN in .env')
}

const clientWithToken = client.withConfig({
  token,
  perspective: 'previewDrafts',
  stega: {
    enabled: true,
    studioUrl,
  },
});

queryStore.setServerClient(clientWithToken);

export const { loadQuery } = queryStore;

With these steps done, your Remix application should still largely work the same.

Setup Presentation in Sanity Studio

Update your Studio's config file inside the sanity-live-preview directory to include the Presentation plugin:

// ./sanity.config.ts

// Add this import
import {presentationTool} from 'sanity/presentation'
export default defineConfig({ // ...all other settings plugins: [
presentationTool({
previewUrl: 'http://localhost:5173'
}),
// ..all other plugins ], })

You should now see the Presentation Tool available at http://localhost:3333/presentation.

You should now be able to click any of the text on screen, make edits, and momentarily see the changes render in the Remix front end.

Sanity Studio with Presentation Tool open

You now have Visual Editing! This is great!

But, it's always on. This is not great. You don't want every visitor to see draft content, or

Optional: Configuring locations in the Studio

Following the documentation on setting up locations in Presentation will create links from any document to all the places an author could expect to find them in the front end.

In your Studio project:

Create a new file for the locate function

// ./presentation/locate.ts

import { DocumentLocationResolver } from "sanity/presentation";
import { map } from "rxjs";

// Pass 'context' as the second argument
export const locate: DocumentLocationResolver = (params, context) => {
  // Set up locations for post documents
  if (params.type === "post") {
    // Subscribe to the latest slug and title
    const doc$ = context.documentStore.listenQuery(
      `*[_id == $id][0]{slug,title}`,
      params,
      { perspective: "previewDrafts" } // returns a draft article if it exists
    );
    // Return a streaming list of locations
    return doc$.pipe(
      map((doc) => {
        // If the document doesn't exist or have a slug, return null
        if (!doc || !doc.slug?.current) {
          return null;
        }
        return {
          locations: [
            {
              title: doc.title || "Untitled",
              href: `/${doc.slug.current}`,
            },
            {
              title: "Posts",
              href: "/",
            },
          ],
        };
      })
    );
  }
  return null;
}

Update your sanity.config.ts file to import the locate function into the Presentation plugin.

// ./sanity.config.ts

// Add this import
import { locate } from './presentation/locate'
export default defineConfig({ // ...all other settings plugins: [ presentationTool({ previewUrl: 'http://localhost:5173',
locate
}), // ..all other plugins ], })

You should now see the locations at the top of all post type documents:

Locations of where the document is used shown on the document editor

Was this article helpful?