Step 8: Preview drafts (optional)
Let editors open a draft post on your website before it goes live, with a preview route that stays out of search engines.
A preview shows a draft on the real website, before anyone goes live. Editors use it to check how a post looks. This step is optional. Your site works without it.
Here is how it works. In the CMS, an editor selects Preview. The CMS opens a link on your production address, such as https://harborandpine.com/__preview/posts/our-sourdough-starter?token=.... Your website then asks the CMS for that draft, using a short-lived token. Each link works for one hour and for one post only.
Preview needs a reachable website
The preview link opens your site at its production address. So a preview only works once the site is deployed (step 9), or on a computer that answers at that address. For this guide, try the preview after step 9.
8.1 Add the preview route
Create src/routes/[__preview]/$collection/$slug.tsx. The square brackets around __preview are part of the name, so the page is served at /__preview:
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { setResponseHeader } from '@tanstack/react-start/server'
import { getPreviewEntry, previewHeaders } from '@areca-cms/site-sdk'
import type { RichTextValue } from '@areca-cms/site-sdk'
import { z } from 'zod'
import { ArticleBody } from '../../../components/article-body'
/** Asks the CMS for the draft. Runs on the server, so the token stays there. */
const loadPreview = createServerFn({ method: 'GET', strict: { output: false } })
.validator(z.object({ token: z.string().min(1) }))
.handler(async ({ data }) => {
// Tell browsers and search engines not to keep or index this page.
const headers = previewHeaders()
setResponseHeader('X-Robots-Tag', headers['X-Robots-Tag'])
setResponseHeader('Cache-Control', headers['Cache-Control'])
try {
const preview = await getPreviewEntry({ token: data.token })
return { ok: true as const, preview }
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to fetch preview'
return { ok: false as const, error: message }
}
})
export const Route = createFileRoute('/__preview/$collection/$slug')({
validateSearch: (search) => z.object({ token: z.string().min(1) }).parse(search),
loaderDeps: ({ search }) => ({ token: search.token }),
loader: ({ deps }) => loadPreview({ data: { token: deps.token } }),
head: () => ({
meta: [{ name: 'robots', content: 'noindex, nofollow' }],
}),
component: PreviewPage,
})
function PreviewPage() {
const result = Route.useLoaderData()
if (!result.ok) {
return <p role="status">Preview unavailable: {result.error}</p>
}
const { entry } = result.preview
return (
<main className="mx-auto max-w-2xl px-6 py-16">
<p role="status" className="mb-8 rounded bg-amber-100 px-4 py-2 text-sm">
Draft preview. This page is not published.
</p>
<h1 className="text-4xl font-semibold">{String(entry.data.title ?? entry.slug)}</h1>
<ArticleBody body={entry.data.body as RichTextValue} />
</main>
)
}The preview route needs zod, which it imports for the token check. Install it once:
pnpm add zodThe entry comes from the token, not from the address in the browser. So changing the slug in the address bar does not reveal another draft.
8.2 Check the preview
Once the site is deployed (step 9), open a post in the CMS and select Preview. You should see the banner Draft preview. This page is not published., the draft's title and its body. Your visitors still see only the live version of the post.
If something went wrong
- The page says Preview unavailable. The link has expired after one hour, or it was opened for another site. Select Preview again in the CMS.
- The link opens the wrong site. Check the Production URL in the site's Settings › General. Previews open at that address.
- You see a 404 page. Check that the folder is named
[__preview]with square brackets, exactly as shown.
Step 7: Show the posts on your website
Build the blog list page and the single post page in TanStack Start, with rich text and pictures from the pulled content.
Step 9: Deploy and publish for real
Make your website pull the latest content on every build, add a health check, and understand what happens when an editor presses Go live.