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.
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.
# from the command line
npx create-remix@latest remix-live-preview --template SimeonGriggs/remix-tailwind-typography --install
# enter the Remix application's directorycd remix-live-preview
# run the development servernpm run dev
Visit http://localhost:3000 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 its own folder. Using the preconfigured schema from the blog template.
# from the command linenpm 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 directorycd sanity-live-preview
# run the development servernpm 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 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
Work inside the remix-live-preview directory for this section
To query and display Sanity content inside the Remix application, you must install a few packages first.
# in /remix-live-previewnpminstall @sanity/client@latest @sanity/react-loader@latest @sanity/overlays@latest @sanity/image-url @portabletext/react groq
This command installs:
@sanity/client: A package to simplify interacting with the Content Lake
@sanity/react-loader: Functions that simplify querying and live-reloading data from Sanity
@sanity/overlays: Functions for rendering clickable links when in preview mode to enable visual editing
@sanity/image-url: Functions to create complete image URLs from just the ID of a Sanity image asset.
@portabletext/react: A component to render block content from a Portable Text field with configuration options.
groq provides syntax highlighting in your code editor for GROQ queries
To orchestrate these together, you'll need to create a few files.
Create a file for your environment variables. None of these are secrets that must be protected, but it will help you to customize them whenever you deploy your app.
# .env
SANITY_STUDIO_PROJECT_ID="79w8op0f"
SANITY_STUDIO_DATASET="production"
SANITY_STUDIO_URL="http://localhost:3333"
# Do not set to true in production environments
# This will load a larger version of Sanity Client
SANITY_STUDIO_STEGA_ENABLED="true"
Protip
What is "stega"? Throughout this guide, you'll see references to "stega" this is the magic behind Content Source Maps that allows Sanity to encode special characters into data so that a link from content you see, to its document and field in the Sanity Studio, can be created.
Create a component to enable visual editing. In the example code below, it will only be enabled when the site is viewed inside an Iframe.
This component also contains imports a component named VisualEditing which handles tracking the current URL and passing it back up to Presentation.
Replace your app's root route with the code below to load these variables in the loader function and render them to the document window on the client. Read their documentation for more information about handling environment variables in Remix.
You'll also see the "lazy loading" of the visual editing component created in the previous step.
Create a new file to retrieve these project details throughout your app.
These will be used to configure a Sanity Client, the Loader functions, and build image URLs.
// ./app/sanity/projectDetails.tsdeclare global {interfaceWindow{ENV:{SANITY_STUDIO_PROJECT_ID:stringSANITY_STUDIO_DATASET:stringSANITY_STUDIO_URL:stringSANITY_STUDIO_STEGA_ENABLED:string}}}const{SANITY_STUDIO_PROJECT_ID,SANITY_STUDIO_DATASET,SANITY_STUDIO_URL='http://localhost:3333',SANITY_STUDIO_STEGA_ENABLED=false}=typeof document ==='undefined'? process.env : window.ENVexportconst projectId =SANITY_STUDIO_PROJECT_ID!exportconst dataset =SANITY_STUDIO_DATASET!exportconst studioUrl =SANITY_STUDIO_URL!exportconst stegaEnabled =SANITY_STUDIO_STEGA_ENABLED==='true'if(!projectId)thrownewError('Missing SANITY_STUDIO_PROJECT_ID in .env')if(!dataset)thrownewError('Missing SANITY_STUDIO_DATASET in .env')if(!studioUrl)thrownewError('Missing SANITY_STUDIO_URL in .env')if(!stegaEnabled)thrownewError(`Missing SANITY_STUDIO_STEGA_ENABLED in .env`)
Create a new file to set up the Loader.
These will load Sanity content on the server and provide live updates when viewing the site inside Presentation.
// ./app/sanity/loader.tsimport{ createQueryStore }from'@sanity/react-loader'// This is the "smallest" possible version of a query store// Where stega-enabled queries only happen server-side to avoid bundle bloatexportconst queryStore =createQueryStore({client:false, ssr:true})exportconst{useLiveMode, useQuery}= queryStore
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.tsximporttype{ LoaderFunctionArgs }from"@remix-run/node";import{ useLoaderData }from"@remix-run/react";importtype{ SanityDocument }from"@sanity/client";import Post from"~/components/Post";import{ useQuery }from"~/sanity/loader";import{ loadQuery }from"~/sanity/loader.server";import{POST_QUERY}from"~/sanity/queries";exportconstloader=async({ params }: LoaderFunctionArgs)=>{const{data}=awaitloadQuery<SanityDocument>(POST_QUERY, params)return{ data };};exportdefaultfunctionPostRoute(){const{ data }=useLoaderData<typeof loader>();return<Postpost={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!
Add a CORS origin in Sanity Manage
Because our Remix application will make authenticated requests to the Sanity Project, its URL must be added as a valid CORS origin.
Open sanity.io/manage for your project by clicking this link in the top right of your Studio
Navigate to the API tab and enter http://localhost:3000
Check ”Allow credentials.”
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
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'
exportdefaultdefineConfig({// ...all other settings
plugins:[
presentationTool({
previewUrl:'http://localhost:3000'
}),
// ..all other plugins],})
You should now see the Presentation Tool available at http://localhost:3333/presentation. You may only get a loading spinner for now. Each route's loaders need updating to render changes in real-time.
Sanity Studio with Presentation Tool open
Replace the index route to add useQuery. Notice how the data loading on the server sets up the initial state but is then passed through useQuery for live preview updates.
// ./app/routes/_index.tsximport{ useLoaderData }from"@remix-run/react";importtype{ SanityDocument }from"@sanity/client";import Posts from"~/components/Posts";import{ useQuery }from"~/sanity/loader";import{ loadQuery }from"~/sanity/loader.server";import{POSTS_QUERY}from"~/sanity/queries";exportconstloader=async()=>{const initial =awaitloadQuery<SanityDocument[]>(POSTS_QUERY);return{ initial, query:POSTS_QUERY, params:{}};};exportdefaultfunctionIndex(){const{ initial, query, params }=useLoaderData<typeof loader>();const{ data, loading }=useQuery<typeof initial.data>(query, params,{
initial,});// `data` should contain the initial data from the loader// `loading` will only be true when Visual Editing is enabledif(loading &&!data){return<div>Loading...</div>;}return data ?<Posts posts={data}/>:null;}
// ./presentation/locate.tsimport{ DocumentLocationResolver }from"sanity/presentation";import{ map }from"rxjs";// Pass 'context' as the second argumentexportconst locate:DocumentLocationResolver=(params, context)=>{// Set up locations for post documentsif(params.type ==="post"){// Subscribe to the latest slug and titleconst doc$ = context.documentStore.listenQuery(`*[_id == $id][0]{slug,title}`,
params,{ perspective:"previewDrafts"}// returns a draft article if it exists);// Return a streaming list of locationsreturn doc$.pipe(map((doc)=>{// If the document doesn't exist or have a slug, return nullif(!doc ||!doc.slug?.current){returnnull;}return{
locations:[{
title: doc.title ||"Untitled",
href:`/${doc.slug.current}`,},{
title:"Posts",
href:"/",},],};}));}returnnull;}
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'
exportdefaultdefineConfig({// ...all other settings
plugins:[presentationTool({
previewUrl:'http://localhost:3000',
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
Next steps
As your front end grows, you may not wish to make preview versions of every unique component. Consider making a reusable live preview component by following this guide.
Sanity – The Content Operating System that ends your CMS nightmares
Sanity replaces rigid content systems with a developer-first operating system. Define schemas in TypeScript, customize the editor with React, and deliver content anywhere with GROQ. Your team ships in minutes while you focus on building features, not maintaining infrastructure.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.
Automatically track when content was first published with a timestamp that sets once and never overwrites, providing reliable publication history for analytics and editorial workflows.
AI-powered automatic tagging for Sanity blog posts that analyzes content to generate 3 relevant tags, maintaining consistency by reusing existing tags from your content library.