Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/email/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,77 @@ const email = await payload.sendEmail({
})
```

## Sending email with attachments

**Nodemailer adapter (SMTP/SendGrid/etc.)**

Works with `@payloadcms/email-nodemailer` and any Nodemailer transport.

```ts
await payload.sendEmail({
to: '[email protected]',
subject: 'Your report',
html: '<p>See attached.</p>',
attachments: [
// From a file path (local disk, mounted volume, etc.)
{
filename: 'invoice.pdf',
path: '/var/data/invoice.pdf',
contentType: 'application/pdf',
},
// From a Buffer you generated at runtime
{
filename: 'report.csv',
content: Buffer.from('col1,col2\nA,B\n'),
contentType: 'text/csv',
},
],
})
```

Anything supported by Nodemailer’s attachments—streams, Buffers, URLs, content IDs for inline images (cid), etc.—will work here.

**Resend adapter**

Works with @payloadcms/email-resend.

For attachments from remote URLs

```ts
await payload.sendEmail({
to: '[email protected]',
subject: 'Your invoice',
html: '<p>Thanks! Invoice attached.</p>',
attachments: [
{
// Resend will fetch this URL
path: 'https://example.com/invoices/1234.pdf',
filename: 'invoice-1234.pdf',
},
],
})
```

For a local file

```ts
import { readFile } from 'node:fs/promises'

const pdf = await readFile('/var/data/invoice.pdf')
await payload.sendEmail({
to: '[email protected]',
subject: 'Your invoice',
html: '<p>Thanks! Invoice attached.</p>',
attachments: [
{
filename: 'invoice.pdf',
// Resend expects Base64 here
content: pdf.toString('base64'),
},
],
})
```

## Using multiple mail providers

Payload supports the use of a single transporter of email, but there is nothing stopping you from having more. Consider a use case where sending bulk email is handled differently than transactional email and could be done using a [hook](/docs/hooks/overview).
41 changes: 41 additions & 0 deletions docs/fields/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,47 @@ _\* An asterisk denotes that a property is required._
used as a GraphQL enum.
</Banner>

### Limitations for Arrays / Nested Fields (especially on MongoDB)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we discussing array and block field limitations inside of the select field docs page?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this update was meant to provide clarity regarding this: #12836


<Banner type="warning">
Avoid unique: true on fields nested inside an array or blocks.
</Banner>
In MongoDB this creates a collection-wide unique multikey index; missing values
are treated like null and will collide, causing duplicate-key errors on
insert/update. If you need uniqueness within a parent document’s array (or
conditional uniqueness), use a custom validate function or a hook.

If you need collection-wide uniqueness for values that currently live in an array, consider
normalizing those values into a top-level field or a separate collection where a
standard unique index makes sense.

Example:

```ts
import type { Field } from 'payload'

export const ItemsArray: Field = {
name: 'items',
type: 'array',
fields: [
{
name: 'code',
type: 'text',
// DO NOT use unique: true here; see note above
validate: async (value, { data }) => {
// value is the current 'code'; data.items is the full array
if (!value || !Array.isArray(data?.items)) return true
const codes = data.items.map((i) => i?.code ?? '').filter(Boolean)
const duplicates = new Set(
codes.filter((c, i) => codes.indexOf(c) !== i),
)
return duplicates.size === 0 || 'Codes in this array must be unique.'
},
},
],
}
```

### filterOptions

Used to dynamically filter which options are available based on the current user, document data, or other criteria.
Expand Down
7 changes: 4 additions & 3 deletions docs/production/deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ const nextConfig = {
Dockerfile

```dockerfile
# Dockerfile
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.js file.
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

FROM node:18-alpine AS base
FROM node:22-alpine AS base

# Install dependencies only when needed
FROM base AS deps
Expand Down Expand Up @@ -204,6 +204,7 @@ ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Remove this line if you do not have this folder
COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
Expand Down Expand Up @@ -235,7 +236,7 @@ version: '3'

services:
payload:
image: node:18-alpine
image: node:22-alpine
ports:
- '3000:3000'
volumes:
Expand Down
47 changes: 47 additions & 0 deletions docs/upload/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,53 @@ export const Media: CollectionConfig = {
}
```

3. Dynamic thumbnails via hooks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you use this method over the 2nd method? That should be explained in the docs if there is a good reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11453


```ts
import type { CollectionConfig, AfterChangeHook, AfterReadHook } from 'payload'

// Example helper that builds a CDN URL (your logic here)
const buildThumbURL = ({ filename }: { filename?: string }) =>
filename ? `https://cdn.example.com/thumbs/${filename}.jpg` : undefined

const setThumbURL: AfterChangeHook = async ({ doc, operation }) => {
// compute a thumbnail URL (first frame, resized, etc.)
const thumbnailURL = buildThumbURL({ filename: doc?.filename })
// persist to the same doc so the Admin can reuse it
return { ...doc, thumbnailURL }
}

const exposeThumbURL: AfterReadHook = async ({ doc }) => {
// ensure the field is always present on reads
return {
...doc,
thumbnailURL:
doc.thumbnailURL ?? buildThumbURL({ filename: doc?.filename }),
}
}

export const Media: CollectionConfig = {
slug: 'media',
upload: true,
admin: {
// Use the field value for the Admin thumbnail
adminThumbnail: ({ doc }) => doc?.thumbnailURL,
},
hooks: {
afterChange: [setThumbURL],
afterRead: [exposeThumbURL],
},
fields: [
// store the dynamic URL (hidden from editors if you like)
{
name: 'thumbnailURL',
type: 'text',
admin: { hidden: true },
},
],
}
```

## Restricted File Types

Possibly problematic file types are automatically restricted from being uploaded to your application.
Expand Down
21 changes: 21 additions & 0 deletions templates/website/src/components/Media/ImageMedia/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ const { breakpoints } = cssVariables
const placeholderBlur =
'data:image/png;base64,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'

/**
* ImageMedia
*
* This component intentionally passes a **relative** `src` (e.g. `/media/...`),
* so Next.js uses its **built-in image optimization** — no custom `loader` needed.
*
* If your storage/plugin returns **absolute** URLs (e.g. `https://cdn.example.com/...`),
* choose ONE of the following:
* A) Allow the remote host in next.config.js:
* images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }
* B) Provide a per-instance **custom loader** for CDN transforms:
* const imageLoader: ImageLoader = ({ src, width, quality }) =>
* `https://cdn.example.com${src}?w=${width}&q=${quality ?? 75}`
* <Image loader={imageLoader} src="/media/hero.jpg" width={1200} height={600} alt="" />
* C) Skip optimization for that image:
* <Image unoptimized src="https://cdn.example.com/hero.jpg" width={1200} height={600} alt="" />
*
* TL;DR: Template defaults = relative src → no loader prop required.
* Only add `loader` if you’re deliberately using remote/CDN URLs or custom transforms.
*/

export const ImageMedia: React.FC<MediaProps> = (props) => {
const {
alt: altFromProps,
Expand Down
Loading