React Router

Setting up a preview mode

Most often than not, editors of a DatoCMS project will find very beneficial to have a preview of how the changes they are making to ie. an article will be rendered inside the final website.

With React Router you can easily add a "preview mode" to your production website. With that, requests coming from editors will add a special header — X-Include-Drafts — that returns content that is not yet published.

Step 1: Create resource routes to turn Preview mode on/off

First, we need to create a couple of resource routes to enable/disable Preview Mode. We're going to use React Router's built-in session management to store a cookie inside the browser of the visitor.

First step is to actually create the session manager. Create a new file under app/sessions.ts:

app/sessions.ts
import { createCookieSessionStorage } from 'react-router';
const { getSession, commitSession, destroySession } = createCookieSessionStorage({
cookie: {
name: '__session',
maxAge: 604_800,
path: '/',
},
});
export { getSession, commitSession, destroySession };

Now we can use it inside a new resource route under app/routes/preview-start.ts, that we can call to turn on the preview mode:

app/routes/preview-start.ts
import { redirect } from 'react-router';
import { getSession, commitSession } from '~/sessions';
import type { Route } from './+types/preview-start';
export async function action({ request }: Route.ActionArgs) {
const session = await getSession(request.headers.get('Cookie'));
session.set('preview', 'yes');
return redirect('/', {
headers: {
'Set-Cookie': await commitSession(session),
},
});
}

Similarly, we also need to create a route under app/routes/preview-stop.ts, to turn preview mode off:

app/routes/preview-stop.ts
import { redirect } from 'react-router';
import { getSession, commitSession } from '~/sessions';
import type { Route } from './+types/preview-stop';
export async function action({ request }: Route.ActionArgs) {
const session = await getSession(request.headers.get('Cookie'));
session.unset('preview');
return redirect('/', {
headers: {
'Set-Cookie': await commitSession(session),
},
});
}

Both routes only export an action, with no component to render, so we declare them in app/routes.ts as any other route:

app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('preview/start', 'routes/preview-start.ts'),
route('preview/stop', 'routes/preview-stop.ts'),
] satisfies RouteConfig;

We can now tweak the app/root.tsx file to add to every page a button to toggle the preview on and off:

app/root.tsx
import {
Form,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteLoaderData,
} from 'react-router';
import { getSession } from '~/sessions';
import type { Route } from './+types/root';
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request.headers.get('Cookie'));
return { previewEnabled: session.has('preview') };
}
export function Layout({ children }: { children: React.ReactNode }) {
const data = useRouteLoaderData('root');
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{data?.previewEnabled ? (
<Form method="post" action="/preview/stop">
<button>Exit preview mode</button>
</Form>
) : (
<Form method="post" action="/preview/start">
<button>Enter preview mode</button>
</Form>
)}
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}

Step 2: Fetch non-published content with X-Include-Drafts

Now every loader can know from the session if the visitor is currently in preview mode by looking at the request object.

If that's the case, we can run the same query, but passing the X-Include-Drafts header, which returns the records at their latest version available instead of the one that's currently set as published:

app/routes/home.tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';
import { getSession } from '~/sessions';
const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
posts: allBlogPosts(first: $limit) {
title
}
}`;
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request.headers.get('Cookie'));
return load(HOMEPAGE_QUERY, {
variables: { limit: 10 },
includeDrafts: session.has('preview'),
});
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { posts } = loaderData;
return <div>{JSON.stringify(posts, null, 2)}</div>;
}

Last updated: