Create live preview page in Astro with headless Payload CMS
tags: astro, cloudflare, astro ssg, astro ssr, payload cms, preview payload cms with astro
Overview
In this article we check a simple way we can add preview feature for your Astro projectif you are using PayloadCms as your headless cms software
the idea is Payload has its good integration with NextJs also with live editor
and of course you can check some solutions provided by the community
i am sharing what worked for me based on requirements like:
- you have an Astro project with ssg and ssr pages
- you need a quick way for you and your content team to preview their cms changes before redeploying the app
- in most cases you have Astro integrated on somehing like Cloudflare or Netlify or even self hosted
in all cases you will need a rebuild if you have ssg pages and would be better to verify first - you don’t want to tweak that much on you Astro project and adding one route fore preview feature is very easy
- there are more scenarios where you might consider the solution we made even if it is simple
it might be just what you need without adding more plugins to your cms or front end.\
Existing Setup Requirements
- A running Payload CMS instance (local or hosted)
- An Astro project configured with hybrid or server output mode
- Basic understanding of SSR (Server-Side Rendering) vs SSG (Static Site Generation)
- Payload CMS collections configured with live preview enabled
- REST API access enabled in your Payload configuration
Project Configuration
Your Astro project should haveoutput set to either 'hybrid' or 'server' in astro.config.mjs:
export default defineConfig({ output: 'hybrid', // or 'server' // ... other config});Knowledge Prerequisites
- Familiarity with Astro’s routing system
- Understanding of Payload CMS collections and documents
- Basic knowledge of REST API calls
- Experience with environment variables in both Node.js and Astro
Demo Projects
for this article i created a quick demo for Payload CMS with preview with Astro function Demo Payload Repoand for Astro project you can find demo repo here Demo Astro Repo
Environment Variables Setup
Proper environment configuration is crucial for the preview feature to work correctly.You’ll need to set up environment variables in both your Payload CMS and Astro projects.
Payload CMS Environment Variables
In your Payload CMS project root, create or update your.env file:
# Payload ConfigurationPAYLOAD_SECRET=your-secret-key-hereDATABASE_URI=mongodb://localhost:27017/your-database
# Frontend URLs for PreviewFRONTEND_URL=localhost:4321FRONTEND_URL_PRODUCTION=your-production-domain.com
# Environment ModeNODE_ENV=development
# Optional: If using Vercel# VERCEL_PROJECT_PRODUCTION_URL=your-vercel-domain.vercel.appImportant Notes:
FRONTEND_URL: Your local Astro development server URL (without protocol in this example)FRONTEND_URL_PRODUCTION: Your production Astro site domain- The preview function will automatically use the correct URL based on
NODE_ENV
Astro Project Environment Variables
In your Astro project root, create or update your.env file:
# Payload CMS API ConfigurationPAYLOAD_API_URL=http://localhost:3000PAYLOAD_API_URL_PRODUCTION=https://your-payload-cms-domain.com
# API Authentication (if required)PAYLOAD_API_TOKEN=your-api-token-here
# Optional: For multi-language sitesDEFAULT_LOCALE=enImportant Notes:
PAYLOAD_API_URL: Your local Payload CMS instance URLPAYLOAD_API_URL_PRODUCTION: Your production Payload CMS URLPAYLOAD_API_TOKEN: Optional but recommended for securing API access
Cloudflare Environment Variables
If deploying to Cloudflare Pages, set these variables in your Cloudflare dashboard:# In Cloudflare Pages Settings > Environment VariablesPAYLOAD_API_URL_PRODUCTION=https://your-payload-cms-domain.comPAYLOAD_API_TOKEN=your-api-token-hereFor Cloudflare Workers/Pages, access them via:
const ASTRO_ENV = Astro.locals?.runtime?.env;const apiUrl = ASTRO_ENV?.PAYLOAD_API_URL_PRODUCTION;Security Considerations
- Never commit
.envfiles to version control - Add
.envto your.gitignorefile - Use different API tokens for development and production
- Consider using environment-specific
.env.developmentand.env.productionfiles - Rotate API tokens regularly
How This Actually Works
Before diving into code, here’s the flow: when someone clicks preview in Payload,it generates a URL to your Astro site with query params (slug, collection, language).
Your Astro SSR route catches that, fetches the content from Payload’s API, and renders it.
That’s it - no magic, just a preview endpoint that does on-demand rendering.
The key pieces you need:
- A collection prefix map so Payload knows how to build URLs (
pages: '',posts: 'blog', etc.) - The preview function in Payload that generates the URL with all needed params
- An SSR route in Astro (
preview.astro) that reads those params and fetches content - A fetch utility to grab data from Payload’s REST API
One thing I found helpful: test the preview URL generation first.
Just click preview in Payload and check the URL it creates - if that looks right, you’re halfway there.
The Astro side is straightforward once you have the right data coming through.
Define preview function in Payload CMS
In Payload project assuming you have implemented something similar to the demo repoor some Payload project for a site with live preview feature enabled
also you may use Payload website template to test this
in the Payload project there is a function under src/utilities/generatePreviewPath.ts
you will add preview code function for Astro without the need to remove existing one
// some code ....export const generatePreviewPathAstro = ({ collection, slug, req }: Props) => { const path = `${collectionPrefixMap[collection]}/${slug}`
const params = { slug, collection, path, lang: req.locale || 'en', }
const encodedParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => { encodedParams.append(key, value) })
const isProduction = process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL) // const protocol = isProduction ? 'https://' : req.protocol+'//' const protocol = isProduction ? 'https://' : '' const frontendUrl = isProduction ? process.env.FRONTEND_URL_PRODUCTION : process.env.FRONTEND_URL
const url = `${protocol}${frontendUrl}/${req.locale === 'en' ? '' : req.locale + '/'}preview?${encodedParams.toString()}`
return url}Define preview ssr route in Astro
after defining the preview function in Payload cms now it is time to have a SSR path under Astro project where it will fetch collection dynamically from Payload using rest api this part in Astro assumes you implemented rendering logic for each component you have for Payload the idea is you separate rendering logic and make it based on Payload components then when you get response from your Payload api with layout object that has all blocks you can apply some logic to pick which component to render here is an example of preview page using multi language site assuming you have implemented logic on how to render your site adding extra preview path to be ssr will utilize same rendering logic---export const prerender = false;import Layout from "../layouts/Layout.Astro";import RenderBlocks from "../components/RenderPayloadBlocks.Astro";import { fetchDoc } from "../utils/payLoadAPICalls";
//http://localhost:4321/preview?slug=about&collection=pages&path=%2Fabout&lang=arconst searchParams = Astro.url.searchParams;const ASTRO_ENV = Astro.locals?.runtime?.env;const slug = (searchParams.get("slug") || "") .replace(/^\/+|\/+$/g, "") .replace(/%2F/g, "/");const collection = searchParams.get("collection") || "";const path = searchParams.get("path") || "";const lang = searchParams.get("lang") || "";
console.log(slug, collection, path, lang);const docs = await fetchDoc(slug, lang, collection, ASTRO_ENV);
const { layout: layoutBlocks, meta: seo } = docs.docs[0];---
{ (() => { try { // Safe rendering with additional checks
return ( <Layout lang={lang} seo={seo}> <RenderBlocks layoutBlocks={layoutBlocks} /> </Layout> ); } catch (renderError) { console.error("SSR: Critical rendering error:", renderError); // Emergency fallback - this should never happen but provides safety return ( <Layout lang={lang} seo={{ title: "Error", description: "Page temporarily unavailable" }} > <div class="error-fallback"> <h1>Content Temporarily Unavailable</h1> <p>Please try again later.</p> </div> </Layout> ); } })()}Tips & Things I Learned Along the Way
For Content Teams
One thing I noticed: content editors sometimes forget to refresh the preview after saving changes.The preview button generates a new tab, but that tab doesn’t auto-refresh.
I found it helpful to just tell the team: “Click preview, make your edits, save, then refresh that preview tab.”
Simple workflow, no confusion.
Also, if you’re working with non-technical editors, consider adding a visual indicator on the preview page
like a banner that says “Preview Mode” so they don’t accidentally think they’re on the live site.
Development Workflow Tips
When testing locally, I run both Payload and Astro dev servers side by side.A trick that saved me time: use different ports obviously (Payload on 3000, Astro on 4321),
but also keep both terminal windows visible so you can see errors from both sides immediately.
Another thing - CORS can be annoying during development.
If Astro can’t fetch from Payload, check your Payload config has the right CORS settings.
In development, I usually just allow localhost with all ports.
Debugging Preview Issues
Most preview issues come down to three things:- Wrong URL generated: Check the
collectionPrefixMapmatches your Astro routes - API fetch fails: Log the full API URL you’re building and test it in the browser directly
- Missing environment variables: This one got me a few times - Cloudflare env vars are accessed differently than local ones
Pro tip: Add console logs in both the Payload preview function and the Astro preview route.
You’ll see exactly what data is being passed around. Remove them later.
Multi-Language Sites
If you’re doing multi-language like in the example, make sure yourfetchDoc functionactually passes the locale to Payload’s API.
Also, the locale param in the URL needs to match what Payload expects.
in our case we are using en and ar for locale but you can use any locale you want
When Working with Teams
If multiple people are using this, consider adding some basic error handling to show friendly messages.Instead of “Cannot read property ‘layout’ of undefined”, show “Content not found - try publishing the page first.”
Your content team will thank you.
Limitations of the previous approach
some limitations you need to consider when using this previous approach- even if it is relatively easy to implement it is not an actual visual builder
- iframe preview inside Payload editor is not included so with every change you need to refresh the preview link
it is normal and expected behavior on other platforms but if you need some sort of instant feedback
this might not be the thing for you - the previous approach doesn’t show how to put this preview page behing some authentication layer
you can actually acheive this by having Astro project on cloudflare or something behind some rules
like allowing certain emails to see this preview site, and this can be your only dev cloudflare worker
that you use for this and without adding your own auth layer and with only utilizing cloudflare features\