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

11 KiB

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:

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.

{
  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.

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.