Move to infisical. Create local dev environment. Add ci gates. Modernize repo
This commit is contained in:
+39
-419
@@ -1,424 +1,44 @@
|
||||
# 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
|
||||
# Payload CMS
|
||||
|
||||
Payload is embedded in `apps/next` and manages marketing content. Convex remains
|
||||
the application backend for auth, realtime data, files, and workflows.
|
||||
|
||||
Configuration lives in `apps/next/src/payload.config.ts`. Payload uses
|
||||
`PAYLOAD_SECRET`, `PAYLOAD_DB_URL`, and `NEXT_PUBLIC_SITE_URL` from the validated
|
||||
environment contract. Local `dev` values come from Infisical and point Payload
|
||||
to Postgres at `localhost:5432`, because Next runs on the host.
|
||||
|
||||
Start the isolated services with `bun db:up`, then run `bun dev:next`. The local
|
||||
database persists across `bun db:down`; `bun db:down:wipe` removes it. Local
|
||||
Convex data is stored in a separate volume and Convex does not receive
|
||||
`POSTGRES_URL` by default.
|
||||
|
||||
To refresh the local seed snapshot from current staging/production-derived
|
||||
Payload content, stop Next and run `bun db:sync:payload`. The command uses
|
||||
staging only as a read-only dump source, verifies the restore destination is
|
||||
localhost, writes `.local/payload-staging.dump`, and force-applies it locally.
|
||||
Normal `bun db:up` never contacts staging; it only restores that local snapshot
|
||||
when `.local/payload-seed-state.env` is absent or does not match the current
|
||||
snapshot. The marker stays outside Postgres so Payload schema push does not see
|
||||
an unmanaged table. The snapshot is gitignored and contains sensitive
|
||||
production-derived records, including Payload users and password hashes. Do not
|
||||
share or commit it.
|
||||
|
||||
The public landing page is in `apps/next/src/app/(frontend)/page.tsx`; schemas
|
||||
are under `apps/next/src/payload`, and rendering components are under
|
||||
`apps/next/src/components/landing`. Custom routes belong in `(frontend)`, not
|
||||
the generated `(payload)` route group.
|
||||
|
||||
Do not manually edit Payload-generated routes, layouts, import maps, or
|
||||
`apps/next/payload-types.ts`. When schema changes require regeneration, run the
|
||||
Payload generators through the package's environment wrapper:
|
||||
|
||||
```sh
|
||||
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.
|
||||
For staging, use `INFISICAL_ENV=staging`; CI uses temporary Gitea-injected
|
||||
configuration and never Infisical. Production Payload may use the existing
|
||||
external/VPN Postgres URL or the commented optional Compose Postgres service.
|
||||
|
||||
Reference in New Issue
Block a user