# Payload CMS In St Pete IT 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. ## What Payload Is Responsible For Payload currently manages the editable marketing content layer inside the Next.js app. 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 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. ## High-Level Architecture ### Core config 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` ### Collections and globals Current CMS entities: - `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 ### Block system Payload page content is built from reusable blocks. Schema side: - `apps/next/src/payload/blocks/*.ts` - exported via `apps/next/src/payload/blocks/index.ts` Render side: - `apps/next/src/components/payload/blocks/*.tsx` - selected by `apps/next/src/components/payload/blocks/render-blocks.tsx` The rule is simple: every Payload block needs both parts. - schema block: defines the fields editors can fill in - renderer block: turns that block data into frontend UI If one side is missing, the admin or the frontend will be incomplete. ### Frontend route flow Landing page route: - `apps/next/src/app/(frontend)/page.tsx` Contact page route: - `apps/next/src/app/(frontend)/contact/page.tsx` Service page route: - `apps/next/src/app/(frontend)/services/[slug]/page.tsx` Shared server fetch helpers: - `apps/next/src/lib/payload-helpers.tsx` Behavior: 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 ### Live preview and publish behavior There are two cooperating pieces: - `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 Important requirement: - `src/proxy.ts` and `src/lib/proxy/ban-sus-ips.ts` must not block valid Payload REST API requests under `/api` 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. ## Environment Variables Payload depends on these env vars: - `PAYLOAD_SECRET` - `PAYLOAD_DB_URL` - `NEXT_PUBLIC_SITE_URL` Why they matter: - `PAYLOAD_SECRET` secures Payload sessions and server behavior - `PAYLOAD_DB_URL` connects Payload to Postgres - `NEXT_PUBLIC_SITE_URL` is used by live preview to target the frontend correctly 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. ## How To Create A New Page Like The Current Ones This section assumes you want another page managed by the existing `pages` collection. ### Option A: Create a new service page using the existing system This is the simplest case. 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/` Why this works without adding a new route: - 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 If the page should exist by default in new environments, also add it to `apps/next/src/payload/seed/service-pages.ts`. ### Option B: Create another landing-style page with the same block approach If the page is not a service page, decide whether it belongs: - in the existing `pages` collection with a new route, or - in a new Payload collection if the content model is materially different If it fits `pages`: 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` Example pattern: ```tsx const page = await getPageBySlug('some-slug'); if (!page) return notFound(); return (
); ``` ## Copy-Paste Route Template Use this when creating a new non-service Payload-backed page route. 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 (