← Back to Home

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 project
if 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:


Existing Setup Requirements

Project Configuration

Your Astro project should have output set to either 'hybrid' or 'server' in astro.config.mjs:

export default defineConfig({
output: 'hybrid', // or 'server'
// ... other config
});

Knowledge Prerequisites

Demo Projects

for this article i created a quick demo for Payload CMS with preview with Astro function Demo Payload Repo
and 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:

.env (Payload CMS Project)
# Payload Configuration
PAYLOAD_SECRET=your-secret-key-here
DATABASE_URI=mongodb://localhost:27017/your-database
# Frontend URLs for Preview
FRONTEND_URL=localhost:4321
FRONTEND_URL_PRODUCTION=your-production-domain.com
# Environment Mode
NODE_ENV=development
# Optional: If using Vercel
# VERCEL_PROJECT_PRODUCTION_URL=your-vercel-domain.vercel.app

Important Notes:

Astro Project Environment Variables

In your Astro project root, create or update your .env file:

.env (Astro Project)
# Payload CMS API Configuration
PAYLOAD_API_URL=http://localhost:3000
PAYLOAD_API_URL_PRODUCTION=https://your-payload-cms-domain.com
# API Authentication (if required)
PAYLOAD_API_TOKEN=your-api-token-here
# Optional: For multi-language sites
DEFAULT_LOCALE=en

Important Notes:

Cloudflare Environment Variables

If deploying to Cloudflare Pages, set these variables in your Cloudflare dashboard:

Terminal window
# In Cloudflare Pages Settings > Environment Variables
PAYLOAD_API_URL_PRODUCTION=https://your-payload-cms-domain.com
PAYLOAD_API_TOKEN=your-api-token-here

For Cloudflare Workers/Pages, access them via:

const ASTRO_ENV = Astro.locals?.runtime?.env;
const apiUrl = ASTRO_ENV?.PAYLOAD_API_URL_PRODUCTION;

Security Considerations

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:

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 repo
or 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

generatePreviewPath.ts
// 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

pages/preview.Astro
---
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=ar
const 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:

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 your fetchDoc function
actually 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

Conclusions

in this article we tried to bridge the gap between Astro and Payload especially when it comes to live preview links without the need to use any frameworks and depending on ssr logic in Astro to render preview page of Payload content if you think this is not the best option for you can refer to Payload docs for Live Preview this can help you further implement such features if you are using different frameworks or you need more sophisticated implementation