Add Plan 3: Payload CMS Integration implementation plan

10 tasks covering: cms-core dependencies, Users/Articles/Media collections,
slug auto-generation hook, SiteSettings global, Payload config, dual-mode
cms-client (local + HTTP), thin Next.js admin shell, build verification.
This commit is contained in:
2026-04-06 14:38:33 +02:00
parent 46c2dd18d8
commit 0196de59a4

View File

@@ -0,0 +1,977 @@
# Plan 3: Payload CMS Integration — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement Payload CMS integration: `@repo/cms-core` (config, collections, hooks), `@repo/cms-client` (dual-mode local+HTTP client with generated types), and `apps/cms` (thin Next.js shell serving the Payload admin panel).
**Architecture:** Payload config and collections live in `@repo/cms-core` — standalone, testable, framework-agnostic. `@repo/cms-client` provides a dual-mode client (Local API primary, HTTP fallback) that receives a Payload instance via injection. `apps/cms` is a thin Next.js 15 shell that imports config from cms-core and serves the admin panel. Type generation runs `payload generate:types` against cms-core's config.
**Tech Stack:** Payload CMS 3.x, @payloadcms/db-postgres, @payloadcms/next, @payloadcms/richtext-lexical, Next.js 15, PostgreSQL 16, sharp
---
## File Map
### packages/cms-core
| File | Responsibility |
|---|---|
| `packages/cms-core/package.json` | Dependencies: payload, @payloadcms/db-postgres, @payloadcms/richtext-lexical |
| `packages/cms-core/src/payload.config.ts` | Root Payload config (db, editor, collections, globals) |
| `packages/cms-core/src/collections/users/index.ts` | Users collection with auth enabled |
| `packages/cms-core/src/collections/articles/index.ts` | Articles collection config |
| `packages/cms-core/src/collections/articles/fields.ts` | Article field definitions |
| `packages/cms-core/src/collections/articles/hooks/before-change.ts` | Slug auto-generation hook |
| `packages/cms-core/src/collections/media/index.ts` | Media collection with uploads |
| `packages/cms-core/src/globals/site-settings.ts` | Site settings global |
| `packages/cms-core/src/index.ts` | Exports config + all collections |
### packages/cms-client
| File | Responsibility |
|---|---|
| `packages/cms-client/package.json` | Dependencies: payload (types only) |
| `packages/cms-client/src/client.ts` | createPayloadClient() factory |
| `packages/cms-client/src/local-client.ts` | LocalPayloadClient — wraps Payload instance |
| `packages/cms-client/src/http-client.ts` | HTTPPayloadClient — REST API fallback |
| `packages/cms-client/src/types.ts` | Shared client types (PayloadClient interface) |
| `packages/cms-client/src/index.ts` | Exports |
### apps/cms
| File | Responsibility |
|---|---|
| `apps/cms/package.json` | Dependencies: next, payload, @payloadcms/next, @repo/cms-core |
| `apps/cms/next.config.mjs` | Next.js config wrapped with withPayload |
| `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` | Admin panel catch-all route |
| `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Admin 404 page |
| `apps/cms/src/app/(payload)/layout.tsx` | Payload layout with RootLayout |
| `apps/cms/src/app/(payload)/custom.scss` | Empty custom styles |
| `apps/cms/src/payload-types.ts` | Generated types (via payload generate:types) |
---
### Task 1: Install cms-core dependencies
**Files:**
- Modify: `packages/cms-core/package.json`
- Modify: `packages/cms-core/tsconfig.json`
- [ ] **Step 1: Update packages/cms-core/package.json**
```json
{
"name": "@repo/cms-core",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"payload": "^3.14.0",
"@payloadcms/db-postgres": "^3.14.0",
"@payloadcms/richtext-lexical": "^3.14.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0"
}
}
```
- [ ] **Step 2: Update tsconfig.json**
```json
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
```
- [ ] **Step 3: Run pnpm install**
Run: `pnpm install`
Expected: Payload and its dependencies install successfully.
- [ ] **Step 4: Commit**
```bash
git add packages/cms-core/package.json packages/cms-core/tsconfig.json pnpm-lock.yaml
git commit -m "feat(cms-core): add Payload CMS dependencies"
```
---
### Task 2: Users collection
**Files:**
- Create: `packages/cms-core/src/collections/users/index.ts`
- [ ] **Step 1: Create Users collection**
```typescript
import type { CollectionConfig } from "payload";
export const Users: CollectionConfig = {
slug: "users",
auth: true,
admin: {
useAsTitle: "email",
},
fields: [
{
name: "displayName",
type: "text",
},
{
name: "role",
type: "select",
options: [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "Author", value: "author" },
],
defaultValue: "author",
required: true,
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/collections/users/
git commit -m "feat(cms-core): add Users collection with auth"
```
---
### Task 3: Articles collection with fields and hooks
**Files:**
- Create: `packages/cms-core/src/collections/articles/fields.ts`
- Create: `packages/cms-core/src/collections/articles/hooks/before-change.ts`
- Create: `packages/cms-core/src/collections/articles/index.ts`
- [ ] **Step 1: Create fields.ts**
```typescript
import type { Field } from "payload";
export const articleFields: Field[] = [
{
name: "title",
type: "text",
required: true,
maxLength: 255,
},
{
name: "slug",
type: "text",
unique: true,
admin: {
position: "sidebar",
description: "Auto-generated from title if left empty",
},
},
{
name: "content",
type: "richText",
},
{
name: "status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
defaultValue: "draft",
required: true,
admin: {
position: "sidebar",
},
},
{
name: "author",
type: "relationship",
relationTo: "users",
required: true,
admin: {
position: "sidebar",
},
},
{
name: "featuredImage",
type: "upload",
relationTo: "media",
},
{
name: "publishedAt",
type: "date",
admin: {
position: "sidebar",
date: {
pickerAppearance: "dayAndTime",
},
},
},
];
```
- [ ] **Step 2: Create hooks/before-change.ts**
This is a CMS-operational hook (slug auto-generation) — stays in cms-core per the design spec.
```typescript
import type { CollectionBeforeChangeHook } from "payload";
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
export const autoGenerateSlug: CollectionBeforeChangeHook = ({
data,
operation,
}) => {
if (operation === "create" || operation === "update") {
if (data && data.title && !data.slug) {
data.slug = generateSlug(data.title);
}
}
return data;
};
```
- [ ] **Step 3: Create articles/index.ts**
```typescript
import type { CollectionConfig } from "payload";
import { articleFields } from "./fields.js";
import { autoGenerateSlug } from "./hooks/before-change.js";
export const Articles: CollectionConfig = {
slug: "articles",
admin: {
useAsTitle: "title",
defaultColumns: ["title", "status", "author", "updatedAt"],
},
hooks: {
beforeChange: [autoGenerateSlug],
},
versions: {
drafts: true,
},
fields: articleFields,
};
```
- [ ] **Step 4: Commit**
```bash
git add packages/cms-core/src/collections/articles/
git commit -m "feat(cms-core): add Articles collection with slug auto-generation hook"
```
---
### Task 4: Media collection
**Files:**
- Create: `packages/cms-core/src/collections/media/index.ts`
- [ ] **Step 1: Create Media collection**
```typescript
import type { CollectionConfig } from "payload";
export const Media: CollectionConfig = {
slug: "media",
upload: {
mimeTypes: ["image/*", "application/pdf"],
},
admin: {
useAsTitle: "filename",
},
fields: [
{
name: "alt",
type: "text",
required: true,
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/collections/media/
git commit -m "feat(cms-core): add Media collection with uploads"
```
---
### Task 5: Site settings global
**Files:**
- Create: `packages/cms-core/src/globals/site-settings.ts`
- [ ] **Step 1: Create site-settings.ts**
```typescript
import type { GlobalConfig } from "payload";
export const SiteSettings: GlobalConfig = {
slug: "site-settings",
admin: {
group: "Settings",
},
fields: [
{
name: "siteName",
type: "text",
required: true,
defaultValue: "My App",
},
{
name: "siteDescription",
type: "textarea",
},
],
};
```
- [ ] **Step 2: Commit**
```bash
git add packages/cms-core/src/globals/
git commit -m "feat(cms-core): add SiteSettings global"
```
---
### Task 6: Payload config + exports
**Files:**
- Create: `packages/cms-core/src/payload.config.ts`
- Modify: `packages/cms-core/src/index.ts`
- [ ] **Step 1: Create payload.config.ts**
```typescript
import { buildConfig } from "payload";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Users } from "./collections/users/index.js";
import { Articles } from "./collections/articles/index.js";
import { Media } from "./collections/media/index.js";
import { SiteSettings } from "./globals/site-settings.js";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
editor: lexicalEditor(),
collections: [Users, Articles, Media],
globals: [SiteSettings],
secret: process.env.PAYLOAD_SECRET || "default-secret-change-me",
db: postgresAdapter({
pool: {
connectionString:
process.env.DATABASE_URL ||
"postgresql://postgres:postgres@localhost:5432/template",
},
}),
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
});
```
- [ ] **Step 2: Update src/index.ts**
```typescript
export { Users } from "./collections/users/index.js";
export { Articles } from "./collections/articles/index.js";
export { Media } from "./collections/media/index.js";
export { SiteSettings } from "./globals/site-settings.js";
export { default as config } from "./payload.config.js";
```
- [ ] **Step 3: Commit**
```bash
git add packages/cms-core/src/payload.config.ts packages/cms-core/src/index.ts
git commit -m "feat(cms-core): add Payload config with postgres adapter and lexical editor"
```
---
### Task 7: cms-client — dual-mode Payload client
**Files:**
- Modify: `packages/cms-client/package.json`
- Create: `packages/cms-client/src/types.ts`
- Create: `packages/cms-client/src/local-client.ts`
- Create: `packages/cms-client/src/http-client.ts`
- Create: `packages/cms-client/src/client.ts`
- Modify: `packages/cms-client/src/index.ts`
- [ ] **Step 1: Update packages/cms-client/package.json**
```json
{
"name": "@repo/cms-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc --noEmit",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"payload": "^3.14.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0"
}
}
```
- [ ] **Step 2: Create src/types.ts**
```typescript
export interface FindOptions {
where?: Record<string, unknown>;
sort?: string;
limit?: number;
page?: number;
depth?: number;
locale?: string;
}
export interface PayloadClientResult<T> {
docs: T[];
totalDocs: number;
limit: number;
totalPages: number;
page: number;
pagingCounter: number;
hasPrevPage: boolean;
hasNextPage: boolean;
prevPage: number | null;
nextPage: number | null;
}
export interface PayloadClient {
find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>>;
findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T>;
create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T>;
update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T>;
delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T>;
}
```
- [ ] **Step 3: Create src/local-client.ts**
```typescript
import type { Payload } from "payload";
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
export class LocalPayloadClient implements PayloadClient {
constructor(private payload: Payload) {}
async find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>> {
const result = await this.payload.find({
collection: collection as any,
where: options?.where as any,
sort: options?.sort,
limit: options?.limit,
page: options?.page,
depth: options?.depth,
locale: options?.locale as any,
});
return result as unknown as PayloadClientResult<T>;
}
async findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.findByID({
collection: collection as any,
id,
depth: options?.depth,
});
return result as unknown as T;
}
async create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.create({
collection: collection as any,
data: data as any,
depth: options?.depth,
});
return result as unknown as T;
}
async update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const result = await this.payload.update({
collection: collection as any,
id,
data: data as any,
depth: options?.depth,
});
return result as unknown as T;
}
async delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T> {
const result = await this.payload.delete({
collection: collection as any,
id,
});
return result as unknown as T;
}
}
```
- [ ] **Step 4: Create src/http-client.ts**
```typescript
import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js";
export class HTTPPayloadClient implements PayloadClient {
constructor(private baseURL: string) {}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseURL}${path}`, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!response.ok) {
throw new Error(`Payload API error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async find<T = Record<string, unknown>>(
collection: string,
options?: FindOptions
): Promise<PayloadClientResult<T>> {
const params = new URLSearchParams();
if (options?.limit) params.set("limit", String(options.limit));
if (options?.page) params.set("page", String(options.page));
if (options?.sort) params.set("sort", options.sort);
if (options?.depth) params.set("depth", String(options.depth));
if (options?.where) params.set("where", JSON.stringify(options.where));
const query = params.toString();
return this.request<PayloadClientResult<T>>(
`/api/${collection}${query ? `?${query}` : ""}`
);
}
async findByID<T = Record<string, unknown>>(
collection: string,
id: string,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}/${id}${query ? `?${query}` : ""}`
);
}
async create<T = Record<string, unknown>>(
collection: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}${query ? `?${query}` : ""}`,
{ method: "POST", body: JSON.stringify(data) }
);
}
async update<T = Record<string, unknown>>(
collection: string,
id: string,
data: Record<string, unknown>,
options?: { depth?: number }
): Promise<T> {
const params = new URLSearchParams();
if (options?.depth) params.set("depth", String(options.depth));
const query = params.toString();
return this.request<T>(
`/api/${collection}/${id}${query ? `?${query}` : ""}`,
{ method: "PATCH", body: JSON.stringify(data) }
);
}
async delete<T = Record<string, unknown>>(
collection: string,
id: string
): Promise<T> {
return this.request<T>(`/api/${collection}/${id}`, { method: "DELETE" });
}
}
```
- [ ] **Step 5: Create src/client.ts**
```typescript
import type { Payload } from "payload";
import type { PayloadClient } from "./types.js";
import { LocalPayloadClient } from "./local-client.js";
import { HTTPPayloadClient } from "./http-client.js";
type PayloadClientOptions =
| { mode: "local"; payload: Payload }
| { mode: "http"; baseURL: string };
export function createPayloadClient(options: PayloadClientOptions): PayloadClient {
if (options.mode === "local") {
return new LocalPayloadClient(options.payload);
}
return new HTTPPayloadClient(options.baseURL);
}
```
- [ ] **Step 6: Update src/index.ts**
```typescript
export { createPayloadClient } from "./client.js";
export { LocalPayloadClient } from "./local-client.js";
export { HTTPPayloadClient } from "./http-client.js";
export type {
PayloadClient,
PayloadClientResult,
FindOptions,
} from "./types.js";
```
- [ ] **Step 7: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add packages/cms-client/ pnpm-lock.yaml
git commit -m "feat(cms-client): add dual-mode Payload client (local + HTTP)"
```
---
### Task 8: apps/cms — thin Next.js shell
**Files:**
- Modify: `apps/cms/package.json`
- Create: `apps/cms/next.config.mjs`
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx`
- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx`
- Create: `apps/cms/src/app/(payload)/layout.tsx`
- Create: `apps/cms/src/app/(payload)/custom.scss`
- Modify: `apps/cms/tsconfig.json`
- [ ] **Step 1: Update apps/cms/package.json**
```json
{
"name": "@repo/cms",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev --port 3001",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"generate:types": "payload generate:types"
},
"dependencies": {
"@payloadcms/next": "^3.14.0",
"@payloadcms/ui": "^3.14.0",
"@repo/cms-core": "workspace:*",
"next": "^15.3.0",
"payload": "^3.14.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.33.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}
```
- [ ] **Step 2: Update apps/cms/tsconfig.json**
```json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@payload-config": ["../../packages/cms-core/src/payload.config.ts"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"],
"exclude": ["node_modules"]
}
```
- [ ] **Step 3: Create next.config.mjs**
```javascript
import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withPayload(nextConfig);
```
- [ ] **Step 4: Create admin catch-all page**
```tsx
// apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "@payload-config";
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap.js";
type Args = {
params: Promise<{ segments: string[] }>;
searchParams: Promise<Record<string, string | string[]>>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams });
export default Page;
```
- [ ] **Step 5: Create admin not-found page**
```tsx
// apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from "next";
import config from "@payload-config";
import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views";
import { importMap } from "../importMap.js";
type Args = {
params: Promise<{ segments: string[] }>;
searchParams: Promise<Record<string, string | string[]>>;
};
export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams });
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams });
export default NotFound;
```
- [ ] **Step 6: Create payload layout**
```tsx
// apps/cms/src/app/(payload)/layout.tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { ServerFunctionClient } from "payload";
import config from "@payload-config";
import { RootLayout } from "@payloadcms/next/layouts";
import React from "react";
import { importMap } from "./importMap.js";
import "./custom.scss";
type Args = {
children: React.ReactNode;
};
const serverFunction: ServerFunctionClient = async function (args) {
"use server";
const { default: payloadModule } = await import("payload");
return payloadModule.handleServerFunctions({ ...args, config, importMap });
};
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
);
export default Layout;
```
- [ ] **Step 7: Create empty importMap and custom.scss**
```typescript
// apps/cms/src/app/(payload)/importMap.js
export const importMap = {};
```
```scss
// apps/cms/src/app/(payload)/custom.scss
// Custom admin panel styles
```
- [ ] **Step 8: Run pnpm install and commit**
Run: `pnpm install`
```bash
git add apps/cms/ pnpm-lock.yaml
git commit -m "feat(cms): add thin Next.js shell for Payload admin panel"
```
---
### Task 9: Update docker-compose with CMS service
**Files:**
- Modify: `docker-compose.yml`
- [ ] **Step 1: Update docker-compose.yml**
```yaml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: template
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
```
Note: CMS app service will be added when Dockerfiles are created in a later plan. For now, local dev uses `pnpm dev --filter @repo/cms`.
- [ ] **Step 2: Commit (no change needed if docker-compose is already correct)**
Only commit if docker-compose was modified.
---
### Task 10: Verify build
- [ ] **Step 1: Run pnpm install from root**
Run: `pnpm install`
Expected: All dependencies resolve.
- [ ] **Step 2: Run turbo build**
Run: `pnpm build`
Expected: All workspaces build. The cms-core and cms-client packages should pass `tsc --noEmit`. The cms app should run `next build` (may need database for full build — if it fails due to no DB, that's expected and acceptable at this stage).
- [ ] **Step 3: Commit any remaining changes**
```bash
git add -A && git status
# Only commit if there are changes
git commit -m "chore: update lockfile after CMS integration"
```