Real-time previews with Next.js' draft mode
One of the downsides of building static sites with content files is the delay occuring between saving changes and seeing them on the website.
You typically need to open a PR and wait for deploy previews.
This recipe shows you how to create immediate previews of your Keystatic content with Next.js' draft mode feature.
This recipe assumes you've got an existing Keystatic site, that:
- uses the Reader API to retrieve content
- is connected to a GitHub repo, running in github mode or cloud mode
Creating "start" and "end" preview routes
Create an app/preview/start/route.tsx
file that will enable draft mode when accessed:
import { redirect } from 'next/navigation';
import { draftMode, cookies } from 'next/headers';
export async function GET(req: Request) {
const url = new URL(req.url);
const params = url.searchParams;
const branch = params.get('branch');
const to = params.get('to');
if (!branch || !to) {
return new Response('Missing branch or to params', { status: 400 });
}
draftMode().enable();
cookies().set('ks-branch', branch);
const toUrl = new URL(to, url.origin);
toUrl.protocol = url.protocol;
toUrl.host = url.host;
redirect(toUrl.toString());
}
Next, create an app/preview/end/route.tsx
file used to disable draft mode:
import { cookies, draftMode } from 'next/headers';
export function POST(req: Request) {
if (req.headers.get('origin') !== new URL(req.url).origin) {
return new Response('Invalid origin', { status: 400 });
}
const referrer = req.headers.get('Referer');
if (!referrer) {
return new Response('Missing Referer', { status: 400 });
}
draftMode().disable();
cookies().delete('ks-branch');
return Response.redirect(referrer, 303);
}
Adding a "stop draft mode" button in the front-end
Add the following to your main layout component to allow editors to opt out of draft mode:
+ import { cookies, draftMode } from 'next/headers';
export default async function RootLayout() {
+ const { isEnabled } = draftMode();
return (
<div>
{children}
+ {isEnabled && (
+ <div>
+ Draft mode ({cookies().get('ks-branch')?.value}){' '}
+ <form method="POST" action="/preview/end">
+ <button>End preview</button>
+ </form>
+ </div>
+ )}
</div>
);
}
Adding a Preview URL key to collections or singletons
The draft mode opt-in will happen from the Keystatic Admin UI.
In the Keystatic config, collections and singletons can have a previewUrl
key. This will generate an Admin UI link to the content preview, in draft mode:
collections: {
posts: collection({
label: 'Posts',
slugField: 'title',
path: `content/posts/*`,
+ previewUrl: `/preview/start?branch={branch}&to=/posts/{slug}`,
schema: { //... }
}),
},
This prefixes the front-end route for a post entry with the /preview/start
route we created earlier.
Updating the Keystatic Reader
The reader
you're currently using from the Keystatic Reader API needs to be updated. If draft mode is turned on, it should read from GitHub directly, using Keystatic's GitHub reader.
Since there is a little bit of setup involved, it makes sense to create reusable draft-mode-aware reader.
Make sure you replace the repo: 'REPO_ORG/REPO_NAME'
line in the code snippet below with your own repo org and name!
// src/utils/reader.ts
import { createReader } from '@keystatic/core/reader';
import { createGitHubReader } from '@keystatic/core/reader/github';
import keystaticConfig from '../../keystatic.config';
import { cache } from 'react';
import { cookies, draftMode } from 'next/headers';
export const reader = cache(() => {
let isDraftModeEnabled = false;
// draftMode throws in e.g. generateStaticParams
try {
isDraftModeEnabled = draftMode().isEnabled;
} catch {}
if (isDraftModeEnabled) {
const branch = cookies().get('ks-branch')?.value;
if (branch) {
return createGitHubReader(keystaticConfig, {
// Replace the below with your repo org an name
repo: 'REPO_ORG/REPO_NAME',
ref: branch,
token: process.env.PREVIEW_GITHUB_TOKEN,
});
}
}
// If draft mode is off, use the regular reader
return createReader(process.cwd(), keystaticConfig);
});
Updating existing uses of the reader
The new reader
is a function, so you'll need to update all your existing use cases to call the reader()
function:
- const posts = await reader.collections.posts.all();
+ const posts = await reader().collections.posts.all();
Testing the preview
In the Keystatic Admin UI, create a new post and save it in a new branch.
Next to the Save
button, you will find a preview icon.
Click on it and you should see the post you just created!