vault backup: 2026-05-15 15:22:27
This commit is contained in:
parent
0e6a12242c
commit
efc8facf3c
18 changed files with 1049 additions and 1 deletions
|
|
@ -35,7 +35,7 @@ This 3-hop pattern works for hundreds of articles without vector search.
|
|||
| [[wiki/reports/_index\|reports/]] | Weekly and monthly summaries — generate: `uv run python scripts/report-generator.py --weekly` | 1 |
|
||||
| [[wiki/infrastructure/_index\|infrastructure/]] | Server inventory: all 10 SSH hosts — optical, optical-dev, optical-prod, baic, librechat, modocmms, box-cli, aimpress, pve | 12 |
|
||||
| [[wiki/testing/_index\|testing/]] | Web app testing: functional, performance, security, UI types; TDD/BDD/Agile methodologies; Selenium/Cypress/Playwright/JMeter/OWASP ZAP tools | 1 |
|
||||
| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization | 56 |
|
||||
| [[wiki/payloadcms/_index\|payloadcms/]] | Full Payload CMS reference — getting started, config, database (Postgres/MongoDB/SQLite), all 22 field types, access control, hooks, authentication (cookies, JWT, API keys, custom strategies, token data), admin UI, custom components, Lexical rich text, live preview, versions/drafts, Local/REST/GraphQL APIs, queries, plugins, jobs queue, upload, ecommerce, production deploy, TypeScript, migration guides, i18n, localization | 63 |
|
||||
|
||||
| [[wiki/shared-patterns/_index\|shared-patterns/]] | Oliver Agency standard library patterns: httpx, structlog, pydantic-settings, alembic — reuse before writing from scratch | 4 |
|
||||
| [[wiki/mistakes/_index\|mistakes/]] | Anti-patterns extracted from sessions — per-stack running lists (fastapi, react, docker, postgres, general) — injected at session start | 5 |
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@
|
|||
|
||||
| [[wiki/payloadcms/upload\|Upload & Media]] | Upload config, imageSizes, focal point, storage adapters (S3/R2/GCS/Azure/Vercel Blob), access control, gotchas | raw/upload__*.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/features\|Features — Email, Folders, Trash, Query Presets]] | Email adapters (Nodemailer/Resend), folder grouping (beta), soft-delete trash workflow, saved query presets | raw/email/folders/trash/query-presets | 2026-05-15 |
|
||||
| [[wiki/payloadcms/email\|Email — Adapters, SMTP, Resend, Attachments]] | Full email reference: Nodemailer (SMTP/SendGrid/dev), Resend (serverless), sendEmail API, attachments (Buffer/path/Base64/URL), media collection attachments | raw/email__overview.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/local-api\|Local API]] | Direct DB access via `payload.*` — CRUD, auth ops, globals, server functions, access control, outside Next.js | raw/local-api__*.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/rest-api\|REST API]] | Auto-generated HTTP endpoints, all query params, auth routes, SDK, custom endpoints, method override | raw/rest-api__overview.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/queries\|Queries]] | Where operators, and/or logic, depth, pagination, sort, select/populate — full reference with examples | raw/queries__*.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/ecommerce\|Ecommerce]] | Plugin setup, Stripe payments, cart API, frontend hooks, multi-currency, piecemeal collections | raw/ecommerce__*.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/ecommerce-advanced\|Ecommerce — Advanced / Piecemeal Collections API]] | All 8 `create*Collection` factory functions with full prop/access tables + TS types (CurrenciesConfig, Currency, CountryType, InventoryConfig) | raw/ecommerce__advanced.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/ecommerce-frontend\|Ecommerce — Frontend React Utilities]] | EcommerceProvider, useCart, usePayments (2-step checkout), useAddresses, useCurrency, useEcommerceConfig, useEcommerce; session management (onLogin, onLogout, mergeCart) | raw/ecommerce__frontend.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/ecommerce-payment-adapters\|Ecommerce — Payment Adapters]] | Payment adapter pattern, Stripe setup + webhooks (local CLI), custom adapter interface (initiatePayment/confirmOrder/group field), client-side adapter, security gotchas | raw/ecommerce__payments.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/production\|Production & Performance]] | Deployment, Docker, building without DB, abuse prevention, performance optimization patterns | raw/production__*.md, raw/performance__overview.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/typescript\|TypeScript]] | Native TS support, `generate:types` CLI, `interfaceName` for reusable interfaces, TS language service plugin | raw/typescript__*.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/migration-troubleshooting\|Migration & Troubleshooting]] | v2→v3 and v3→v4 migration guides, codemod, dependency dedup fix, hierarchy feature, Vercel Content Link, examples | raw/migration-guide__*.md, troubleshooting, hierarchy, integrations | 2026-05-15 |
|
||||
|
|
@ -58,3 +62,5 @@
|
|||
| [[wiki/payloadcms/hooks\|PayloadCMS — Hooks]] | - Hooks execute side effects at precise moments in the Document lifecycle | https://payloadcms.com/docs/hooks/overview | 2026-05-15 |
|
||||
| [[wiki/payloadcms/live-preview\|PayloadCMS — Live Preview]] | - Renders your frontend inside an iframe directly in the Admin Panel | https://payloadcms.com/docs/live-preview/overview | 2026-05-15 |
|
||||
| [[wiki/payloadcms/rich-text\|PayloadCMS — Rich Text (Lexical)]] | - Rich text is powered by **Lexical** (Meta's framework), exposed via `@payloadcms/richtext-lexical` | https://payloadcms.com/docs/rich-text/overview | 2026-05-15 |
|
||||
| [[wiki/payloadcms/examples-overview\|Examples Overview]] | 9 official starter examples (auth, draft-preview, multi-tenant, tailwind-shadcn, whitelabel…) — `npx create-payload-app --example <name>` | raw/examples__overview.md | 2026-05-15 |
|
||||
| [[wiki/payloadcms/fields-array\|Array Field]] | Repeating row field — config options, minRows/maxRows, localized, unique gotcha, custom RowLabel, nested arrays | raw/fields__array.md | 2026-05-15 |
|
||||
|
|
|
|||
212
wiki/payloadcms/ecommerce-advanced.md
Normal file
212
wiki/payloadcms/ecommerce-advanced.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
---
|
||||
title: "Ecommerce — Advanced / Piecemeal Collections API"
|
||||
aliases: [ecommerce-piecemeal, ecommerce-collection-factories]
|
||||
tags: [payloadcms, ecommerce, stripe, typescript]
|
||||
sources: [raw/ecommerce__advanced.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
# Ecommerce — Advanced / Piecemeal Collections API
|
||||
|
||||
The plugin exposes individual collection factory functions so you can adopt only the pieces you need without registering the full plugin. Useful when building a custom ecommerce flow on top of Payload.
|
||||
|
||||
## Collection Factory Functions
|
||||
|
||||
| Factory | Collection | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `createAddressesCollection` | `addresses` | Customer shipping/billing addresses |
|
||||
| `createCartsCollection` | `carts` | Guest + authenticated carts; retained after purchase for analytics |
|
||||
| `createOrdersCollection` | `orders` | Customer-facing order records; linked to ≥1 Transaction |
|
||||
| `createTransactionsCollection` | `transactions` | Payment data — admin-only |
|
||||
| `createProductsCollection` | `products` | Product catalog with prices, variant links |
|
||||
| `createVariantsCollection` | `variants` | Unique purchasable variants linked to a product + options |
|
||||
| `createVariantTypesCollection` | `variantTypes` | Taxonomy grouping (e.g. "size") |
|
||||
| `createVariantOptionsCollection` | `variantOptions` | Individual values of a type (e.g. "small") |
|
||||
|
||||
## createAddressesCollection
|
||||
|
||||
```ts
|
||||
import { createAddressesCollection } from 'payload-plugin-ecommerce'
|
||||
|
||||
const Addresses = createAddressesCollection({
|
||||
access: { isAdmin, isAuthenticated, isCustomer, isDocumentOwner },
|
||||
addressFields: [{ name: 'company', type: 'text', label: 'Company' }],
|
||||
// customersSlug: 'customers' (default)
|
||||
// supportedCountries: CountryType[] (default: all)
|
||||
})
|
||||
```
|
||||
|
||||
**Access object:**
|
||||
|
||||
| Key | Type | Used for |
|
||||
|-----|------|----------|
|
||||
| `isAdmin` | `Access` | Full admin access |
|
||||
| `isAuthenticated` | `Access` | Allow any customer to `create` |
|
||||
| `isCustomer` | `FieldAccess` | Auto-assign `customer` ID on create |
|
||||
| `isDocumentOwner` | `Access` | Limit read/update/delete to owner |
|
||||
|
||||
## createCartsCollection
|
||||
|
||||
```ts
|
||||
const Carts = createCartsCollection({
|
||||
access: { isAdmin, isAuthenticated, isDocumentOwner, publicAccess },
|
||||
enableVariants: true,
|
||||
currenciesConfig: {
|
||||
defaultCurrency: 'usd',
|
||||
currencies: [{ code: 'usd', symbol: '$' }, { code: 'eur', symbol: '€' }],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- `publicAccess` — optional; allows guest (unauthenticated) cart creation
|
||||
- `isDocumentOwner` — limits read/update/delete to cart owner only
|
||||
|
||||
## createOrdersCollection
|
||||
|
||||
```ts
|
||||
const Orders = createOrdersCollection({
|
||||
access: { isAdmin, isDocumentOwner, adminOnlyFieldAccess },
|
||||
enableVariants: true,
|
||||
currenciesConfig: { ... },
|
||||
addressFields: [{ name: 'deliveryInstructions', type: 'text', label: 'Delivery Instructions' }],
|
||||
})
|
||||
```
|
||||
|
||||
- `adminOnlyFieldAccess` — restricts `transactionId` field to admins
|
||||
- `addressFields` — injected as shipping address sub-fields
|
||||
|
||||
## createTransactionsCollection
|
||||
|
||||
```ts
|
||||
const Transactions = createTransactionsCollection({
|
||||
access: { isAdmin }, // admin-only for all operations
|
||||
enableVariants: true,
|
||||
currenciesConfig: { ... },
|
||||
addressFields: [...], // billing address fields
|
||||
paymentMethods: [], // PaymentAdapter[]
|
||||
})
|
||||
```
|
||||
|
||||
Only `isAdmin` is available — transactions are never exposed to customers.
|
||||
|
||||
## createProductsCollection
|
||||
|
||||
```ts
|
||||
const Products = createProductsCollection({
|
||||
access: { isAdmin, adminOrPublishedStatus },
|
||||
enableVariants: true,
|
||||
currenciesConfig: { ... },
|
||||
inventory: {
|
||||
enabled: true,
|
||||
trackByVariant: true,
|
||||
lowStockThreshold: 5,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- `adminOrPublishedStatus` — non-admins can only read published products
|
||||
- `inventory` accepts `boolean` (on/off) or `InventoryConfig`
|
||||
|
||||
## createVariantsCollection
|
||||
|
||||
```ts
|
||||
const Variants = createVariantsCollection({
|
||||
access: { isAdmin, adminOrPublishedStatus },
|
||||
currenciesConfig: { ... },
|
||||
inventory: { enabled: true, lowStockThreshold: 5 },
|
||||
})
|
||||
```
|
||||
|
||||
- `adminOrPublishedStatus` here checks the *related product's* published status
|
||||
|
||||
## createVariantTypesCollection
|
||||
|
||||
```ts
|
||||
const VariantTypes = createVariantTypesCollection({
|
||||
access: { isAdmin, publicAccess },
|
||||
// variantOptionsSlug: 'variantOptions'
|
||||
})
|
||||
```
|
||||
|
||||
- `publicAccess` — allow anyone to read variant types (e.g. "size", "color")
|
||||
|
||||
## createVariantOptionsCollection
|
||||
|
||||
```ts
|
||||
const VariantOptions = createVariantOptionsCollection({
|
||||
access: { isAdmin, publicAccess },
|
||||
// variantTypesSlug: 'variantTypes'
|
||||
})
|
||||
```
|
||||
|
||||
- `publicAccess` — allow anyone to read variant options (e.g. "small", "red")
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
### CurrenciesConfig
|
||||
|
||||
```ts
|
||||
type CurrenciesConfig = {
|
||||
defaultCurrency: string // must match a code in currencies[]
|
||||
currencies: CurrencyType[]
|
||||
}
|
||||
```
|
||||
|
||||
### Currency (CurrencyType)
|
||||
|
||||
```ts
|
||||
type CurrencyType = {
|
||||
code: string // ISO 4217, e.g. 'usd'
|
||||
symbol: string // e.g. '$'
|
||||
label?: string // e.g. 'USD'
|
||||
decimals?: number // e.g. 2 — IMPORTANT: prices stored as integers
|
||||
}
|
||||
```
|
||||
|
||||
> **Critical:** `decimals` controls how integer prices are formatted. USD stores `1000` → display `$10.00`. JPY uses `0` decimals.
|
||||
|
||||
### CountryType
|
||||
|
||||
```ts
|
||||
type CountryType = {
|
||||
value: string // ISO 3166-1 alpha-2, e.g. 'US'
|
||||
label: string // e.g. 'United States'
|
||||
}
|
||||
```
|
||||
|
||||
### InventoryConfig
|
||||
|
||||
```ts
|
||||
type InventoryConfig = {
|
||||
fieldName?: string // default: 'inventory'
|
||||
// other fields depend on plugin version
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- **No plugin needed** — import individual `create*Collection` factories and add them directly to Payload config
|
||||
- **Access objects differ per collection** — check the table for each factory; not all access keys are available everywhere
|
||||
- **Transactions are always admin-only** — `isAdmin` is the only access key available
|
||||
- **`publicAccess` on carts enables guest shopping** — carts without auth; use localStorage to persist cart ID
|
||||
- **Prices are stored as integers** — always set `decimals` in `CurrencyType` to format correctly
|
||||
- **`inventory` accepts bool or config** — `true` enables defaults; pass `InventoryConfig` to customise field name or threshold
|
||||
- **All `*Slug` params are optional** — override only if you renamed the default collection slugs
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/ecommerce|Ecommerce Overview]] — full plugin setup, Stripe flow, cart API, frontend hooks
|
||||
- [[wiki/payloadcms/access-control|Access Control]] — `Access` and `FieldAccess` function signatures
|
||||
- [[wiki/payloadcms/plugins|Plugins]] — how to use and build Payload plugins
|
||||
- [[wiki/payloadcms/collection-config|Collection Config]] — base collection config options
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/ecommerce__advanced.md` — payloadcms.com/docs/ecommerce/advanced
|
||||
202
wiki/payloadcms/ecommerce-frontend.md
Normal file
202
wiki/payloadcms/ecommerce-frontend.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
---
|
||||
title: "Ecommerce — Frontend React Utilities"
|
||||
aliases: [ecommerce-react-hooks, payload-ecommerce-frontend, useCart, usePayments]
|
||||
tags: [payloadcms, ecommerce, react, hooks, stripe, cart]
|
||||
sources: [raw/ecommerce__frontend.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`@payloadcms/plugin-ecommerce/client/react` exports a context provider + 6 focused hooks for managing cart, addresses, payments, and currency on the frontend. All hooks require `EcommerceProvider` to be mounted above them in the component tree.
|
||||
|
||||
## EcommerceProvider
|
||||
|
||||
Wrap your app (or checkout subtree) with this provider:
|
||||
|
||||
```tsx
|
||||
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react'
|
||||
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
|
||||
import { USD, EUR } from '@payloadcms/plugin-ecommerce'
|
||||
|
||||
export const Providers = ({ children }) => (
|
||||
<EcommerceProvider
|
||||
enableVariants={true}
|
||||
currenciesConfig={{
|
||||
supportedCurrencies: [USD, EUR],
|
||||
defaultCurrency: 'USD',
|
||||
}}
|
||||
paymentMethods={[stripeAdapterClient]}
|
||||
>
|
||||
{children}
|
||||
</EcommerceProvider>
|
||||
)
|
||||
```
|
||||
|
||||
### Key Props
|
||||
|
||||
| Prop | Default | Notes |
|
||||
|------|---------|-------|
|
||||
| `addressesSlug` | `addresses` | Collection slug |
|
||||
| `cartsSlug` | `carts` | Collection slug |
|
||||
| `customersSlug` | `users` | Collection slug |
|
||||
| `currenciesConfig` | — | `{ supportedCurrencies, defaultCurrency }` |
|
||||
| `paymentMethods` | — | Array of client-side payment adapters |
|
||||
| `enableVariants` | `true` | Product variants support |
|
||||
| `syncLocalStorage` | `true` | Persist cart ID across reloads; `{ key: 'cart' }` for custom key |
|
||||
| `api.apiRoute` | `/api` | Payload API base route |
|
||||
| `api.cartsFetchQuery` | — | Extra depth/select/populate for cart fetches |
|
||||
| `debug` | `false` | Verbose console output |
|
||||
|
||||
## Hooks Reference
|
||||
|
||||
### useCart
|
||||
|
||||
Manages cart state and item operations.
|
||||
|
||||
```tsx
|
||||
const { cart, addItem, removeItem, incrementItem, decrementItem, clearCart, isLoading } = useCart()
|
||||
```
|
||||
|
||||
| Property | Type | Notes |
|
||||
|----------|------|-------|
|
||||
| `cart` | `Cart \| null` | Current cart state |
|
||||
| `addItem` | `(item, quantity?) => void` | Add product/variant |
|
||||
| `removeItem` | `(id) => void` | Remove by ID |
|
||||
| `incrementItem` | `(id) => void` | +1 quantity |
|
||||
| `decrementItem` | `(id) => void` | -1 quantity; removes at 0 |
|
||||
| `clearCart` | `() => void` | Empty the cart |
|
||||
| `isLoading` | `boolean` | Any async op in flight |
|
||||
|
||||
### usePayments
|
||||
|
||||
Two-step checkout: `initiatePayment` → `confirmOrder`.
|
||||
|
||||
```tsx
|
||||
const { initiatePayment, confirmOrder, paymentMethods, selectedPaymentMethod, isLoading } = usePayments()
|
||||
```
|
||||
|
||||
**Step 1 — initiate** (cart verified, transaction created, no order yet):
|
||||
```ts
|
||||
const data = await initiatePayment('stripe', {
|
||||
additionalData: { customerEmail, billingAddress, shippingAddress },
|
||||
})
|
||||
// data.client_secret → complete payment with Stripe.js
|
||||
```
|
||||
|
||||
**Step 2 — confirm** (after Stripe.js confirms payment on client):
|
||||
```ts
|
||||
const data = await confirmOrder('stripe', {
|
||||
additionalData: { paymentIntentID: paymentIntent.id, customerEmail },
|
||||
})
|
||||
// data.orderID → redirect to order page
|
||||
```
|
||||
|
||||
> Order is only created after `confirmOrder`. For off-site redirects (e.g. Stripe callback URL), route the relevant info back before calling `confirmOrder`.
|
||||
|
||||
### useAddresses
|
||||
|
||||
```tsx
|
||||
const { addresses, createAddress, updateAddress, isLoading } = useAddresses()
|
||||
```
|
||||
|
||||
| Property | Type |
|
||||
|----------|------|
|
||||
| `addresses` | `Address[]` |
|
||||
| `createAddress` | `(data) => Promise<Address>` |
|
||||
| `updateAddress` | `(id, partial) => Promise<Address>` |
|
||||
|
||||
### useCurrency
|
||||
|
||||
Formats prices (stored as integers) for display:
|
||||
|
||||
```tsx
|
||||
const { currency, setCurrency, formatPrice, currenciesConfig } = useCurrency()
|
||||
|
||||
<div>{formatPrice(amount)}</div> // handles decimals per currency
|
||||
```
|
||||
|
||||
- All prices are stored as integers (avoids float precision issues)
|
||||
- `formatPrice` accounts for currency-specific decimal places
|
||||
|
||||
### useEcommerceConfig
|
||||
|
||||
Access collection slugs and API settings for custom API calls:
|
||||
|
||||
```tsx
|
||||
const { cartsSlug, customersSlug, addressesSlug, api } = useEcommerceConfig()
|
||||
const endpoint = `${api.apiRoute}/${cartsSlug}`
|
||||
```
|
||||
|
||||
### useEcommerce (unified hook)
|
||||
|
||||
Combines all hooks. Also exposes session management methods:
|
||||
|
||||
```tsx
|
||||
const {
|
||||
cart, addresses, config, isLoading,
|
||||
selectedPaymentMethod, clearSession,
|
||||
onLogin, onLogout, mergeCart, refreshCart,
|
||||
} = useEcommerce()
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### onLogin
|
||||
|
||||
Call after successful login to merge guest cart → user cart:
|
||||
|
||||
```tsx
|
||||
const response = await fetch('/api/users/login', { method: 'POST', body: ... })
|
||||
if (response.ok) await onLogin()
|
||||
```
|
||||
|
||||
Handles: fetch user data → merge guest items → transfer guest cart (if user has none) → clear guest secrets.
|
||||
|
||||
### onLogout / clearSession
|
||||
|
||||
```tsx
|
||||
await fetch('/api/users/logout', { method: 'POST' })
|
||||
onLogout() // alias for clearSession()
|
||||
```
|
||||
|
||||
Clears: cart data, cart ID, cart secret, addresses, user state (memory + localStorage).
|
||||
|
||||
### mergeCart
|
||||
|
||||
Manual cart merge (e.g. custom flows):
|
||||
|
||||
```ts
|
||||
const mergedCart = await mergeCart('user-cart-id', 'guest-cart-id', 'guest-cart-secret')
|
||||
// Source cart is deleted after merge; matching items have quantities combined
|
||||
```
|
||||
|
||||
### refreshCart
|
||||
|
||||
Force-sync cart from server:
|
||||
|
||||
```tsx
|
||||
const { refreshCart } = useEcommerce()
|
||||
<button onClick={refreshCart}>Refresh</button>
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- **Two-step checkout**: `initiatePayment` verifies cart and creates a transaction; `confirmOrder` creates the actual order — never skip step 2.
|
||||
- **Integer prices**: All prices stored as integers; always use `formatPrice()` for display — never divide manually.
|
||||
- **Guest → auth cart merge**: Call `onLogin()` after login, not after — it handles the merge automatically.
|
||||
- **`syncLocalStorage`**: Cart ID persists across reloads by default; use `{ key: 'custom' }` to avoid key collisions.
|
||||
- **`useEcommerce` is the kitchen-sink hook**: Use focused hooks (`useCart`, `usePayments`) for tree-shaking; `useEcommerce` for convenience when you need everything.
|
||||
- **`api.cartsFetchQuery.depth`**: Bump to 2+ if you need populated product/variant data in the cart response.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/ecommerce|Ecommerce Plugin — Setup & Overview]]
|
||||
- [[wiki/payloadcms/ecommerce-advanced|Ecommerce — Advanced / Piecemeal Collections API]]
|
||||
- [[wiki/payloadcms/authentication-overview|Authentication — Overview]]
|
||||
- [[wiki/payloadcms/queries|Queries — depth, select, populate]]
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/ecommerce__frontend.md` — https://payloadcms.com/docs/ecommerce/frontend
|
||||
181
wiki/payloadcms/ecommerce-payment-adapters.md
Normal file
181
wiki/payloadcms/ecommerce-payment-adapters.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
---
|
||||
title: "Ecommerce — Payment Adapters"
|
||||
aliases: [payment-adapter, stripe-adapter, payload-payments]
|
||||
tags: [payloadcms, ecommerce, stripe, payments, plugin]
|
||||
sources: [raw/ecommerce__payments.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Ecommerce Plugin uses a **payment adapter pattern** — each payment provider is a self-contained unit that plugs into `/api/payments/{provider_name}/{action}`. Currently only Stripe ships out of the box, but you can implement your own.
|
||||
|
||||
## REST Endpoints (Auto-generated)
|
||||
|
||||
| Action | Method | Description |
|
||||
|--------|--------|-------------|
|
||||
| `initiate` | POST | Create transaction with status "Processing" |
|
||||
| `confirm-order` | POST | Create order after payment completes on frontend |
|
||||
|
||||
Endpoint path: `/api/payments/{provider_name}/{action}`
|
||||
|
||||
## Stripe Setup
|
||||
|
||||
```bash
|
||||
pnpm add stripe # >= 18.5.0
|
||||
```
|
||||
|
||||
```ts
|
||||
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
|
||||
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
|
||||
|
||||
ecommercePlugin({
|
||||
payments: {
|
||||
paymentMethods: [
|
||||
stripeAdapter({
|
||||
secretKey: process.env.STRIPE_SECRET_KEY,
|
||||
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET, // optional
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Stripe Config Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `secretKey` | `string` | From Stripe Dashboard → API keys |
|
||||
| `publishableKey` | `string` | From Stripe Dashboard → API keys |
|
||||
| `webhookSecret` | `string` | Optional — required for webhooks |
|
||||
| `appInfo` | `object` | Optional `{ name, version }` for Stripe logs |
|
||||
| `webhooks` | `object` | Optional — map of `event_type → handler fn` |
|
||||
| `groupOverrides` | `object` | Optional — override default transaction group fields |
|
||||
|
||||
### Stripe Webhooks
|
||||
|
||||
Webhooks are **optional** — the plugin doesn't use them internally. Handler signature:
|
||||
|
||||
```ts
|
||||
webhooks: {
|
||||
'payment_intent.succeeded': ({ event, req, stripe }) => {
|
||||
req.payload.logger.info('Payment succeeded')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Development (Stripe CLI)
|
||||
|
||||
```bash
|
||||
stripe login
|
||||
stripe listen --forward-to localhost:3000/api/payments/stripe/webhooks
|
||||
# Copy the whsec_... secret shown → paste to .env as STRIPE_WEBHOOKS_SIGNING_SECRET
|
||||
stripe trigger payment_intent.succeeded # test
|
||||
```
|
||||
|
||||
> **Gotcha:** The webhook signing secret from `stripe listen` is ephemeral — it changes on CLI restart. Update `.env` and restart dev server each time.
|
||||
|
||||
### Frontend Usage
|
||||
|
||||
```ts
|
||||
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react'
|
||||
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
|
||||
|
||||
<EcommerceProvider
|
||||
paymentMethods={[
|
||||
stripeAdapterClient({ publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '' }),
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</EcommerceProvider>
|
||||
```
|
||||
|
||||
Use `usePayments` hook for `initiatePayment` / `confirmOrder` — see [[wiki/payloadcms/ecommerce-frontend|Ecommerce Frontend]].
|
||||
|
||||
## Custom Payment Adapter
|
||||
|
||||
Implement the `PaymentAdapter` interface:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | `string` | Provider ID — used in API path and frontend |
|
||||
| `label` | `string` | Optional human-readable label |
|
||||
| `initiatePayment` | `fn` | Called at `/initiate` — creates transaction with status "Processing" |
|
||||
| `confirmOrder` | `fn` | Called at `/confirm-order` — creates order after client payment completes |
|
||||
| `endpoints` | `Endpoint[]` | Optional extra endpoints at `/api/payments/{name}/...` |
|
||||
| `group` | `GroupField` | Payload group field for storing provider-specific data on transactions |
|
||||
|
||||
### `initiatePayment` Args
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `transactionsSlug` | The transaction being processed |
|
||||
| `data.billingAddress` | Billing address |
|
||||
| `data.shippingAddress` | Optional — fall back to billing |
|
||||
| `data.cart` | Cart collection item |
|
||||
| `data.customerEmail` | Required for guest checkouts |
|
||||
| `data.currency` | Payment currency |
|
||||
| `req` | PayloadRequest |
|
||||
|
||||
Returns `{ message: string, ...extra }`. Throw to return 4xx/5xx.
|
||||
|
||||
Transaction is created here with status **"Processing"** — abandoned purchases stay here.
|
||||
|
||||
### `confirmOrder` Args
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `ordersSlug` | Orders collection slug |
|
||||
| `transactionsSlug` | Transactions slug |
|
||||
| `cartsSlug` | Carts slug |
|
||||
| `customersSlug` | Customers slug |
|
||||
| `data.customerEmail` | For guest checkouts |
|
||||
| `req` | PayloadRequest |
|
||||
|
||||
Returns `{ message, orderID, transactionID }`. Creates order, marks cart `purchasedAt`, updates transaction status to `"succeeded"`.
|
||||
|
||||
### Payment Fields (Group Field)
|
||||
|
||||
Each adapter must supply a `group` GroupField to store provider-specific data on transactions:
|
||||
|
||||
```ts
|
||||
// Stripe example — stored under transaction.stripe.*
|
||||
const groupField: GroupField = {
|
||||
name: 'stripe',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{ name: 'customerID', type: 'text' },
|
||||
{ name: 'paymentIntentID', type: 'text' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Adapter
|
||||
|
||||
Implements `PaymentAdapterClient`:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `name` | `string` | Provider ID |
|
||||
| `label` | `string` | Optional display label |
|
||||
| `initiatePayment` | `boolean` | Whether to call `/initiate` endpoint |
|
||||
| `confirmOrder` | `boolean` | Whether to call `/confirm-order` endpoint |
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- **Two-step checkout:** `initiatePayment` (server-side, creates Stripe PaymentIntent) → client confirms payment → `confirmOrder` (server-side, creates Order)
|
||||
- **Server-side price validation is mandatory** — never trust price info from client
|
||||
- **Pass only transaction ID to frontend**, never the full transaction object or secret key
|
||||
- **Webhook signing secret is ephemeral** in local dev — update `.env` on every `stripe listen` restart
|
||||
- **`NEXT_PUBLIC_` prefix required** for publishable key to reach client-side in Next.js
|
||||
- Plugin creates endpoints automatically; custom adapters just implement the interface
|
||||
- Stripe webhooks are optional — core plugin flow works without them
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/ecommerce__payments.md`
|
||||
- [[wiki/payloadcms/ecommerce|Ecommerce Plugin Overview]]
|
||||
- [[wiki/payloadcms/ecommerce-frontend|Ecommerce Frontend React Utilities]]
|
||||
- [[wiki/payloadcms/ecommerce-advanced|Ecommerce Advanced / Piecemeal Collections]]
|
||||
|
|
@ -3,6 +3,7 @@ tags: [payloadcms, tech-patterns]
|
|||
topic: payloadcms
|
||||
sources: [ecommerce__overview.md, ecommerce__plugin.md, ecommerce__payments.md, ecommerce__frontend.md, ecommerce__advanced.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
# PayloadCMS — Ecommerce
|
||||
|
|
@ -176,6 +177,37 @@ products: {
|
|||
}
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
The plugin ships admin UI translations under the `plugin-ecommerce` namespace. Import and register them explicitly:
|
||||
|
||||
```ts
|
||||
import { en } from '@payloadcms/translations/languages/en'
|
||||
import { enTranslations as ecommerceEn } from '@payloadcms/plugin-ecommerce/translations/languages/en'
|
||||
|
||||
buildConfig({
|
||||
i18n: {
|
||||
supportedLanguages: { en },
|
||||
translations: { en: ecommerceEn },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Override specific strings by spreading and patching the namespace:
|
||||
|
||||
```ts
|
||||
translations: {
|
||||
en: {
|
||||
...ecommerceEn,
|
||||
'plugin-ecommerce': {
|
||||
...ecommerceEn['plugin-ecommerce'],
|
||||
cart: 'Shopping Cart',
|
||||
orders: 'My Orders',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`publicAccess` is the only default** — all other access functions must be implemented by you
|
||||
|
|
|
|||
201
wiki/payloadcms/email.md
Normal file
201
wiki/payloadcms/email.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
---
|
||||
title: "Email — Adapters, SMTP, Resend, Attachments"
|
||||
aliases: [payload-email, payloadcms-email, nodemailer-payload]
|
||||
tags: [payloadcms, email, nodemailer, resend, smtp, attachments]
|
||||
sources: [raw/email__overview.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Payload uses an **adapter pattern** for email. Pass an adapter into `email` property of `buildConfig`. Without it, Payload logs a warning on startup and on every `sendEmail` call.
|
||||
|
||||
Two official adapters:
|
||||
|
||||
| Adapter | Package | Best for |
|
||||
|---------|---------|----------|
|
||||
| **Nodemailer** | `@payloadcms/email-nodemailer` | SMTP, SendGrid, any Nodemailer transport; easy v2→v3 migration |
|
||||
| **Resend** | `@payloadcms/email-resend` | Vercel / serverless — much lighter than Nodemailer |
|
||||
|
||||
Both require at minimum: `defaultFromAddress` + `defaultFromName`.
|
||||
|
||||
---
|
||||
|
||||
## Nodemailer Adapter
|
||||
|
||||
### SMTP via `transportOptions`
|
||||
|
||||
```ts
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
|
||||
export default buildConfig({
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@example.com',
|
||||
defaultFromName: 'My App',
|
||||
transportOptions: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
### Pre-created transport via `transport`
|
||||
|
||||
```ts
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@example.com',
|
||||
defaultFromName: 'My App',
|
||||
transport: nodemailer.createTransport({ /* ... */ }),
|
||||
})
|
||||
```
|
||||
|
||||
### SendGrid custom transport
|
||||
|
||||
```ts
|
||||
import nodemailerSendgrid from 'nodemailer-sendgrid'
|
||||
|
||||
email: nodemailerAdapter({
|
||||
defaultFromAddress: 'info@example.com',
|
||||
defaultFromName: 'My App',
|
||||
transportOptions: nodemailerSendgrid({ apiKey: process.env.SENDGRID_API_KEY }),
|
||||
})
|
||||
```
|
||||
|
||||
### Dev mode (no config)
|
||||
|
||||
Calling `nodemailerAdapter()` with no args uses **ethereal.email** — logs credentials to console. Safe for local development.
|
||||
|
||||
```ts
|
||||
email: nodemailerAdapter()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resend Adapter
|
||||
|
||||
Preferred for **Vercel / serverless**. Requires only an API key.
|
||||
|
||||
```ts
|
||||
import { resendAdapter } from '@payloadcms/email-resend'
|
||||
|
||||
email: resendAdapter({
|
||||
defaultFromAddress: 'dev@example.com',
|
||||
defaultFromName: 'My App',
|
||||
apiKey: process.env.RESEND_API_KEY || '',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sending Email
|
||||
|
||||
Available anywhere `payload` instance is accessible:
|
||||
|
||||
```ts
|
||||
await payload.sendEmail({
|
||||
to: 'user@example.com',
|
||||
subject: 'Hello',
|
||||
html: '<p>Body</p>', // or: text: 'Body'
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Attachments
|
||||
|
||||
### Nodemailer — file path or Buffer
|
||||
|
||||
```ts
|
||||
await payload.sendEmail({
|
||||
to: 'user@example.com',
|
||||
subject: 'Report',
|
||||
html: '<p>See attached.</p>',
|
||||
attachments: [
|
||||
{ filename: 'invoice.pdf', path: '/var/data/invoice.pdf', contentType: 'application/pdf' },
|
||||
{ filename: 'report.csv', content: Buffer.from('col1,col2\nA,B\n'), contentType: 'text/csv' },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Supports anything Nodemailer supports: streams, Buffers, URLs, inline CID images.
|
||||
|
||||
### Resend — remote URL or Base64
|
||||
|
||||
```ts
|
||||
// Remote URL (Resend fetches it)
|
||||
attachments: [{ path: 'https://example.com/invoice.pdf', filename: 'invoice.pdf' }]
|
||||
|
||||
// Local file — must be Base64
|
||||
import { readFile } from 'node:fs/promises'
|
||||
const pdf = await readFile('/var/data/invoice.pdf')
|
||||
attachments: [{ filename: 'invoice.pdf', content: pdf.toString('base64') }]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Attaching Files from Payload Media Collections
|
||||
|
||||
### Local storage + Nodemailer
|
||||
|
||||
```ts
|
||||
const doc = await payload.findByID({ collection: 'media', id })
|
||||
await payload.sendEmail({
|
||||
attachments: [{ filename: doc.filename, path: doc.url, contentType: doc.mimeType }],
|
||||
})
|
||||
```
|
||||
|
||||
### Cloud storage (S3/Azure/GCS) + Nodemailer
|
||||
|
||||
```ts
|
||||
const response = await fetch(doc.url)
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
attachments: [{ filename: doc.filename, content: buffer, contentType: doc.mimeType }]
|
||||
```
|
||||
|
||||
### Resend + cloud storage
|
||||
|
||||
```ts
|
||||
// Resend fetches the URL directly — no download needed
|
||||
attachments: [{ filename: doc.filename, path: doc.url }]
|
||||
```
|
||||
|
||||
### Resend + local storage
|
||||
|
||||
```ts
|
||||
import { readFile } from 'node:fs/promises'
|
||||
const buf = await readFile(doc.url)
|
||||
attachments: [{ filename: doc.filename, content: buf.toString('base64') }]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- **Adapter pattern** — swap Nodemailer ↔ Resend without changing send-call code
|
||||
- **Resend for serverless** — significantly lighter bundle than Nodemailer; use on Vercel
|
||||
- **Nodemailer for SMTP/SendGrid** — `transportOptions` object or pre-built `transport`; any Nodemailer transport works
|
||||
- **Dev shortcut** — `nodemailerAdapter()` with no args → ethereal.email auto-config
|
||||
- **Attachments differ by adapter**: Nodemailer accepts file paths/Buffers/streams; Resend expects Base64 for local files, URL for remote
|
||||
- **Multiple providers** — Payload only supports one `email` config, but you can use additional transports manually inside hooks
|
||||
- **Auth emails** — password reset, email verification use same adapter; see [[wiki/payloadcms/authentication-email|Authentication — Email Verification & Password Reset]]
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/authentication-email|Authentication — Email Verification & Password Reset]]
|
||||
- [[wiki/payloadcms/hooks|Hooks]] — trigger custom emails via collection hooks
|
||||
- [[wiki/payloadcms/upload|Upload & Media]] — attaching media collection files to emails
|
||||
- [[wiki/payloadcms/configuration|Payload Config — Overview]] — `buildConfig` top-level options
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/email__overview.md` — compiled 2026-05-15
|
||||
- https://payloadcms.com/docs/email/overview
|
||||
57
wiki/payloadcms/examples-overview.md
Normal file
57
wiki/payloadcms/examples-overview.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: "Payload CMS — Examples Overview"
|
||||
aliases: [payload-examples, payload-starter-templates, payload-boilerplate]
|
||||
tags: [payloadcms, examples, boilerplate, starter, templates]
|
||||
sources: [raw/examples__overview.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Payload ships a curated set of official example projects on GitHub. Each example demonstrates **one specific feature** in isolation — no extra noise, easy to read.
|
||||
|
||||
## Available Examples
|
||||
|
||||
| Example | What it shows |
|
||||
|---------|--------------|
|
||||
| `auth` | Authentication flows (login, register, JWT, cookies) |
|
||||
| `custom-components` | Custom Admin UI React components and slots |
|
||||
| `draft-preview` | Draft + preview workflow with Next.js |
|
||||
| `email` | Email adapter setup and sending |
|
||||
| `form-builder` | Form Builder plugin end-to-end |
|
||||
| `live-preview` | Live Preview iframe inside admin |
|
||||
| `multi-tenant` | Multi-tenant collection isolation |
|
||||
| `tailwind-shadcn-ui` | Tailwind CSS + shadcn/ui in admin |
|
||||
| `whitelabel` | White-label / rebranded Admin UI |
|
||||
|
||||
## Creating a Project from an Example
|
||||
|
||||
```sh
|
||||
npx create-payload-app --example <example_name>
|
||||
```
|
||||
|
||||
Replace `<example_name>` with the folder name from the table above (e.g. `draft-preview`, `multi-tenant`).
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Each example is **self-contained** — clone and run without extra setup.
|
||||
- Start from the example closest to your use case, then extend.
|
||||
- `draft-preview` is the go-to starting point for content-preview workflows with Next.js App Router.
|
||||
- `multi-tenant` is useful for SaaS-style projects where each tenant owns their own data scope.
|
||||
- `tailwind-shadcn-ui` saves setup time when using shadcn components inside a custom admin.
|
||||
- New examples are added continuously — check the GitHub repo before building a custom solution.
|
||||
- Missing a use case? Open a Discussion or PR on the Payload repo.
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/admin-preview|Admin Preview & Draft Preview]]
|
||||
- [[wiki/payloadcms/live-preview|Live Preview]]
|
||||
- [[wiki/payloadcms/custom-components|Custom Components]]
|
||||
- [[wiki/payloadcms/plugins|Official Plugins]]
|
||||
- [[wiki/payloadcms/getting-started|Getting Started]]
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/examples__overview.md`
|
||||
- https://payloadcms.com/docs/examples/overview
|
||||
157
wiki/payloadcms/fields-array.md
Normal file
157
wiki/payloadcms/fields-array.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
---
|
||||
title: "PayloadCMS — Array Field"
|
||||
aliases: [payload-array-field, array-field-payload]
|
||||
tags: [payloadcms, fields, array, cms, typescript]
|
||||
sources: [raw/fields__array.md]
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Array Field stores repeating sets of fields as an array of objects. Each row contains the same field structure you define. Arrays can be nested inside other arrays for infinitely deep data structures.
|
||||
|
||||
**Common use cases:**
|
||||
- Image slider (upload + caption text)
|
||||
- Navigation items (relationship + checkbox "open in new tab")
|
||||
- Event agenda timeslots (date + label + relationship)
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const MyArrayField: Field = {
|
||||
name: 'slider',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'image', type: 'upload', relationTo: 'media', required: true },
|
||||
{ name: 'caption', type: 'text' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `name` | yes | DB property name |
|
||||
| `fields` | yes | Field definitions for each row |
|
||||
| `minRows` | no | Minimum row count during validation |
|
||||
| `maxRows` | no | Maximum row count during validation |
|
||||
| `labels` | no | Customize `singular`/`plural` row labels in Admin |
|
||||
| `defaultValue` | no | Array of row objects as default |
|
||||
| `localized` | no | Localizes entire array (no need to localize each sub-field) |
|
||||
| `required` | no | Field must have a value |
|
||||
| `validate` | no | Custom validation function (runs client + server) |
|
||||
| `saveToJWT` | no | Include in JWT if top-level in auth collection |
|
||||
| `hooks` | no | Field lifecycle hooks |
|
||||
| `access` | no | Field-level access control |
|
||||
| `hidden` | no | Hide from APIs/Admin but still save to DB |
|
||||
| `interfaceName` | no | Reusable TypeScript interface + GraphQL type name |
|
||||
| `dbName` | no | Custom Postgres table name (auto-generated otherwise) |
|
||||
| `virtual` | no | Disable DB persistence or link to a relationship path |
|
||||
| `typescriptSchema` | no | Override generated TS type with a JSON schema |
|
||||
|
||||
## Admin Options
|
||||
|
||||
```ts
|
||||
admin: {
|
||||
initCollapsed: true, // start rows collapsed
|
||||
isSortable: false, // disable drag-to-reorder (default: true)
|
||||
components: {
|
||||
RowLabel: './RowLabel', // custom React component per row
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Row Label
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useRowLabel } from '@payloadcms/ui'
|
||||
|
||||
export const ArrayRowLabel = () => {
|
||||
const { data, rowNumber } = useRowLabel<{ title?: string }>()
|
||||
return <div>{data.title || 'Slide'} {String(rowNumber).padStart(2, '0')}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
### `unique: true` on nested fields
|
||||
Setting `unique: true` on a field **inside** an array creates a **collection-wide** unique index — not per-document. Two documents cannot share the same value at that nested path. On MongoDB, documents without the array collide on `null`.
|
||||
|
||||
**Fix:** Use a custom `validate` function on the array field to enforce uniqueness within a single document's rows.
|
||||
|
||||
### Localization
|
||||
When `localized: true` on the array itself, all sub-fields are localized automatically. No need to mark each nested field.
|
||||
|
||||
## Full Example
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const ExampleCollection: CollectionConfig = {
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
{
|
||||
name: 'slider',
|
||||
type: 'array',
|
||||
label: 'Image Slider',
|
||||
minRows: 2,
|
||||
maxRows: 10,
|
||||
interfaceName: 'CardSlider',
|
||||
labels: { singular: 'Slide', plural: 'Slides' },
|
||||
fields: [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'image', type: 'upload', relationTo: 'media', required: true },
|
||||
{ name: 'caption', type: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Components
|
||||
|
||||
### Server Component (Field)
|
||||
```tsx
|
||||
import { ArrayField } from '@payloadcms/ui'
|
||||
import type { ArrayFieldServerComponent } from 'payload'
|
||||
|
||||
export const CustomArrayFieldServer: ArrayFieldServerComponent = ({
|
||||
clientField, path, schemaPath, permissions,
|
||||
}) => <ArrayField field={clientField} path={path} schemaPath={schemaPath} permissions={permissions} />
|
||||
```
|
||||
|
||||
### Client Component (Field)
|
||||
```tsx
|
||||
'use client'
|
||||
import { ArrayField } from '@payloadcms/ui'
|
||||
import type { ArrayFieldClientComponent } from 'payload'
|
||||
|
||||
export const CustomArrayFieldClient: ArrayFieldClientComponent = (props) => <ArrayField {...props} />
|
||||
```
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
- Use `type: 'array'` with a `fields` array to define repeating row structure
|
||||
- `minRows` / `maxRows` add validation bounds without custom code
|
||||
- `localized: true` on the array localizes all sub-fields — don't set it per sub-field
|
||||
- `unique: true` inside an array = collection-wide DB index, not per-row uniqueness; use `validate` instead
|
||||
- `interfaceName` generates a reusable TypeScript interface for the row shape
|
||||
- Custom `RowLabel` via `admin.components.RowLabel` + `useRowLabel` hook for dynamic row headings
|
||||
- Arrays can be nested inside arrays for complex nested data
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/payloadcms/fields-complex|Fields: Complex]] — all complex field types overview
|
||||
- [[wiki/payloadcms/fields-basic|Fields: Basic]] — scalar field reference
|
||||
- [[wiki/payloadcms/database-indexes|Database Indexes]] — unique index behavior and gotchas
|
||||
- [[wiki/payloadcms/localization|Localization]] — how `localized: true` works across field types
|
||||
- [[wiki/payloadcms/typescript|TypeScript]] — `interfaceName` and type generation
|
||||
- [[wiki/payloadcms/hooks|Hooks]] — field lifecycle hooks
|
||||
|
||||
## Sources
|
||||
|
||||
- `raw/fields__array.md` — https://payloadcms.com/docs/fields/array
|
||||
Loading…
Add table
Reference in a new issue