# 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 (
{isPreview ? : null} {/* render the normalized Payload data here */}
); }; 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.