Icons representing cloud object storage and delivery platforms used for media and asset hosting.

Payload CMS media regeneration

Published 27 December, 2025

This tutorial documents a controlled two-step process for repairing and normalizing media files stored in a media collection.

The approach is designed to mitigate risk, preserve file integrity, and standardize filenames and MIME types without disrupting downstream consumers.

The process is intentionally split into two scripts to isolate concerns and allow validation between phases.

This method is compatible with cloud-based storage adapters, including R2-compatible storage, Amazon S3, and similar remote object storage providers.


Icons representing cloud object storage and delivery platforms used for media and asset hosting.

Overview

The workflow consists of:

  1. Media regeneration
    Re-ingesting existing media binaries to trigger Payload’s internal processing (image sizes, metadata, storage consistency).
  2. Normalization
    Standardizing filenames and extensions while enforcing valid MIME mappings.

Each step supports dry-run execution to ensure operational safety before live updates.


Regeneration

This step reprocesses existing media files by downloading each asset and re-uploading it back into the same document record. The objective is to regenerate derived assets such as responsive image sizes or metadata that may be missing or corrupted.

Objectives

  • Reads all documents from the media collection
  • Fetches the binary from the stored URL
  • Re-uploads the file using payload.update
  • Preserves document IDs and relationships
  • Supports dry-run logging without mutation

/debug/regenerateMedia.ts

typescript
import config from '@payload-config'
import { getPayload } from 'payload'
 
const collection = 'media' // Adjust to your target collection
const dryRun = true // Set to false for live runs
const limit = 1000 // Adjust to your desired limit
 
async function regenerateMedia(): Promise<void> {
  const payload = await getPayload({ config })
 
  const media = await payload.find({
    collection,
    depth: 0,
    limit,
  })
 
  if (!media.totalDocs) {
    payload.logger.info('No media found')
    return
  }
 
  payload.logger.info(`[${dryRun ? 'Dry Run' : 'Live'}] Found ${media.totalDocs} media files`)
 
  for (const doc of media.docs) {
    if (!doc.url || !doc.filename || !doc.mimeType) {
      payload.logger.warn(`Skipping ${doc.id} (missing data)`)
      continue
    }
 
    try {
      const res = await fetch(doc.url)
      if (!res.ok) throw new Error(`Fetch failed: ${res.status}`)
 
      const buffer = Buffer.from(await res.arrayBuffer())
 
      payload.logger.info(
        `[${dryRun ? 'Dry Run' : 'Live'}] ${doc.filename} > ${buffer.length} bytes (${doc.mimeType})`
      )
 
      if (dryRun) {
        payload.logger.debug(`[Dry Run] Would regenerate sizes for ${doc.filename}`)
        continue
      }
 
      await payload.update({
        collection,
        data: {},
        file: {
          data: buffer,
          mimetype: doc.mimeType,
          name: doc.filename,
          size: buffer.length,
        },
        id: doc.id,
      })
 
      payload.logger.info(`Regenerated ${doc.filename}`)
    } catch (err) {
      payload.logger.error(`Failed ${doc.filename}`)
      console.error(err)
    }
  }
 
  payload.logger.info(`[${dryRun ? 'DRY RUN' : 'LIVE'}] Complete`)
}
 
await regenerateMedia()

Considerations

  • Network failures are isolated per document
  • No filename or MIME mutation occurs in this step
  • Safe to rerun multiple times

Normalization

Normalization is a required second step due to constraints imposed by cloud-based object storage systems.

Storage providers such as S3-compatible services and R2 do not support in-place object replacement when filenames already exist. During the regeneration process, Payload CMS therefore creates new objects with suffixed filenames (for example, image-1.jpg, image-2.jpg) to avoid collisions.

The normalization phase resolves this side effect by re-ingesting each asset with a deterministic, cleaned filename. Numeric suffixes introduced during regeneration are removed, valid extensions are enforced, and MIME types are corrected where necessary.

This ensures that regenerated media conforms to the project’s intended naming conventions while preserving the newly generated size variants produced by the updated media schema.

Objectives

  • Remove duplicate suffixes (e.g. -1, -2)
  • Enforce valid extensions
  • Derive missing extensions from MIME type
  • Correct known MIME edge cases such as SVG

/debug/renameMedia.ts

typescript
import config from '@payload-config'
import mime from 'mime-types'
import { getPayload } from 'payload'
 
const collection = 'media' // Adjust to your target collection
const dryRun = true // Set to false for live runs
const limit = 1000 // Adjust to your desired limit
 
const validExt = new Set([
  'aac','avi','avif','bmp','doc','docx','flac','gif','jpeg','jpg',
  'm4a','m4v','mkv','mov','mp3','mp4',
  'ogg','pdf','png','ppt','pptx','svg',
  'tif','tiff','wav','webm','webp','xls','xlsx',
  'zip',
])
 
function deriveFilename(filename: string, mimeType: string): string {
  const hasDot = filename.includes('.')
  const ext = filename.split('.').pop()?.toLowerCase()
  const base = hasDot ? filename.slice(0, -(ext!.length + 1)) : filename
 
  const cleanBase = normalizeBase(base)
 
  if (ext && validExt.has(ext)) {
    return `${cleanBase}.${ext}`
  }
 
  const derivedExt = mime.extension(mimeType)
  return derivedExt ? `${cleanBase}.${derivedExt}` : cleanBase
}
 
function normalizeBase(name: string): string {
  return name.replace(/-\d+$/, '')
}
 
async function normalizeMedia(): Promise<void> {
  const payload = await getPayload({ config })
 
  const media = await payload.find({
    collection,
    depth: 0,
    limit,
    pagination: false,
  })
 
  payload.logger.info(
    `[${dryRun ? 'Dry Run' : 'Live'}] Scanning ${media.docs.length} media files`,
  )
 
  for (const doc of media.docs) {
    if (!doc.filename || !doc.url || !doc.mimeType) continue
 
    const nextName = deriveFilename(doc.filename, doc.mimeType)
 
    if (nextName === doc.filename) continue
 
    payload.logger.info(
      `[${dryRun ? 'Dry Run' : 'Live'}] ${doc.filename} > ${nextName}`,
    )
 
    if (dryRun) continue
 
    try {
      const res = await fetch(doc.url)
      if (!res.ok) throw new Error(`Fetch failed: ${res.status}`)
 
      const buffer = Buffer.from(await res.arrayBuffer())
 
      await payload.update({
        collection,
        data: {},
        file: {
          data: buffer,
          mimetype: normalizeMimeType(nextName, doc.mimeType),
          name: nextName,
          size: buffer.length,
        },
        id: doc.id,
      })
    } catch (err) {
      payload.logger.error(`Failed ${doc.filename}`)
      console.error(err)
    }
  }
 
  payload.logger.info(`[${dryRun ? 'Dry Run' : 'Live'}] Media normalization complete`)
}
 
await normalizeMedia()
 
function normalizeMimeType(filename: string, mimeType: string): string {
  if (filename.toLowerCase().endsWith('.svg')) {
    return 'image/svg+xml'
  }
  return mimeType
}

Package

Before executing either phase, the regeneration and normalization scripts must be registered in the project’s package.json. This ensures repeatable execution, auditability, and alignment with standard Node.js operational workflows.

Registering scripts also reduces the risk of ad-hoc execution errors and enables consistent invocation across local, staging, and production environments.

json
{
  "scripts": {
    "regenerate:images": "payload run debug/regenerateMedia.ts",
    "regenerate:rename": "payload run debug/renameMedia.ts"
  }
}

Execution

This workflow is designed to safely restore and standardize media assets stored in Payload CMS while minimizing operational and data integrity risk.

Start your development server. Without an active dev server, the commands cannot connect to the database or image storage adapter.

Regenerate media

  • Run regeneration with dryRun = true
  • Validate logs and byte sizes
  • Run regeneration live
bash
pnpm run regenerate:images

renameMedia

  • Run normalization with dryRun = true
  • Validate filename changes
  • Run normalization live
bash
pnpm run regenerate:rename

This two-step workflow provides a controlled and reliable method for updating Payload CMS media following changes to the media size schema.

The regeneration step re-ingests existing media binaries, allowing Payload CMS to naturally regenerate all required derived file sizes based on the current project configuration. This ensures internal consistency without direct manipulation of storage objects.

Executed sequentially and validated through dry-run mode, this approach enables teams to adapt existing media libraries to evolving schema requirements while maintaining predictable asset naming, storage integrity, and operational control.

Vincent is the founder and director of Rubix Studios, with over 20 years of experience in branding, marketing, film, photography, and web development. He is a certified partner with industry leaders including Google, Microsoft, AWS, and HubSpot. Vincent also serves as a member of the Maribyrnong City Council Business and Innovation Board and is undertaking an Executive MBA at RMIT University.