diff --git a/AGENTS.md b/AGENTS.md index 4e2cf7e..1a98167 100644 --- a/AGENTS.md +++ b/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 ``` diff --git a/README.md b/README.md index b610f62..990461f 100644 --- a/README.md +++ b/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 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 diff --git a/docs/payload-cms.md b/docs/payload-cms.md index 8042955..ff691bf 100644 --- a/docs/payload-cms.md +++ b/docs/payload-cms.md @@ -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/` +- `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 ( -
- - -
-); +```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 => { - 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 ( -
- -