Update all docs & md files
This commit is contained in:
132
AGENTS.md
132
AGENTS.md
@@ -5,6 +5,10 @@ repository. Read it fully before making any changes. It covers architecture, pat
|
||||
constraints, and known issues — everything you need to work effectively without
|
||||
breaking things or introducing inconsistencies.
|
||||
|
||||
This file is a living document. If your work changes architecture, workflows,
|
||||
deployment scripts, Payload structure, important file paths, or known issues, update
|
||||
`AGENTS.md` in the same task so the next agent inherits the right context.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
@@ -294,6 +298,10 @@ The root `.env` is gitignored. The root `.env.example` is the committed template
|
||||
shows all required variable names without real values. **Keep `.env.example` up to
|
||||
date whenever you add a new env var.**
|
||||
|
||||
The root `/.env` is also the only required Docker Compose env file. The helper scripts
|
||||
in `scripts/` load this same root file before calling `docker compose`, so do not
|
||||
create or rely on a separate `docker/.env`.
|
||||
|
||||
### Complete variable reference
|
||||
|
||||
| Variable | Used By | Purpose | Sync to Convex? |
|
||||
@@ -309,6 +317,19 @@ date whenever you add a new env var.**
|
||||
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Next.js build | Sentry project name | No |
|
||||
| `PAYLOAD_SECRET` | Payload CMS | Payload application secret | No |
|
||||
| `PAYLOAD_DB_URL` | Payload CMS | Postgres connection string used by Payload | No |
|
||||
| `NETWORK` | Docker Compose | External Docker network name | No |
|
||||
| `NEXT_CONTAINER_NAME` | Docker Compose | Next.js container name | No |
|
||||
| `NEXT_DOMAIN` | Docker Compose | Public Next.js domain used by compose interpolation | No |
|
||||
| `BACKEND_CONTAINER_NAME` | Docker Compose | Convex backend container name | No |
|
||||
| `DASHBOARD_CONTAINER_NAME` | Docker Compose | Convex dashboard container name | No |
|
||||
| `BACKEND_DOMAIN` | Docker Compose | Public backend domain used in compose interpolation | No |
|
||||
| `DASHBOARD_DOMAIN` | Docker Compose | Public dashboard domain used in compose interpolation | No |
|
||||
| `INSTANCE_NAME` | Docker Compose | Self-hosted Convex instance name | No |
|
||||
| `INSTANCE_SECRET` | Docker Compose | Self-hosted Convex instance secret | No |
|
||||
| `CONVEX_CLOUD_ORIGIN` | Docker Compose | Public Convex API origin | No |
|
||||
| `CONVEX_SITE_ORIGIN` | Docker Compose | Public Convex site/auth origin | No |
|
||||
| `NEXT_PUBLIC_DEPLOYMENT_URL` | Docker Compose | Deployment URL passed to Convex dashboard | No |
|
||||
| `POSTGRES_URL` | Docker Compose | Self-hosted Convex Postgres connection string | No |
|
||||
| `CONVEX_SELF_HOSTED_URL` | Convex CLI | URL of the self-hosted Convex backend | No |
|
||||
| `CONVEX_SELF_HOSTED_ADMIN_KEY` | Convex CLI | Admin key for the self-hosted backend | No |
|
||||
| `CONVEX_SITE_URL` | Convex Auth | CORS domain for token exchange (localhost:3000 for dev) | Yes (Dashboard) |
|
||||
@@ -384,6 +405,9 @@ Or set it directly in the Convex Dashboard.
|
||||
|
||||
Also update `/.env.example` with the new variable (empty value or a placeholder).
|
||||
|
||||
If the variable is used by Docker Compose or one of the helper scripts in `scripts/`,
|
||||
it still belongs in the same root `/.env`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependency Management — The Catalog System
|
||||
@@ -781,16 +805,43 @@ pieces are:
|
||||
generated admin/API files
|
||||
- **`apps/next/src/payload/collections/`** and `apps/next/src/payload/globals/` — the
|
||||
editable Payload schema files you should actually work in
|
||||
- **`apps/next/src/payload/globals/landing-page-blocks.ts`** — the current block schema
|
||||
definitions for the landing-page global
|
||||
- **`apps/next/src/lib/payload/`** — server helpers for obtaining the cached Payload
|
||||
client and fetching CMS content
|
||||
- **`apps/next/src/components/payload/refresh-route-on-save.tsx`** — enables live
|
||||
preview refreshes for the landing page when `?preview=true` is present
|
||||
|
||||
Current state of the template:
|
||||
|
||||
- Payload currently powers the homepage via the `landing-page` global, not a `pages`
|
||||
collection
|
||||
- the editable block definitions live in
|
||||
`apps/next/src/payload/globals/landing-page-blocks.ts`
|
||||
- the frontend reads the saved global through
|
||||
`apps/next/src/lib/payload/get-landing-page-content.ts`
|
||||
- saved Payload content is merged with resilient defaults from
|
||||
`apps/next/src/components/landing/content.ts`, so the page still renders even when
|
||||
content is partial or missing fields
|
||||
- live preview works by loading `/` with `?preview=true` and rendering
|
||||
`RefreshRouteOnSave`
|
||||
|
||||
If you need a deeper walkthrough, read `docs/payload-cms.md`. That file should stay in
|
||||
sync with the actual Payload implementation in this repo.
|
||||
|
||||
The current homepage is backed by the `landing-page` Payload global. The server page at
|
||||
`apps/next/src/app/(frontend)/page.tsx` calls
|
||||
`apps/next/src/lib/payload/get-landing-page-content.ts`, which merges saved Payload
|
||||
content with defaults from `apps/next/src/components/landing/content.ts`.
|
||||
|
||||
When adding more Payload-managed marketing content in this template, there are two
|
||||
reasonable paths:
|
||||
|
||||
1. add another Payload global plus a matching frontend route if the page is singular
|
||||
and custom
|
||||
2. introduce a `pages` collection once you need many reusable CMS-managed pages with
|
||||
shared routing patterns
|
||||
|
||||
### The dual Convex provider setup
|
||||
|
||||
Two providers are required and both must be present:
|
||||
@@ -1148,22 +1199,15 @@ The Convex backend and dashboard are long-running and rarely need to be rebuilt.
|
||||
The typical workflow when you've made changes to the Next.js app:
|
||||
|
||||
```bash
|
||||
# On the production server (via SSH)
|
||||
cd docker/
|
||||
|
||||
# Build the Next.js app with current source
|
||||
sudo docker compose build next-app
|
||||
|
||||
# Once build succeeds, bring up the new container
|
||||
sudo docker compose up -d next-app
|
||||
./scripts/build-next-app
|
||||
```
|
||||
|
||||
If you need to start everything from scratch:
|
||||
|
||||
```bash
|
||||
sudo docker compose up -d convex-backend convex-dashboard
|
||||
./scripts/docker-compose up -d backend dashboard
|
||||
# Wait for backend to be healthy, then:
|
||||
sudo docker compose up -d next-app
|
||||
./scripts/docker-compose up -d next
|
||||
```
|
||||
|
||||
### The `.dockerignore` situation (known issue)
|
||||
@@ -1180,11 +1224,22 @@ Do not un-comment those lines without understanding the full implications — re
|
||||
`.env` from the build context would break Sentry source map uploads and
|
||||
`NEXT_PUBLIC_*` variable baking into the client bundle.
|
||||
|
||||
**Note on `docker/.env`:** This file is entirely separate and serves a different
|
||||
purpose. It is read by Docker Compose itself (not the build) to interpolate the
|
||||
`${VARIABLE}` placeholders in `compose.yml` — things like container names, network
|
||||
names, and Convex origin URLs. It is not the same as the root `/.env` and the two
|
||||
are not interchangeable.
|
||||
Docker Compose now reads from the root `/.env`, and the helper scripts in `scripts/`
|
||||
wrap Compose so you do not need to repeatedly pass `--env-file` and `-f`.
|
||||
|
||||
### Docker helper scripts
|
||||
|
||||
The preferred deployment workflow now uses the helper scripts in `scripts/`:
|
||||
|
||||
- `scripts/docker-compose` — wrapper for custom Compose commands using the repo's root
|
||||
`/.env` and `docker/compose.yml`
|
||||
- `scripts/build-next-app` — build and restart the Next container
|
||||
- `scripts/update-next-app` — `git pull`, then build and restart the Next container
|
||||
- `scripts/update-convex` — pull and restart the backend/dashboard containers
|
||||
- `scripts/generate-convex-admin-key` — run the backend container's admin key script
|
||||
|
||||
The `scripts/docker-compose` wrapper also translates short aliases like `next`,
|
||||
`backend`, and `dashboard` into the container names defined in the root `/.env`.
|
||||
|
||||
### Convex data persistence
|
||||
|
||||
@@ -1192,13 +1247,12 @@ are not interchangeable.
|
||||
This directory is gitignored. Back it up before any server migrations, restarts, or
|
||||
image updates.
|
||||
|
||||
### `generate_convex_admin_key`
|
||||
### `generate-convex-admin-key`
|
||||
|
||||
A bash script in `docker/` that generates the admin key from a running Convex backend:
|
||||
A bash script in `scripts/` generates the admin key from a running Convex backend:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
./generate_convex_admin_key
|
||||
./scripts/generate-convex-admin-key
|
||||
```
|
||||
|
||||
This runs the key generation script inside the `convex-backend` container and prints
|
||||
@@ -1281,11 +1335,13 @@ deployment), these things should be updated:
|
||||
repository URL, and quick-start text
|
||||
- `apps/next/src/payload/globals/landing-page.ts` — update or replace the current landing
|
||||
page global schema
|
||||
- `apps/next/src/payload/globals/landing-page-blocks.ts` — update the landing-page block
|
||||
definitions if the marketing content model changes
|
||||
- `apps/next/src/payload.config.ts` — add/remove Payload collections and globals as the
|
||||
project evolves
|
||||
- Root `package.json` — update the workspace `name` field to fit the new project
|
||||
- `/.env` — fill out all values for the new deployment
|
||||
- `docker/.env` — fill out container names and domain URLs
|
||||
- `/.env` — also fill out the Docker Compose container names and domain URLs
|
||||
|
||||
---
|
||||
|
||||
@@ -1323,12 +1379,18 @@ deployment), these things should be updated:
|
||||
|
||||
### Adding or changing Payload content
|
||||
|
||||
1. Edit the relevant schema file in `apps/next/src/payload/collections/` or
|
||||
`apps/next/src/payload/globals/`
|
||||
2. Register the collection/global in `apps/next/src/payload.config.ts`
|
||||
3. Fetch the content from server code via `apps/next/src/lib/payload/`
|
||||
1. For the current homepage, edit the landing-page global or its block definitions in
|
||||
`apps/next/src/payload/globals/landing-page.ts` and
|
||||
`apps/next/src/payload/globals/landing-page-blocks.ts`
|
||||
2. Keep the frontend data contract in sync with
|
||||
`apps/next/src/components/landing/content.ts`
|
||||
3. Fetch or merge the content through `apps/next/src/lib/payload/`
|
||||
4. If you need fresh Payload types, regenerate `apps/next/payload-types.ts`
|
||||
5. Do not hand-edit the generated files in `apps/next/src/app/(payload)/`
|
||||
5. If you change the database schema shape, regenerate
|
||||
`apps/next/src/payload-generated-schema.ts`
|
||||
6. Do not hand-edit the generated files in `apps/next/src/app/(payload)/`
|
||||
7. If you introduce additional Payload-managed pages or collections, update
|
||||
`docs/payload-cms.md` and this `AGENTS.md` file in the same task
|
||||
|
||||
### Adding a new environment variable
|
||||
|
||||
@@ -1443,12 +1505,13 @@ on Gitea) for automated lint and typecheck on pull requests.
|
||||
Currently:
|
||||
|
||||
- `packages/backend/scripts/generateKeys.mjs` — generates JWT keys (run manually)
|
||||
- `docker/generate_convex_admin_key` — bash script to get the admin key from Docker
|
||||
- `scripts/generate-convex-admin-key` — bash script to get the admin key from Docker
|
||||
- `scripts/docker-compose` and related helpers — wrap Docker Compose with the root
|
||||
`/.env`
|
||||
|
||||
These are separate workflows that both need to be run once when setting up a new
|
||||
deployment. Explore combining these into a unified setup script — potentially one that
|
||||
generates the keys AND automatically syncs them to Convex in one step, possibly
|
||||
moving everything to `docker/scripts/`.
|
||||
generates the keys AND automatically syncs them to Convex in one step.
|
||||
|
||||
---
|
||||
|
||||
@@ -1499,10 +1562,11 @@ bun dev # Runs convex dev (push functions + watch)
|
||||
bun setup # Runs convex dev --until-success (push once then exit)
|
||||
bun with-env npx convex env set VAR "value" # Sync env var to Convex deployment
|
||||
|
||||
# ── Docker (run from docker/) ─────────────────────────────────────────────────
|
||||
sudo docker compose up -d convex-backend convex-dashboard # Start Convex
|
||||
sudo docker compose build next-app # Build Next.js image
|
||||
sudo docker compose up -d next-app # Deploy Next.js
|
||||
sudo docker compose logs -f # Stream all logs
|
||||
./generate_convex_admin_key # Get admin key
|
||||
# ── Docker / Deployment ───────────────────────────────────────────────────────
|
||||
./scripts/docker-compose ps # Show compose status
|
||||
./scripts/docker-compose up -d backend dashboard # Start Convex
|
||||
./scripts/build-next-app # Build and deploy Next.js app
|
||||
./scripts/update-next-app # git pull + rebuild Next.js app
|
||||
./scripts/update-convex # Pull and restart Convex services
|
||||
./scripts/generate-convex-admin-key # Get admin key
|
||||
```
|
||||
|
||||
119
README.md
119
README.md
@@ -1,6 +1,8 @@
|
||||
# Convex Turbo Monorepo
|
||||
|
||||
A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex backend. Built with TypeScript, Tailwind CSS, and modern tooling.
|
||||
A production-ready Turborepo starter with Next.js, Expo, self-hosted Convex, and
|
||||
embedded Payload CMS for live-editable marketing content. Built with TypeScript,
|
||||
Tailwind CSS, and modern tooling.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +24,7 @@ A production-ready Turborepo starter with Next.js, Expo, and self-hosted Convex
|
||||
- **Framework:** Next.js 16 (App Router) + Expo 54
|
||||
- **Backend:** Convex (self-hosted)
|
||||
- **Auth:** @convex-dev/auth with Authentik OAuth & Password providers
|
||||
- **CMS:** Payload CMS embedded inside the Next.js app
|
||||
- **Styling:** Tailwind CSS v4 + shadcn/ui
|
||||
- **Language:** TypeScript (strict mode)
|
||||
- **Package Manager:** Bun
|
||||
@@ -64,39 +67,45 @@ git remote add origin https://your-git-host.com/your/new-repo.git
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Configure the Docker Environment
|
||||
|
||||
The `docker/` directory contains everything needed to run the Convex backend and the
|
||||
Next.js app in production.
|
||||
### Step 2 — Configure the Single Root Environment File
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `docker/.env` and fill in your values. The variables below the Next.js app section
|
||||
control the Convex containers — you'll need to choose:
|
||||
The root `/.env` is the single required env file for this repo. It is used for:
|
||||
|
||||
- app/package runtime env vars
|
||||
- Next.js build-time env vars
|
||||
- Payload env vars
|
||||
- Docker Compose interpolation
|
||||
- helper scripts in `scripts/`
|
||||
|
||||
Fill it in with your values. The Docker-related variables in the same file control the
|
||||
Convex containers and Next.js container — you'll need to choose:
|
||||
|
||||
- `INSTANCE_NAME` — a unique name for your Convex instance
|
||||
- `INSTANCE_SECRET` — a secret string (generate something random)
|
||||
- `CONVEX_CLOUD_ORIGIN` — the public HTTPS URL for your Convex backend API (e.g. `https://api.convex.example.com`)
|
||||
- `CONVEX_SITE_ORIGIN` — the public HTTPS URL for Convex Auth HTTP routes (e.g. `https://api.convex.example.com`)
|
||||
- `NEXT_PUBLIC_DEPLOYMENT_URL` — the URL for the Convex dashboard (e.g. `https://dashboard.convex.example.com`)
|
||||
- `NEXT_CONTAINER_NAME`, `BACKEND_CONTAINER_NAME`, and `DASHBOARD_CONTAINER_NAME` — the Docker service container names
|
||||
|
||||
Do not create or rely on a separate `docker/.env`.
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Start the Convex Containers
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
sudo docker compose up -d convex-backend convex-dashboard
|
||||
./scripts/docker-compose up -d backend dashboard
|
||||
```
|
||||
|
||||
Wait a moment for `convex-backend` to pass its health check, then verify both
|
||||
containers are running:
|
||||
|
||||
```bash
|
||||
sudo docker compose ps
|
||||
./scripts/docker-compose ps
|
||||
```
|
||||
|
||||
Reverse-proxy the two Convex services through nginx-proxy-manager (or your preferred
|
||||
@@ -110,8 +119,7 @@ can proceed.
|
||||
With the backend container running, generate the admin key:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
./generate_convex_admin_key
|
||||
./scripts/generate-convex-admin-key
|
||||
```
|
||||
|
||||
Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the root
|
||||
@@ -119,21 +127,16 @@ Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Configure Root Environment Variables
|
||||
### Step 5 — Finish Configuring Root Environment Variables
|
||||
|
||||
Create the root `.env` file:
|
||||
|
||||
```bash
|
||||
# From the repo root
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Fill out all values in `/.env`:
|
||||
Fill out all values in root `/.env`:
|
||||
|
||||
```bash
|
||||
# Next.js
|
||||
NODE_ENV=development
|
||||
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
|
||||
PAYLOAD_SECRET= # openssl rand -hex 32
|
||||
PAYLOAD_DB_URL=postgresql://user:password@host:5432/db_name
|
||||
NEXT_PUBLIC_SITE_URL=https://example.com
|
||||
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
|
||||
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
|
||||
@@ -154,6 +157,18 @@ USESEND_FROM_EMAIL=My App <noreply@example.com>
|
||||
AUTH_AUTHENTIK_ID=
|
||||
AUTH_AUTHENTIK_SECRET=
|
||||
AUTH_AUTHENTIK_ISSUER=https://auth.example.com/application/o/my-app/
|
||||
|
||||
# Docker Compose
|
||||
NETWORK=nginx-bridge
|
||||
NEXT_CONTAINER_NAME=next-app
|
||||
BACKEND_CONTAINER_NAME=convex-backend
|
||||
DASHBOARD_CONTAINER_NAME=convex-dashboard
|
||||
INSTANCE_NAME=convex
|
||||
INSTANCE_SECRET=
|
||||
CONVEX_CLOUD_ORIGIN=https://api.convex.example.com
|
||||
CONVEX_SITE_ORIGIN=https://convex.convex.example.com
|
||||
NEXT_PUBLIC_DEPLOYMENT_URL=https://dashboard.convex.example.com
|
||||
POSTGRES_URL=postgresql://user:password@host:5432/convex
|
||||
```
|
||||
|
||||
---
|
||||
@@ -209,6 +224,26 @@ bun dev # All apps (Next.js + Expo + Backend)
|
||||
- **Next.js:** http://localhost:3000
|
||||
- **Convex Dashboard:** https://dashboard.convex.example.com
|
||||
|
||||
### Docker Helper Scripts
|
||||
|
||||
This repo includes helper scripts so you do not have to keep passing `--env-file` and
|
||||
`-f` manually to Docker Compose.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
./scripts/docker-compose ps
|
||||
./scripts/docker-compose up -d backend dashboard
|
||||
./scripts/docker-compose up -d next
|
||||
./scripts/build-next-app
|
||||
./scripts/update-next-app
|
||||
./scripts/update-convex
|
||||
./scripts/generate-convex-admin-key
|
||||
```
|
||||
|
||||
`./scripts/docker-compose` also accepts short service aliases like `next`, `backend`,
|
||||
and `dashboard` and maps them to the container names from root `/.env`.
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
@@ -278,7 +313,13 @@ convex-monorepo/
|
||||
├── docker/ # Self-hosted deployment
|
||||
│ ├── compose.yml
|
||||
│ ├── Dockerfile
|
||||
│ └── .env.example
|
||||
│ └── data/ # Convex data volume (gitignored in real use)
|
||||
│
|
||||
├── scripts/ # Docker/deployment helper scripts using root .env
|
||||
│ ├── docker-compose
|
||||
│ ├── build-next-app
|
||||
│ ├── update-next-app
|
||||
│ └── update-convex
|
||||
│
|
||||
├── turbo.json # Turborepo configuration
|
||||
└── package.json # Root workspace & catalogs
|
||||
@@ -298,6 +339,7 @@ convex-monorepo/
|
||||
### Next.js App
|
||||
|
||||
- **App Router:** Next.js 16 with React Server Components
|
||||
- **Editable Marketing Content:** Payload-backed landing page with live preview
|
||||
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
|
||||
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
|
||||
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
|
||||
@@ -327,26 +369,18 @@ convex-monorepo/
|
||||
### Production Deployment (Docker)
|
||||
|
||||
Once the Convex containers are running (they only need to be started once), deploying
|
||||
a new version of the Next.js app is a two-command workflow:
|
||||
a new version of the Next.js app is a one-command workflow:
|
||||
|
||||
```bash
|
||||
# SSH onto your server, then:
|
||||
cd docker/
|
||||
|
||||
# Build the new image
|
||||
sudo docker compose build next-app
|
||||
|
||||
# Deploy it
|
||||
sudo docker compose up -d next-app
|
||||
./scripts/build-next-app
|
||||
```
|
||||
|
||||
To start all services from scratch:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
sudo docker compose up -d convex-backend convex-dashboard
|
||||
./scripts/docker-compose up -d backend dashboard
|
||||
# Wait for backend health check to pass, then:
|
||||
sudo docker compose up -d next-app
|
||||
./scripts/docker-compose up -d next
|
||||
```
|
||||
|
||||
**Services:**
|
||||
@@ -359,11 +393,10 @@ sudo docker compose up -d next-app
|
||||
|
||||
### Production Checklist
|
||||
|
||||
- [ ] Fill out `docker/.env` with your domain names and secrets
|
||||
- [ ] Fill out root `/.env` with all app, Payload, and Docker Compose values
|
||||
- [ ] Start `convex-backend` and `convex-dashboard` containers
|
||||
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./generate_convex_admin_key`
|
||||
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./scripts/generate-convex-admin-key`
|
||||
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
|
||||
- [ ] Fill out root `/.env` with all environment variables
|
||||
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
|
||||
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
|
||||
- [ ] Build and start the `next-app` container
|
||||
@@ -374,6 +407,7 @@ sudo docker compose up -d next-app
|
||||
## Documentation
|
||||
|
||||
- **[AGENTS.md](./AGENTS.md)** — Comprehensive guide for AI agents & developers
|
||||
- **[docs/payload-cms.md](./docs/payload-cms.md)** — How Payload is wired up and how to extend it in this template
|
||||
- **[Convex Docs](https://docs.convex.dev)** — Official Convex documentation
|
||||
- **[Turborepo Docs](https://turbo.build/repo/docs)** — Turborepo documentation
|
||||
- **[Next.js Docs](https://nextjs.org/docs)** — Next.js documentation
|
||||
@@ -402,11 +436,16 @@ import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
|
||||
### Docker containers won't start
|
||||
|
||||
1. Check Docker logs: `sudo docker compose logs`
|
||||
2. Verify environment variables in `docker/.env`
|
||||
1. Check Docker logs: `./scripts/docker-compose logs`
|
||||
2. Verify environment variables in root `/.env`
|
||||
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
|
||||
4. Check that the required ports (3210, 6791) are not already in use
|
||||
|
||||
### Only one `.env` file matters
|
||||
|
||||
Use the root `/.env` for everything in this repo, including Docker helper scripts and
|
||||
Docker Compose interpolation. Do not create or rely on a separate `docker/.env`.
|
||||
|
||||
### Auth doesn't work in production
|
||||
|
||||
Make sure `CONVEX_SITE_URL` is set to your production Next.js URL in the **Convex
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
# Payload CMS In St Pete IT
|
||||
# Payload CMS In `convex-monorepo`
|
||||
|
||||
This document explains how Payload CMS is integrated into the repo, how the landing and
|
||||
service pages work, how to add new Payload-managed pages, and how to migrate existing
|
||||
hardcoded pages into the current block-based setup.
|
||||
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.
|
||||
|
||||
## What Payload Is Responsible For
|
||||
## Current Scope
|
||||
|
||||
Payload currently manages the editable marketing content layer inside the Next.js app.
|
||||
That means:
|
||||
Payload currently powers the editable marketing layer inside the Next.js app.
|
||||
|
||||
Today that means:
|
||||
|
||||
- the landing page at `/`
|
||||
- the contact page at `/contact`
|
||||
- the service pages at `/services/[slug]`
|
||||
- shared marketing settings in the `site-settings` global
|
||||
- the admin UI at `/admin`
|
||||
- the REST API under `/api`
|
||||
- live preview and route refresh when editors save or publish changes
|
||||
- the GraphQL API under `/api/graphql`
|
||||
- live preview for the landing page
|
||||
|
||||
Payload is not replacing Convex. Convex still handles the product backend: auth,
|
||||
tickets, invoices, appointments, portal data, and admin operations. Payload only owns
|
||||
the CMS side for page content.
|
||||
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
|
||||
|
||||
@@ -29,118 +28,109 @@ Payload is configured in `apps/next/src/payload.config.ts`.
|
||||
|
||||
Important pieces there:
|
||||
|
||||
- `postgresAdapter(...)` points Payload at Postgres through `PAYLOAD_DB_URL`
|
||||
- `secret: env.PAYLOAD_SECRET` enables Payload auth/session security
|
||||
- `collections: [Media, Pages]` registers the current CMS collections
|
||||
- `globals: [SiteSettings]` registers shared settings
|
||||
- `admin.livePreview` enables live preview for the `pages` collection
|
||||
- `typescript.outputFile` writes generated types to `apps/next/payload-types.ts`
|
||||
- `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
|
||||
|
||||
### Collections and globals
|
||||
### Current Payload data model
|
||||
|
||||
Current CMS entities:
|
||||
This template is intentionally small right now.
|
||||
|
||||
- `pages` in `apps/next/src/payload/collections/pages.ts`
|
||||
- stores both the landing page and service pages
|
||||
- uses `pageType` to distinguish `landing` vs `service`
|
||||
- stores block layout in `layout`
|
||||
- stores SEO fields in `seo`
|
||||
- stores service-specific structured data in `structuredData`
|
||||
- `media` in `apps/next/src/payload/collections/media.ts`
|
||||
- image uploads used by blocks and SEO
|
||||
- `site-settings` in `apps/next/src/payload/globals/site-settings.ts`
|
||||
- shared business info and service-page CTA settings
|
||||
Current Payload entities:
|
||||
|
||||
### Block system
|
||||
- `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
|
||||
|
||||
Payload page content is built from reusable blocks.
|
||||
There is no `pages` collection in this template yet.
|
||||
|
||||
Schema side:
|
||||
That means the current pattern is:
|
||||
|
||||
- `apps/next/src/payload/blocks/*.ts`
|
||||
- exported via `apps/next/src/payload/blocks/index.ts`
|
||||
- one global for one marketing page
|
||||
- one frontend route that reads that global
|
||||
- reusable block schemas that editors can reorder inside the global
|
||||
|
||||
Render side:
|
||||
## How the Landing Page Works
|
||||
|
||||
- `apps/next/src/components/payload/blocks/*.tsx`
|
||||
- selected by `apps/next/src/components/payload/blocks/render-blocks.tsx`
|
||||
### Schema side
|
||||
|
||||
The rule is simple: every Payload block needs both parts.
|
||||
The landing page is defined by:
|
||||
|
||||
- schema block: defines the fields editors can fill in
|
||||
- renderer block: turns that block data into frontend UI
|
||||
- `apps/next/src/payload/globals/landing-page.ts`
|
||||
- `apps/next/src/payload/globals/landing-page-blocks.ts`
|
||||
|
||||
If one side is missing, the admin or the frontend will be incomplete.
|
||||
`landing-page.ts` defines a single global with a `layout` field of type `blocks`.
|
||||
|
||||
### Frontend route flow
|
||||
`landing-page-blocks.ts` defines the actual editable block types, including:
|
||||
|
||||
Landing page route:
|
||||
- `hero`
|
||||
- `logoCloud`
|
||||
- `features`
|
||||
- `stats`
|
||||
- `techStack`
|
||||
- `testimonials`
|
||||
- `pricing`
|
||||
- `faq`
|
||||
- `cta`
|
||||
|
||||
### Frontend side
|
||||
|
||||
The frontend route is:
|
||||
|
||||
- `apps/next/src/app/(frontend)/page.tsx`
|
||||
|
||||
Contact page route:
|
||||
That route calls:
|
||||
|
||||
- `apps/next/src/app/(frontend)/contact/page.tsx`
|
||||
- `apps/next/src/lib/payload/get-landing-page-content.ts`
|
||||
|
||||
Service page route:
|
||||
That helper fetches the `landing-page` global from Payload and merges it with fallback
|
||||
content from:
|
||||
|
||||
- `apps/next/src/app/(frontend)/services/[slug]/page.tsx`
|
||||
- `apps/next/src/components/landing/content.ts`
|
||||
|
||||
Shared server fetch helpers:
|
||||
That fallback layer is important. It means the page can still render even if:
|
||||
|
||||
- `apps/next/src/lib/payload-helpers.tsx`
|
||||
- the Payload DB is empty
|
||||
- an editor saves partial content
|
||||
- a newly added field is missing from older content
|
||||
|
||||
Behavior:
|
||||
### Rendering flow
|
||||
|
||||
1. the route calls `getPageBySlug(...)`
|
||||
2. Payload fetches the matching `pages` document
|
||||
3. the page metadata is generated from `seo` / fallback values
|
||||
4. the page content is rendered through `LivePreviewPage`
|
||||
5. `LivePreviewPage` uses Payload live preview to update content in the editor iframe
|
||||
6. `RefreshRouteOnSave` refreshes the route after save/publish so server-rendered data
|
||||
stays in sync
|
||||
The homepage flow is:
|
||||
|
||||
### Live preview and publish behavior
|
||||
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
|
||||
|
||||
There are two cooperating pieces:
|
||||
## 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/live-preview-page.tsx`
|
||||
- subscribes to Payload live preview messages with `useLivePreview`
|
||||
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
||||
- refreshes the current route after document saves/publishes
|
||||
|
||||
That component uses Payload's live-preview utilities plus Next's router refresh so
|
||||
saved changes show up in the preview iframe.
|
||||
|
||||
Important requirement:
|
||||
|
||||
- `src/proxy.ts` and `src/lib/proxy/ban-sus-ips.ts` must not block valid Payload REST API
|
||||
requests under `/api`
|
||||
- `NEXT_PUBLIC_SITE_URL` must point to the correct frontend origin
|
||||
|
||||
That was a real bug during setup: `PATCH` requests to publish pages were being blocked by
|
||||
the suspicious-method middleware until `/api` writes were explicitly allowed.
|
||||
|
||||
## Seeded Content
|
||||
|
||||
Payload content is seeded from:
|
||||
|
||||
- `apps/next/src/payload/seed/landing-page.ts`
|
||||
- `apps/next/src/payload/seed/service-pages.ts`
|
||||
- `apps/next/src/payload/seed/index.ts`
|
||||
|
||||
Run the seed with:
|
||||
|
||||
```bash
|
||||
cd apps/next
|
||||
bun run seed
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
- updates `site-settings`
|
||||
- creates or updates the `home` landing page
|
||||
- creates or updates the `contact` page
|
||||
- creates or updates the default service pages
|
||||
|
||||
This matters because a fresh Payload database will otherwise return no page documents and
|
||||
the frontend route will 404.
|
||||
If preview appears blank, stale, or disconnected, that is one of the first values to
|
||||
check.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -152,151 +142,68 @@ Payload depends on these env vars:
|
||||
|
||||
Why they matter:
|
||||
|
||||
- `PAYLOAD_SECRET` secures Payload sessions and server behavior
|
||||
- `PAYLOAD_SECRET` secures Payload
|
||||
- `PAYLOAD_DB_URL` connects Payload to Postgres
|
||||
- `NEXT_PUBLIC_SITE_URL` is used by live preview to target the frontend correctly
|
||||
- `NEXT_PUBLIC_SITE_URL` is used by live preview and frontend URL generation
|
||||
|
||||
If live preview points to the wrong place, or publish/save requests appear to work but the
|
||||
preview never updates, this is one of the first things to check.
|
||||
All of them live in the single root `/.env` file.
|
||||
|
||||
## How To Create A New Page Like The Current Ones
|
||||
## Adding a New Landing-Page Block
|
||||
|
||||
This section assumes you want another page managed by the existing `pages` collection.
|
||||
If you want editors to control a new section type, add a new block.
|
||||
|
||||
### Option A: Create a new service page using the existing system
|
||||
### 1. Add the block schema
|
||||
|
||||
This is the simplest case.
|
||||
Update:
|
||||
|
||||
1. Open Payload admin at `/admin`
|
||||
2. Go to the `Pages` collection
|
||||
3. Create a new document
|
||||
4. Set:
|
||||
- `title`
|
||||
- `slug`
|
||||
- `pageType = service`
|
||||
5. Build the `layout` using the existing blocks
|
||||
6. Fill in `seo`
|
||||
7. Fill in `structuredData.serviceName` and `structuredData.serviceDescription`
|
||||
8. Save draft or publish
|
||||
9. Visit `/services/<slug>`
|
||||
- `apps/next/src/payload/globals/landing-page-blocks.ts`
|
||||
|
||||
Why this works without adding a new route:
|
||||
Add a new block object to the `landingPageBlocks` array.
|
||||
|
||||
- the app already has a dynamic route at `apps/next/src/app/(frontend)/services/[slug]/page.tsx`
|
||||
- any `pages` doc with `pageType: 'service'` and a matching slug can render there
|
||||
### 2. Extend the frontend content types
|
||||
|
||||
If the page should exist by default in new environments, also add it to
|
||||
`apps/next/src/payload/seed/service-pages.ts`.
|
||||
Update:
|
||||
|
||||
### Option B: Create another landing-style page with the same block approach
|
||||
- `apps/next/src/components/landing/content.ts`
|
||||
|
||||
If the page is not a service page, decide whether it belongs:
|
||||
You usually need to:
|
||||
|
||||
- in the existing `pages` collection with a new route, or
|
||||
- in a new Payload collection if the content model is materially different
|
||||
- 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
|
||||
|
||||
If it fits `pages`:
|
||||
### 3. Teach the landing-page builder to render it
|
||||
|
||||
1. add or reuse blocks in the `layout`
|
||||
2. create the frontend route that fetches the document by slug
|
||||
3. generate metadata from the document
|
||||
4. render the layout with `LivePreviewPage`
|
||||
5. include `RefreshRouteOnSave`
|
||||
Update the landing-page rendering layer in the landing components so the new block type
|
||||
actually appears on the page.
|
||||
|
||||
Example pattern:
|
||||
If you add schema without renderer support, editors can save the block but the frontend
|
||||
will not know what to do with it.
|
||||
|
||||
```tsx
|
||||
const page = await getPageBySlug('some-slug');
|
||||
### 4. Regenerate generated Payload files if needed
|
||||
|
||||
if (!page) return notFound();
|
||||
Useful commands:
|
||||
|
||||
return (
|
||||
<main>
|
||||
<RefreshRouteOnSave serverURL={env.NEXT_PUBLIC_SITE_URL} />
|
||||
<LivePreviewPage page={page} serverURL={env.NEXT_PUBLIC_SITE_URL} />
|
||||
</main>
|
||||
);
|
||||
```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
|
||||
```
|
||||
|
||||
## Copy-Paste Route Template
|
||||
That refreshes:
|
||||
|
||||
Use this when creating a new non-service Payload-backed page route.
|
||||
- `apps/next/payload-types.ts`
|
||||
- `apps/next/src/payload-generated-schema.ts`
|
||||
|
||||
Adjust these parts:
|
||||
|
||||
- the slug passed to `getPageBySlug(...)`
|
||||
- the metadata fallback values
|
||||
- any JSON-LD you want to inject
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
import { LivePreviewPage } from '@/components/payload/live-preview-page';
|
||||
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
|
||||
import { env } from '@/env';
|
||||
import { generatePageMetadata } from '@/lib/metadata';
|
||||
import { getPageBySlug } from '@/lib/payload-helpers';
|
||||
import { jsonLd } from '@/lib/structured-data';
|
||||
|
||||
export const generateMetadata = async (): Promise<Metadata> => {
|
||||
const page = await getPageBySlug('some-slug');
|
||||
|
||||
if (!page) {
|
||||
return generatePageMetadata({
|
||||
title: 'Fallback Title',
|
||||
description: 'Fallback description.',
|
||||
path: '/some-path',
|
||||
});
|
||||
}
|
||||
|
||||
return generatePageMetadata({
|
||||
title: page.seo?.metaTitle ?? page.title,
|
||||
description: page.seo?.metaDescription ?? 'Fallback description.',
|
||||
path: '/some-path',
|
||||
keywords: page.seo?.keywords?.filter(Boolean) as string[] | undefined,
|
||||
noIndex: page.seo?.noIndex ?? false,
|
||||
});
|
||||
};
|
||||
|
||||
const SomePage = async () => {
|
||||
const page = await getPageBySlug('some-slug');
|
||||
|
||||
if (!page) return notFound();
|
||||
|
||||
return (
|
||||
<main>
|
||||
<RefreshRouteOnSave serverURL={env.NEXT_PUBLIC_SITE_URL} />
|
||||
<Script
|
||||
id='ld-json-some-page'
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonLd({ '@context': 'https://schema.org' }),
|
||||
}}
|
||||
/>
|
||||
<LivePreviewPage page={page} serverURL={env.NEXT_PUBLIC_SITE_URL} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SomePage;
|
||||
```
|
||||
|
||||
If the page does not need JSON-LD, remove the `Script` import and block.
|
||||
|
||||
For service pages, do not create a separate static route unless there is a strong reason.
|
||||
Prefer the existing dynamic route at `apps/next/src/app/(frontend)/services/[slug]/page.tsx`.
|
||||
Do not hand-edit those files.
|
||||
|
||||
## Copy-Paste Block Template
|
||||
|
||||
Use this when you need a new reusable Payload block for a page migration.
|
||||
|
||||
Schema file example:
|
||||
Use this as a starting point when you want a new landing-page block.
|
||||
|
||||
```ts
|
||||
import type { Block } from 'payload';
|
||||
|
||||
export const ExampleBlock: Block = {
|
||||
{
|
||||
slug: 'exampleBlock',
|
||||
labels: {
|
||||
singular: 'Example Block',
|
||||
@@ -304,7 +211,7 @@ export const ExampleBlock: Block = {
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
name: 'heading',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
@@ -324,330 +231,194 @@ export const ExampleBlock: Block = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Renderer example:
|
||||
|
||||
```tsx
|
||||
import type { Page } from '../../../../payload-types';
|
||||
|
||||
type ExampleBlockData = Extract<
|
||||
NonNullable<Page['layout']>[number],
|
||||
{ blockType: 'exampleBlock' }
|
||||
>;
|
||||
|
||||
export const ExampleBlockRenderer = ({
|
||||
block,
|
||||
}: {
|
||||
block: ExampleBlockData;
|
||||
}) => {
|
||||
return (
|
||||
<section>
|
||||
<h2>{block.title}</h2>
|
||||
{block.description && <p>{block.description}</p>}
|
||||
<ul>
|
||||
{block.items?.map((item) => (
|
||||
<li key={item.id}>{item.label}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Registration checklist:
|
||||
|
||||
1. export the schema block from `apps/next/src/payload/blocks/index.ts`
|
||||
2. add the block to `apps/next/src/payload/collections/pages.ts`
|
||||
3. add the renderer to `apps/next/src/components/payload/blocks/render-blocks.tsx`
|
||||
4. regenerate `apps/next/payload-types.ts`
|
||||
|
||||
Type generation command:
|
||||
|
||||
```bash
|
||||
cd apps/next
|
||||
bun with-env bunx payload generate:types --config src/payload.config.ts
|
||||
```
|
||||
|
||||
## Copy-Paste Seed Template
|
||||
|
||||
Use this when a new Payload-backed page should exist automatically in local/dev or fresh
|
||||
environments.
|
||||
|
||||
Seed document example:
|
||||
|
||||
```ts
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const examplePageSeed: Record<string, any> = {
|
||||
title: 'Example Page',
|
||||
slug: 'example',
|
||||
pageType: 'standard',
|
||||
layout: [
|
||||
{
|
||||
blockType: 'exampleBlock',
|
||||
title: 'Hello from Payload',
|
||||
},
|
||||
],
|
||||
seo: {
|
||||
metaTitle: 'Example Page',
|
||||
metaDescription: 'Example description.',
|
||||
noIndex: false,
|
||||
},
|
||||
_status: 'published',
|
||||
};
|
||||
```
|
||||
|
||||
Seed upsert example:
|
||||
|
||||
```ts
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'example' } },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (existing.docs.length > 0) {
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: existing.docs[0]!.id,
|
||||
draft: false,
|
||||
data: examplePageSeed as never,
|
||||
});
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
draft: false,
|
||||
data: examplePageSeed as never,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The important part is not the exact code shape. The important part is that a new
|
||||
Payload-backed route should not depend on manually created admin content if the page is
|
||||
expected to exist in every environment.
|
||||
After adding it:
|
||||
|
||||
## How To Add A New Block
|
||||
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
|
||||
|
||||
If editors need a new section type, add a new block.
|
||||
## Copy-Paste Global-Backed Route Template
|
||||
|
||||
### 1. Create the Payload schema block
|
||||
Use this when you want another singular Payload-managed page in this template and you do
|
||||
not need a full `pages` collection yet.
|
||||
|
||||
Add a file under:
|
||||
This is the best next step for one-off pages like `about`, `contact`, or `pricing`.
|
||||
|
||||
- `apps/next/src/payload/blocks/<block-name>.ts`
|
||||
```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';
|
||||
|
||||
This file defines the editor-facing fields.
|
||||
const AboutPage = async () => {
|
||||
const { isEnabled: isPreview } = await draftMode();
|
||||
const payload = await getPayloadClient();
|
||||
|
||||
### 2. Export it from the block barrel
|
||||
const aboutPage = await payload.findGlobal({
|
||||
slug: 'about-page',
|
||||
draft: isPreview,
|
||||
});
|
||||
|
||||
Update:
|
||||
if (!aboutPage) return notFound();
|
||||
|
||||
- `apps/next/src/payload/blocks/index.ts`
|
||||
return (
|
||||
<main>
|
||||
{isPreview ? <RefreshRouteOnSave /> : null}
|
||||
{/* render the normalized Payload data here */}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
### 3. Register it in the `pages` collection
|
||||
export default AboutPage;
|
||||
```
|
||||
|
||||
Update:
|
||||
For this pattern you would also:
|
||||
|
||||
- `apps/next/src/payload/collections/pages.ts`
|
||||
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
|
||||
|
||||
Add the block to the `layout.blocks` array.
|
||||
## When To Add Another Global vs. A `pages` Collection
|
||||
|
||||
### 4. Build the frontend renderer
|
||||
### Add another global when:
|
||||
|
||||
Add:
|
||||
|
||||
- `apps/next/src/components/payload/blocks/<block-name>.tsx`
|
||||
|
||||
### 5. Register the renderer
|
||||
|
||||
Update:
|
||||
|
||||
- `apps/next/src/components/payload/blocks/render-blocks.tsx`
|
||||
|
||||
### 6. Seed it if needed
|
||||
|
||||
If the new block is used in default pages, update the seed files too.
|
||||
|
||||
## How To Migrate An Existing Hardcoded Page To Payload CMS
|
||||
|
||||
This is the full workflow for converting an old React page into the current CMS model.
|
||||
|
||||
### Step 1: Audit the existing page
|
||||
|
||||
Identify all of these before touching code:
|
||||
|
||||
- route path
|
||||
- metadata and title
|
||||
- structured data / JSON-LD
|
||||
- hero section
|
||||
- repeated content sections
|
||||
- CTA areas
|
||||
- any shared content that should become global settings
|
||||
|
||||
Do not start by blindly copying JSX into one giant rich text field. The current system is
|
||||
block-based for a reason.
|
||||
|
||||
### Step 2: Break the page into reusable sections
|
||||
|
||||
Ask which parts are:
|
||||
|
||||
- already represented by existing blocks
|
||||
- better suited as a new reusable block
|
||||
- global content instead of page-local content
|
||||
- the page is singular
|
||||
- the route is fixed
|
||||
- the content model is custom
|
||||
- you only need a few editable marketing pages
|
||||
|
||||
Examples:
|
||||
|
||||
- hero section -> `landingHero` or `serviceHero`
|
||||
- card rows -> `cardGrid`
|
||||
- comparisons -> `tabularComparison` or `twoColumnComparison`
|
||||
- FAQ -> `faq`
|
||||
- pricing -> `pricingCards`
|
||||
- homepage
|
||||
- contact page
|
||||
- about page
|
||||
- pricing page
|
||||
|
||||
### Step 3: Create new blocks if the page needs them
|
||||
### Add a `pages` collection when:
|
||||
|
||||
If the existing block library is not enough:
|
||||
- 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
|
||||
|
||||
1. add a schema block in `apps/next/src/payload/blocks/`
|
||||
2. add a renderer in `apps/next/src/components/payload/blocks/`
|
||||
3. register both sides
|
||||
Examples:
|
||||
|
||||
Keep blocks composable and editor-friendly. Prefer a few clearly named fields over one big
|
||||
opaque content blob.
|
||||
- service pages
|
||||
- case studies
|
||||
- marketing subpages
|
||||
- partner pages
|
||||
|
||||
### Step 4: Move SEO into document fields
|
||||
## How To Migrate Another Hardcoded Page To Payload In This Repo
|
||||
|
||||
Map metadata into the Payload document:
|
||||
Because this template currently uses a global-backed landing page, the most natural next
|
||||
migration path is usually another Payload global.
|
||||
|
||||
- title -> `seo.metaTitle` or route fallback
|
||||
- meta description -> `seo.metaDescription`
|
||||
- keywords -> `seo.keywords`
|
||||
- noindex -> `seo.noIndex`
|
||||
- service-specific schema -> `structuredData`
|
||||
### Step 1: Audit the current page
|
||||
|
||||
If the page uses JSON-LD, keep the generation logic in the route and read values from the
|
||||
Payload document.
|
||||
Identify:
|
||||
|
||||
### Step 5: Replace the hardcoded route with a Payload-backed route
|
||||
- route path
|
||||
- metadata
|
||||
- JSON-LD or structured data
|
||||
- reusable sections
|
||||
- what should stay hardcoded vs. what should become editable
|
||||
|
||||
The route should:
|
||||
### Step 2: Decide the storage model
|
||||
|
||||
1. fetch the page with `getPageBySlug`
|
||||
2. return `notFound()` if it does not exist
|
||||
3. generate metadata from the doc
|
||||
4. render `RefreshRouteOnSave`
|
||||
5. render JSON-LD with `next/script` if needed
|
||||
6. render `LivePreviewPage`
|
||||
Ask:
|
||||
|
||||
For service pages, prefer reusing the dynamic service route instead of creating many
|
||||
one-off route files.
|
||||
- is this a one-off page? use another global
|
||||
- is this the beginning of many similar pages? consider creating a `pages` collection
|
||||
|
||||
### Step 6: Seed the migrated page
|
||||
### Step 3: Model the editable fields
|
||||
|
||||
If the page should exist locally or in fresh environments, add it to the seed system.
|
||||
Prefer structured fields and blocks over dumping everything into one rich text area.
|
||||
|
||||
This prevents the common failure mode where the route now expects Payload content but the
|
||||
database has no corresponding document yet.
|
||||
### Step 4: Add fallback content
|
||||
|
||||
### Step 7: Verify the full editor flow
|
||||
Keep the same resilience pattern used by the current landing page:
|
||||
|
||||
After migration, verify all of these:
|
||||
- define defaults in component/content files
|
||||
- merge Payload content into those defaults
|
||||
|
||||
- the route loads without 404
|
||||
- the document appears in `/admin`
|
||||
- draft save works
|
||||
- publish works
|
||||
- live preview updates while editing
|
||||
- route refreshes after save/publish
|
||||
- dark/light preview styling still looks correct
|
||||
- seeded content loads on a fresh database
|
||||
This avoids blank pages when content is incomplete.
|
||||
|
||||
### Step 8: Delete the old hardcoded page only after verification
|
||||
### Step 5: Wire the route
|
||||
|
||||
Do not remove the old page implementation until the Payload-backed route is proven working.
|
||||
Create a server route in `apps/next/src/app/(frontend)/...` that:
|
||||
|
||||
For the service-page migration in this repo, the safe order was:
|
||||
1. reads preview state
|
||||
2. fetches Payload data
|
||||
3. normalizes or merges it
|
||||
4. renders the page
|
||||
5. includes `RefreshRouteOnSave` in preview mode
|
||||
|
||||
1. create Payload collection and blocks
|
||||
2. add frontend readers and renderers
|
||||
3. seed the docs
|
||||
4. verify save/publish/live preview
|
||||
5. remove the old hardcoded service page files
|
||||
### Step 6: Verify preview and admin flow
|
||||
|
||||
That order avoids breaking the site in the middle of the migration.
|
||||
After migration, verify:
|
||||
|
||||
## Recommended Checklist For Future Migrations
|
||||
|
||||
Use this exact order:
|
||||
|
||||
1. model the content shape
|
||||
2. add or reuse blocks
|
||||
3. add renderers
|
||||
4. register blocks in the collection
|
||||
5. wire the route to Payload
|
||||
6. move metadata and structured data
|
||||
7. seed the content
|
||||
8. verify preview and publish
|
||||
9. remove old hardcoded components/routes
|
||||
- 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. Page 404s after migration
|
||||
|
||||
Usually means the Payload document does not exist yet.
|
||||
### 1. Homepage renders old or partial content
|
||||
|
||||
Check:
|
||||
|
||||
- the route slug
|
||||
- the document slug
|
||||
- whether the seed ran
|
||||
- whether the landing-page global actually saved
|
||||
- whether preview mode is enabled
|
||||
- whether your fallback merge logic is masking a missing field
|
||||
|
||||
### 2. Publish/save shows `Not Found`
|
||||
|
||||
Usually means middleware or proxy rules are intercepting Payload API writes before they
|
||||
reach Payload.
|
||||
### 2. Live preview does not refresh
|
||||
|
||||
Check:
|
||||
|
||||
- `apps/next/src/proxy.ts`
|
||||
- `apps/next/src/lib/proxy/ban-sus-ips.ts`
|
||||
|
||||
### 3. Live preview frame loads but does not update
|
||||
|
||||
Usually means one of these is wrong:
|
||||
|
||||
- `NEXT_PUBLIC_SITE_URL`
|
||||
- Payload live preview config URL
|
||||
- missing `useLivePreview` on the frontend page
|
||||
- missing `RefreshRouteOnSave`
|
||||
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
||||
- the `admin.livePreview.url` value in the Payload global config
|
||||
|
||||
### 4. Dark mode preview looks wrong
|
||||
### 3. New block fields do not show up in the frontend
|
||||
|
||||
Usually means the theme classes or body-level theme tokens are not being applied inside the
|
||||
preview iframe.
|
||||
Usually means the schema changed but the frontend data contract did not.
|
||||
|
||||
Check the frontend layout and ensure `bg-background` / `text-foreground` are applied at the
|
||||
body level.
|
||||
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`
|
||||
- page collection: `apps/next/src/payload/collections/pages.ts`
|
||||
- media collection: `apps/next/src/payload/collections/media.ts`
|
||||
- global settings: `apps/next/src/payload/globals/site-settings.ts`
|
||||
- block schemas: `apps/next/src/payload/blocks/*`
|
||||
- block renderers: `apps/next/src/components/payload/blocks/*`
|
||||
- route helpers: `apps/next/src/lib/payload-helpers.tsx`
|
||||
- landing route: `apps/next/src/app/(frontend)/page.tsx`
|
||||
- contact route: `apps/next/src/app/(frontend)/contact/page.tsx`
|
||||
- service route: `apps/next/src/app/(frontend)/services/[slug]/page.tsx`
|
||||
- live preview client bridge: `apps/next/src/components/payload/live-preview-page.tsx`
|
||||
- save/publish refresh bridge: `apps/next/src/components/payload/refresh-route-on-save.tsx`
|
||||
- seed entrypoint: `apps/next/src/payload/seed/index.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 change is about editable marketing content, reach for Payload first.
|
||||
If the task is “make marketing content editable,” reach for Payload.
|
||||
|
||||
If the change is about business logic, authenticated workflows, tickets, invoices,
|
||||
appointments, or portal/admin operations, it probably belongs in Convex instead.
|
||||
If the task is “build application logic, auth, data workflows, or realtime product
|
||||
features,” it probably belongs in Convex instead.
|
||||
|
||||
Reference in New Issue
Block a user