Data Fetching
Server-side data fetching is done per page and per component with Payload’s local API directly. The only shared helper is getPayload in src/lib/payload/local-api.ts, which returns a cached Payload instance. Each route or component defines its own small getX() function next to where the data is used, so the query lives with the thing that renders it.
The shared helper
Section titled “The shared helper”getPayload wraps Payload’s local API and memoises the instance per request:
import { getPayload } from '@/lib/payload/local-api';
const payload = await getPayload();const { docs } = await payload.find({ collection: 'pages', limit: 1 });Everything else — draft handling, the published-only filter, population depth — is set explicitly on each find/findGlobal call.
Draft and published filtering
Section titled “Draft and published filtering”Pages and components that render previewable content read draftMode() inline and branch the query:
draft: isEnabledtells Payload to return the latest (unpublished) version when preview is on.- The
_status: 'published'filter is added only when preview is off, so normal visitors never see drafts.
The convention is to put the fetch function below the component that uses it (function declarations are hoisted, so order doesn’t matter at runtime).
import { draftMode } from 'next/headers';import { getPayload } from '@/lib/payload/local-api';
export default async function HomePage() { const page = await getHomepage(); // ...render}
async function getHomepage() { const payload = await getPayload(); const { isEnabled } = await draftMode();
const { docs } = await payload.find({ collection: 'pages', depth: 2, limit: 1, draft: isEnabled, where: { and: [ { type: { equals: 'homepage' } }, ...(isEnabled ? [] : [{ _status: { equals: 'published' } }]), ], }, });
return docs.at(0) ?? null;}Fetching by slug
Section titled “Fetching by slug”For [slug] routes, add a slug equals condition to the same and array. Passing depth as a parameter lets the page use one depth (2, for rendering) and generateMetadata use another (1, for meta.image):
async function getPage(slug: string, depth: number) { const payload = await getPayload(); const { isEnabled } = await draftMode();
const { docs } = await payload.find({ collection: 'pages', depth, limit: 1, draft: isEnabled, where: { and: [ { slug: { equals: slug } }, ...(isEnabled ? [] : [{ _status: { equals: 'published' } }]), ], }, });
return docs.at(0) ?? null;}Fetching a global
Section titled “Fetching a global”Globals use findGlobal. The header and footer read draftMode() the same way so preview shows draft global content:
async function getSiteHeader() { const payload = await getPayload(); const { isEnabled } = await draftMode();
return payload.findGlobal({ slug: 'site-header', depth: 2, draft: isEnabled, });}Build-time fetches
Section titled “Build-time fetches”generateStaticParams and sitemap.ts run at build time, where there is no request to read draft state from — calling draftMode() there throws. These fetch published content explicitly with draft: false and a _status: 'published' filter, and never read draftMode():
export async function generateStaticParams() { const payload = await getPayload();
const { docs } = await payload.find({ collection: 'posts', limit: 100, draft: false, where: { _status: { equals: 'published' } }, select: { slug: true }, });
return docs.map((doc) => ({ slug: doc.slug }));}Revalidation
Section titled “Revalidation”Because pages are statically generated, content changes are pushed out with revalidatePath:
- Collections call
revalidateCollection(slug, doc.slug)inafterChangeandafterDelete, which revalidates the document’s path. - Globals call
revalidatePath('/', 'layout')inafterChange. Globals render in the root layout, so this refreshes every page.
So editing or deleting content in the admin regenerates the affected static pages on the next request. See the Collections guide for the hook setup.