324 lines
8.6 KiB
Markdown
324 lines
8.6 KiB
Markdown
# @repo/cms-core -- ALL Payload CMS Configuration
|
|
|
|
## 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 | 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 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` -- 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)
|
|
|
|
**Rule of thumb:** If deleting the hook would break a business requirement, the logic must live in a use case in `@repo/core`.
|
|
|
|
## Recipe: Adding a New Collection
|
|
|
|
This example adds a `Tags` collection.
|
|
|
|
### Step 1: Create the collection folder and fields
|
|
|
|
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`
|