Files
convex-monorepo-payload/docs/payload-cms.md
2026-03-27 17:49:07 -05:00

425 lines
11 KiB
Markdown

# Payload CMS In `convex-monorepo`
This document explains how Payload CMS is integrated into this template, what it manages
today, and how to extend it safely when you want more editable marketing content.
## Current Scope
Payload currently powers the editable marketing layer inside the Next.js app.
Today that means:
- the landing page at `/`
- the admin UI at `/admin`
- the REST API under `/api`
- the GraphQL API under `/api/graphql`
- live preview for the landing page
Payload is not replacing Convex.
- Convex still handles auth, backend logic, realtime data, files, and app workflows
- Payload owns marketing content editing inside the Next app
## High-Level Architecture
### Core config
Payload is configured in `apps/next/src/payload.config.ts`.
Important pieces there:
- `postgresAdapter(...)` connects Payload to Postgres via `PAYLOAD_DB_URL`
- `secret: env.PAYLOAD_SECRET` secures Payload
- `collections: [Users]` currently registers only the Payload admin user collection
- `globals: [LandingPage]` registers the editable landing-page global
- `lexicalEditor()` enables the Payload editor setup
### Current Payload data model
This template is intentionally small right now.
Current Payload entities:
- `users` collection in `apps/next/src/payload/collections/users.ts`
- used by Payload admin itself
- `landing-page` global in `apps/next/src/payload/globals/landing-page.ts`
- stores the homepage layout as a block list
There is no `pages` collection in this template yet.
That means the current pattern is:
- one global for one marketing page
- one frontend route that reads that global
- reusable block schemas that editors can reorder inside the global
## How the Landing Page Works
### Schema side
The landing page is defined by:
- `apps/next/src/payload/globals/landing-page.ts`
- `apps/next/src/payload/globals/landing-page-blocks.ts`
`landing-page.ts` defines a single global with a `layout` field of type `blocks`.
`landing-page-blocks.ts` defines the actual editable block types, including:
- `hero`
- `logoCloud`
- `features`
- `stats`
- `techStack`
- `testimonials`
- `pricing`
- `faq`
- `cta`
### Frontend side
The frontend route is:
- `apps/next/src/app/(frontend)/page.tsx`
That route calls:
- `apps/next/src/lib/payload/get-landing-page-content.ts`
That helper fetches the `landing-page` global from Payload and merges it with fallback
content from:
- `apps/next/src/components/landing/content.ts`
That fallback layer is important. It means the page can still render even if:
- the Payload DB is empty
- an editor saves partial content
- a newly added field is missing from older content
### Rendering flow
The homepage flow is:
1. `/` loads in `apps/next/src/app/(frontend)/page.tsx`
2. the page checks whether `?preview=true` is enabled
3. `getLandingPageContent(isPreview)` fetches the `landing-page` global
4. the fetched global is merged with defaults from `apps/next/src/components/landing/content.ts`
5. `LandingPageBuilder` renders the normalized block data
6. `RefreshRouteOnSave` keeps preview mode refreshed after saves
## Live Preview
Live preview is configured in:
- `apps/next/src/payload/globals/landing-page.ts`
The preview URL is:
- `/?preview=true`
The frontend bridge is:
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
That component uses Payload's live-preview utilities plus Next's router refresh so
saved changes show up in the preview iframe.
Important requirement:
- `NEXT_PUBLIC_SITE_URL` must point to the correct frontend origin
If preview appears blank, stale, or disconnected, that is one of the first values to
check.
## Environment Variables
Payload depends on these env vars:
- `PAYLOAD_SECRET`
- `PAYLOAD_DB_URL`
- `NEXT_PUBLIC_SITE_URL`
Why they matter:
- `PAYLOAD_SECRET` secures Payload
- `PAYLOAD_DB_URL` connects Payload to Postgres
- `NEXT_PUBLIC_SITE_URL` is used by live preview and frontend URL generation
All of them live in the single root `/.env` file.
## Adding a New Landing-Page Block
If you want editors to control a new section type, add a new block.
### 1. Add the block schema
Update:
- `apps/next/src/payload/globals/landing-page-blocks.ts`
Add a new block object to the `landingPageBlocks` array.
### 2. Extend the frontend content types
Update:
- `apps/next/src/components/landing/content.ts`
You usually need to:
- add the new block TypeScript shape
- add default content for it
- add sanitizing / merging logic for it
- include it in the landing-page block union
### 3. Teach the landing-page builder to render it
Update the landing-page rendering layer in the landing components so the new block type
actually appears on the page.
If you add schema without renderer support, editors can save the block but the frontend
will not know what to do with it.
### 4. Regenerate generated Payload files if needed
Useful commands:
```bash
cd apps/next
bun with-env bunx payload generate:types --config src/payload.config.ts
bun with-env bunx payload generate:db-schema --config src/payload.config.ts
```
That refreshes:
- `apps/next/payload-types.ts`
- `apps/next/src/payload-generated-schema.ts`
Do not hand-edit those files.
## Copy-Paste Block Template
Use this as a starting point when you want a new landing-page block.
```ts
{
slug: 'exampleBlock',
labels: {
singular: 'Example Block',
plural: 'Example Blocks',
},
fields: [
{
name: 'heading',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
{
name: 'items',
type: 'array',
fields: [
{
name: 'label',
type: 'text',
required: true,
},
],
},
],
}
```
After adding it:
1. add the block to `landingPageBlocks`
2. add the TypeScript/content shape in `apps/next/src/components/landing/content.ts`
3. add fallback content and sanitizers
4. add rendering support in the landing components
## Copy-Paste Global-Backed Route Template
Use this when you want another singular Payload-managed page in this template and you do
not need a full `pages` collection yet.
This is the best next step for one-off pages like `about`, `contact`, or `pricing`.
```tsx
import { draftMode } from 'next/headers';
import { notFound } from 'next/navigation';
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
import { getPayloadClient } from '@/lib/payload/get-payload';
const AboutPage = async () => {
const { isEnabled: isPreview } = await draftMode();
const payload = await getPayloadClient();
const aboutPage = await payload.findGlobal({
slug: 'about-page',
draft: isPreview,
});
if (!aboutPage) return notFound();
return (
<main>
{isPreview ? <RefreshRouteOnSave /> : null}
{/* render the normalized Payload data here */}
</main>
);
};
export default AboutPage;
```
For this pattern you would also:
1. add a new global in `apps/next/src/payload/globals/`
2. register it in `apps/next/src/payload.config.ts`
3. create the fetch/normalize helper in `apps/next/src/lib/payload/`
4. create fallback content in the relevant component/content files
## When To Add Another Global vs. A `pages` Collection
### Add another global when:
- the page is singular
- the route is fixed
- the content model is custom
- you only need a few editable marketing pages
Examples:
- homepage
- contact page
- about page
- pricing page
### Add a `pages` collection when:
- you need many CMS-managed pages
- page routing should be slug-driven
- many pages share the same block system
- editors should be able to create pages without new code for each route
Examples:
- service pages
- case studies
- marketing subpages
- partner pages
## How To Migrate Another Hardcoded Page To Payload In This Repo
Because this template currently uses a global-backed landing page, the most natural next
migration path is usually another Payload global.
### Step 1: Audit the current page
Identify:
- route path
- metadata
- JSON-LD or structured data
- reusable sections
- what should stay hardcoded vs. what should become editable
### Step 2: Decide the storage model
Ask:
- is this a one-off page? use another global
- is this the beginning of many similar pages? consider creating a `pages` collection
### Step 3: Model the editable fields
Prefer structured fields and blocks over dumping everything into one rich text area.
### Step 4: Add fallback content
Keep the same resilience pattern used by the current landing page:
- define defaults in component/content files
- merge Payload content into those defaults
This avoids blank pages when content is incomplete.
### Step 5: Wire the route
Create a server route in `apps/next/src/app/(frontend)/...` that:
1. reads preview state
2. fetches Payload data
3. normalizes or merges it
4. renders the page
5. includes `RefreshRouteOnSave` in preview mode
### Step 6: Verify preview and admin flow
After migration, verify:
- the route renders normally
- the page updates from the admin
- preview refresh works
- the page still renders when content is partial
## Common Failure Modes
### 1. Homepage renders old or partial content
Check:
- whether the landing-page global actually saved
- whether preview mode is enabled
- whether your fallback merge logic is masking a missing field
### 2. Live preview does not refresh
Check:
- `NEXT_PUBLIC_SITE_URL`
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
- the `admin.livePreview.url` value in the Payload global config
### 3. New block fields do not show up in the frontend
Usually means the schema changed but the frontend data contract did not.
Check:
- `apps/next/src/payload/globals/landing-page-blocks.ts`
- `apps/next/src/components/landing/content.ts`
- the landing-page render path
### 4. Payload admin works but the page looks empty
Usually means one of these:
- the block renderer support is missing
- the global returned data that your merge layer is not handling yet
- the page route is not reading the correct global or preview state
## Important Files At A Glance
- config: `apps/next/src/payload.config.ts`
- payload global: `apps/next/src/payload/globals/landing-page.ts`
- landing-page block schemas: `apps/next/src/payload/globals/landing-page-blocks.ts`
- payload client helper: `apps/next/src/lib/payload/get-payload.ts`
- landing-page fetch helper: `apps/next/src/lib/payload/get-landing-page-content.ts`
- landing-page content defaults: `apps/next/src/components/landing/content.ts`
- landing-page route: `apps/next/src/app/(frontend)/page.tsx`
- preview refresh bridge: `apps/next/src/components/payload/refresh-route-on-save.tsx`
- generated types: `apps/next/payload-types.ts`
- generated db schema: `apps/next/src/payload-generated-schema.ts`
## Practical Rule Of Thumb
If the task is “make marketing content editable,” reach for Payload.
If the task is “build application logic, auth, data workflows, or realtime product
features,” it probably belongs in Convex instead.