docs: extend all 18 AGENTS.md files with comprehensive context, code examples, and recipes
This commit is contained in:
@@ -1,40 +1,323 @@
|
||||
# @repo/cms-core — Payload CMS Definition
|
||||
# @repo/cms-core -- ALL Payload CMS Configuration
|
||||
|
||||
All Payload configuration lives here: payload.config.ts, collections, globals, hooks, access control. The `apps/cms` app is a thin shell that imports this config.
|
||||
## Purpose
|
||||
|
||||
This package contains **all** Payload CMS configuration: `payload.config.ts`, collections, globals, hooks, and access control. The `apps/cms` application is a thin shell that imports from this package -- it contains no CMS logic of its own.
|
||||
|
||||
## Hard Rules
|
||||
|
||||
- **ALL** collections, globals, hooks, and access patterns live here, **never** in `apps/cms`
|
||||
- Can import from `@repo/core/application` (for use case delegation in hooks)
|
||||
- **NEVER** import from `@repo/core/infrastructure`
|
||||
- **NEVER** import from any `apps/*` package
|
||||
- Keep hooks thin (5-10 lines max) -- delegate to use cases for business logic
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
packages/cms-core/
|
||||
src/
|
||||
payload.config.ts # Main Payload config (db, editor, collections, globals)
|
||||
collections/
|
||||
users/
|
||||
index.ts # Users CollectionConfig (auth: true)
|
||||
articles/
|
||||
index.ts # Articles CollectionConfig (versions, hooks)
|
||||
fields.ts # Article field definitions
|
||||
hooks/
|
||||
before-change.ts # Auto-generate slug from title
|
||||
media/
|
||||
index.ts # Media CollectionConfig (upload)
|
||||
globals/
|
||||
site-settings.ts # SiteSettings GlobalConfig
|
||||
index.ts # Package entry: exports all configs
|
||||
package.json
|
||||
AGENTS.md
|
||||
```
|
||||
|
||||
## Existing Collections
|
||||
|
||||
### Users (auth collection)
|
||||
|
||||
- **Slug:** `users`
|
||||
- **Auth:** `true` (provides login, password hashing, session management)
|
||||
- **Fields:** `displayName` (text), `role` (select: admin/editor/author, default: author)
|
||||
- **Admin title:** email
|
||||
|
||||
### Articles (content with versioning)
|
||||
|
||||
- **Slug:** `articles`
|
||||
- **Versions:** `{ drafts: true }` -- enables draft/published workflow
|
||||
- **Hooks:** `beforeChange: [autoGenerateSlug]` -- generates slug from title if slug is empty
|
||||
- **Fields:** title (text, required), slug (text, unique, sidebar), content (richText), status (select: draft/published, default: draft), author (relationship to users, required), featuredImage (upload to media), publishedAt (date)
|
||||
- **Admin title:** title, default columns: title, status, author, updatedAt
|
||||
|
||||
### Media (file uploads)
|
||||
|
||||
- **Slug:** `media`
|
||||
- **Upload:** Accepts `image/*` and `application/pdf`
|
||||
- **Fields:** `alt` (text, required)
|
||||
- **Admin title:** filename
|
||||
|
||||
## Existing Globals
|
||||
|
||||
### SiteSettings
|
||||
|
||||
- **Slug:** `site-settings`
|
||||
- **Admin group:** Settings
|
||||
- **Fields:** `siteName` (text, required, default: "My App"), `siteDescription` (textarea)
|
||||
|
||||
## Hook Rules
|
||||
|
||||
| Category | Location | Examples |
|
||||
|---|---|---|
|
||||
| CMS-operational | Stay in hook | Slugify, image resize, default values |
|
||||
| Business logic | Delegate to use case | Notifications, validation, cross-domain updates |
|
||||
| Category | Where it lives | Description | Examples |
|
||||
|---|---|---|---|
|
||||
| CMS-operational | Stays in the hook file | Logic tied to CMS data shaping, not business rules | Slug generation, image resizing, setting default values, formatting fields |
|
||||
| Business logic | Delegated to `@repo/core` use case | Logic that enforces a business rule or triggers side effects | Sending notifications, validating against external data, cross-domain updates |
|
||||
|
||||
### DO
|
||||
|
||||
- Keep hooks thin (max 5-10 lines)
|
||||
- Import use cases from `@repo/core/application`
|
||||
- Map Payload hook args to use case input types
|
||||
- Keep hooks to 5-10 lines max
|
||||
- Import use cases from `@repo/core/application` (application layer only)
|
||||
- Map Payload hook arguments (`data`, `operation`, `req`) to use case input types
|
||||
- Return `data` from `beforeChange` / `beforeValidate` hooks
|
||||
|
||||
### DON'T
|
||||
|
||||
- Import from `@repo/core/infrastructure`
|
||||
- Put business validation in hooks
|
||||
- Call external services directly from hooks
|
||||
- Duplicate logic that exists in a use case
|
||||
- Import from `@repo/core/infrastructure` -- violates Clean Architecture
|
||||
- Put business validation logic directly in hooks
|
||||
- Call external services (email, analytics, APIs) directly from hooks
|
||||
- Duplicate logic that already exists in a use case
|
||||
- Access `req.payload` for cross-collection operations (delegate to use case instead)
|
||||
|
||||
## Adding a New Collection
|
||||
**Rule of thumb:** If deleting the hook would break a business requirement, the logic must live in a use case in `@repo/core`.
|
||||
|
||||
1. Create folder: `src/collections/{name}/`
|
||||
2. Create: `index.ts` (CollectionConfig), `fields.ts`
|
||||
3. Optionally: `hooks/`, `access/`
|
||||
4. Import in `src/payload.config.ts` collections array
|
||||
5. Export from `src/index.ts`
|
||||
## Recipe: Adding a New Collection
|
||||
|
||||
## Adding a Hook That Calls a Use Case
|
||||
This example adds a `Tags` collection.
|
||||
|
||||
1. Create `src/collections/{name}/hooks/{hook-name}.ts`
|
||||
2. Import use case from `@repo/core` (application layer only)
|
||||
3. Map Payload's hook args to use case input
|
||||
4. Call use case, return data
|
||||
### Step 1: Create the collection folder and fields
|
||||
|
||||
**Rule of thumb:** If deleting the hook would break a business requirement, the logic must be in a use case.
|
||||
Create `src/collections/tags/fields.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Field } from "payload";
|
||||
|
||||
export const tagFields: Field[] = [
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
required: true,
|
||||
unique: true,
|
||||
maxLength: 100,
|
||||
},
|
||||
{
|
||||
name: "slug",
|
||||
type: "text",
|
||||
unique: true,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
description: "Auto-generated from name if left empty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "textarea",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Step 2: Create the CollectionConfig
|
||||
|
||||
Create `src/collections/tags/index.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CollectionConfig } from "payload";
|
||||
import { tagFields } from "./fields";
|
||||
import { autoGenerateSlug } from "./hooks/before-change";
|
||||
|
||||
export const Tags: CollectionConfig = {
|
||||
slug: "tags",
|
||||
admin: {
|
||||
useAsTitle: "name",
|
||||
defaultColumns: ["name", "slug", "updatedAt"],
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [autoGenerateSlug],
|
||||
},
|
||||
fields: tagFields,
|
||||
};
|
||||
```
|
||||
|
||||
### Step 3: Add a CMS-operational hook (slug generation)
|
||||
|
||||
Create `src/collections/tags/hooks/before-change.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CollectionBeforeChangeHook } from "payload";
|
||||
|
||||
function generateSlug(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
export const autoGenerateSlug: CollectionBeforeChangeHook = ({
|
||||
data,
|
||||
operation,
|
||||
}) => {
|
||||
if (operation === "create" || operation === "update") {
|
||||
if (data && data.name && !data.slug) {
|
||||
data.slug = generateSlug(data.name);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
### Step 4: (Optional) Add a business-logic hook delegating to a use case
|
||||
|
||||
Create `src/collections/tags/hooks/after-change.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CollectionAfterChangeHook } from "payload";
|
||||
import { syncTagToSearchIndex } from "@repo/core/application";
|
||||
|
||||
export const syncTagAfterChange: CollectionAfterChangeHook = async ({
|
||||
doc,
|
||||
operation,
|
||||
}) => {
|
||||
// Delegate to use case -- this hook is just a thin bridge
|
||||
await syncTagToSearchIndex({
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
slug: doc.slug,
|
||||
operation,
|
||||
});
|
||||
return doc;
|
||||
};
|
||||
```
|
||||
|
||||
### Step 5: (Optional) Add access control
|
||||
|
||||
Create `src/collections/tags/access/is-admin.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Access } from "payload";
|
||||
|
||||
export const isAdmin: Access = ({ req: { user } }) => {
|
||||
return user?.role === "admin";
|
||||
};
|
||||
```
|
||||
|
||||
Then reference it in the CollectionConfig:
|
||||
|
||||
```typescript
|
||||
export const Tags: CollectionConfig = {
|
||||
slug: "tags",
|
||||
access: {
|
||||
create: isAdmin,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
// read is open by default
|
||||
},
|
||||
// ...rest
|
||||
};
|
||||
```
|
||||
|
||||
### Step 6: Register in payload.config.ts
|
||||
|
||||
Edit `src/payload.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { Tags } from "./collections/tags";
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Users, Articles, Media, Tags], // <-- add Tags
|
||||
// ...rest
|
||||
});
|
||||
```
|
||||
|
||||
### Step 7: Export from package entry
|
||||
|
||||
Edit `src/index.ts`:
|
||||
|
||||
```typescript
|
||||
export { Tags } from "./collections/tags"; // <-- add export
|
||||
```
|
||||
|
||||
## Recipe: Adding a New Global
|
||||
|
||||
This example adds a `Navigation` global.
|
||||
|
||||
### Step 1: Create the GlobalConfig
|
||||
|
||||
Create `src/globals/navigation.ts`:
|
||||
|
||||
```typescript
|
||||
import type { GlobalConfig } from "payload";
|
||||
|
||||
export const Navigation: GlobalConfig = {
|
||||
slug: "navigation",
|
||||
admin: {
|
||||
group: "Settings",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "mainMenu",
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
name: "label",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Register in payload.config.ts
|
||||
|
||||
```typescript
|
||||
import { Navigation } from "./globals/navigation";
|
||||
|
||||
export default buildConfig({
|
||||
globals: [SiteSettings, Navigation], // <-- add Navigation
|
||||
// ...rest
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Export from package entry
|
||||
|
||||
```typescript
|
||||
export { Navigation } from "./globals/navigation"; // <-- add export
|
||||
```
|
||||
|
||||
## Payload Config Overview
|
||||
|
||||
The `payload.config.ts` uses:
|
||||
- **Database:** `@payloadcms/db-postgres` (PostgreSQL via `DATABASE_URL` env var)
|
||||
- **Editor:** `@payloadcms/richtext-lexical` (Lexical rich text editor)
|
||||
- **Secret:** `PAYLOAD_SECRET` env var (required for production)
|
||||
- **TypeScript output:** Generates `payload-types.ts` in this package's `src/` directory
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Purpose |
|
||||
|---|---|
|
||||
| `payload` | Payload CMS core |
|
||||
| `@payloadcms/db-postgres` | PostgreSQL database adapter |
|
||||
| `@payloadcms/richtext-lexical` | Lexical rich text editor |
|
||||
|
||||
## Cross-References
|
||||
|
||||
- **CMS app shell:** `apps/cms/` -- see `apps/cms/AGENTS.md`
|
||||
- **Use cases for hooks:** `packages/core/src/application/use-cases/` -- see `packages/core/AGENTS.md`
|
||||
- **CMS client for querying:** `packages/cms-client/` -- see `packages/cms-client/AGENTS.md`
|
||||
|
||||
Reference in New Issue
Block a user