Fixing React Previews with Headless CMS
Cover photo by Markus Spiske on Unsplash
So you have your headless CMS connected to your React app. Everything works great until your editor asks to see a draft. The static build fails them because the data simply isn’t there yet.
The immediate fix is usually messy
I often see developers throwing up their hands and disabling static generation entirely just to get previews working. That feels like burning down the house to kill a spider. You’re losing the speed that made you choose a modern stack in the first place.
Here is the reality. You need a way to bypass your static cache when a specific preview cookie is present. In Next.js, that’s what draftMode is for. If you’re on a different setup, you’re essentially building a conditional fetcher that checks for a session token before hitting your API.
async function getPageData(slug, isPreview) { const url = isPreview ? `${API_URL}/drafts/${slug}` : `${API_URL}/pages/${slug}`; const res = await fetch(url, { cache: isPreview ? 'no-store' : 'force-cache' }); return res.json();}“But why not just use client-side fetching for everything?” someone might ask. Fair point, and maybe you should. If your site has a massive amount of dynamic content, static generation might be more trouble than it’s worth (I’m guilty of over-engineering this myself). But for most content sites, mixing static and dynamic is the right move.
Don’t overthink the architecture
Keep your preview route simple. It should be a dedicated server-side function that validates the user’s auth token, sets a secure cookie, and redirects them to the live page.
One caveat here: previewing feels like a local-only problem until you realize your editors are spread across different timezones and network conditions. You have to ensure that your draft API endpoints are locked down tighter than your public ones. It’s easy to accidentally expose draft content to the public if you’re lazy with your API middleware (I’ve seen it happen in production).
Keep the preview logic out of your core component tree. If you’re injecting isPreview props into every single component just to toggle some UI, you’re doing it wrong. Use a Context provider or a hook that reads the cookie. It’s cleaner. It’s safer.
How do you handle your preview state without creating a mountain of technical debt?