payload-wordpress-migrator
v0.0.24
Published
A PayloadCMS plugin for WordPress migration - migrate and manage WordPress content directly in your Payload admin dashboard
Maintainers
Readme
Payload WordPress Migrator
A PayloadCMS plugin for migrating WordPress content — manage migrations directly from the Payload admin dashboard.
Features
- Migration Dashboard — real-time progress tracking in the Payload admin UI
- WordPress REST API Integration — secure connectivity with Application Passwords
- Job Management — create, pause, resume, retry, rollback, and dry-run migration jobs
- Content Type Discovery — automatic detection of posts, pages, media, categories, tags, custom post types
- Field Mapping — visual mapping interface with dot-notation support
- Media Migration — auto-import featured images and content media with duplicate detection
- Gutenberg to Lexical — converts WordPress blocks to PayloadCMS Lexical rich text
- Batch Processing — configurable concurrency with background execution
- Access Control — configurable role-based access for migration endpoints
Installation
npm install payload-wordpress-migratorQuick Start
Add the plugin to your Payload config:
import { buildConfig } from 'payload'
import { payloadWordPressMigrator } from 'payload-wordpress-migrator'
export default buildConfig({
plugins: [
payloadWordPressMigrator({
collections: {
posts: { wpPostType: 'post', enableBlocks: true },
pages: { wpPostType: 'page', enableBlocks: true },
media: { wpPostType: 'media' },
},
enableMediaDownload: true,
migrationBatchSize: 25,
}),
],
})WordPress Setup
Application Password:
- In WordPress admin, go to Users > Profile
- Scroll to Application Passwords
- Enter a name (e.g., "Payload Migration") and click Add New Application Password
- Copy the generated password — use this as
WORDPRESS_APP_PASSWORD
Requirements:
- WordPress 5.6+ (for Application Passwords)
- REST API enabled (default in most installations)
- User with appropriate permissions for content access
Configuration
type PayloadWordPressMigratorConfig = {
// WordPress Connection
wpSiteUrl?: string // WordPress site URL
wpUsername?: string // WordPress username
wpPassword?: string // Application password (not the login password)
// Collections
collections?: Partial<Record<CollectionSlug, WordPressCollectionMapping>>
// Plugin Control
disabled?: boolean // Disable plugin (schema still added for DB consistency)
disableDashboard?: boolean // Hide dashboard UI
// Performance
migrationBatchSize?: number // Items per batch (default: 10)
migrationConcurrency?: number // Parallel items within a batch (default: 1)
wpRequestDelay?: number // Delay in ms between WP API requests (default: 0)
// Media
enableMediaDownload?: boolean // Download files from WordPress (default: false)
maxMediaFileSize?: number // Max file size in bytes (default: 10MB)
mediaUploadPath?: string // Custom upload directory for media files
allowedMediaTypes?: string[] // Allowed MIME types
allowSelfSignedCerts?: boolean // Allow self-signed SSL (dev only)
// Access Control
access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>
}
type WordPressCollectionMapping = {
wpPostType: string // WordPress content type slug
fieldMapping?: Record<string, string> // WP field path → Payload field path
enableBlocks?: boolean // Convert Gutenberg blocks to Lexical
customFields?: string[] // ACF field names to migrate
importContentMedia?: boolean // Auto-import images from content HTML
disableHtmlConversion?: boolean // Skip HTML-to-Lexical conversion
}Plugin Architecture
Collections
The plugin creates a wordpress-migration collection for job management and adds migratedFromWordPress metadata fields to configured target collections:
migratedFromWordPress.wpPostId— original WordPress post IDmigratedFromWordPress.wpPostType— WordPress content typemigratedFromWordPress.migrationDate— migration timestamp
API Endpoints
POST /api/wordpress/test-connection # Test WordPress connectivity
GET /api/wordpress/migration-summary # Dashboard data with caching
POST /api/wordpress/discover-content # Content type discovery
GET /api/wordpress/migration-jobs # Job status
POST /api/wordpress/migration-jobs # Start/retry/resume/rollback jobs
PUT /api/wordpress/migration-jobs # Pause/resume jobs
DELETE /api/wordpress/migration-jobs # Delete jobs
POST /api/wordpress/content-fields # Analyze WordPress fields
GET /api/collections/:slug/fields # Analyze PayloadCMS fieldsSupported Content Types
| Content Type | Features | Notes | | --------------------- | ---------------------------------------------------- | ----------------------------------- | | Posts | Content, metadata, categories, tags, featured images | Full Gutenberg block conversion | | Pages | Content, metadata, featured images, custom fields | Hierarchical structure support | | Media | File downloads, metadata, alt text, captions | MIME type filtering and size limits | | Categories | Names, descriptions, hierarchy, counts | Taxonomy relationships | | Tags | Names, descriptions, post associations | — | | Users | User data, roles, capabilities, avatars | Permission mapping | | Custom Post Types | Auto-discovery, custom fields, metadata | ACF integration |
Media Migration
Two modes:
- Metadata Only (default) — migrates titles, descriptions, alt text, and URLs
- Full File Migration — downloads files from WordPress and uploads to PayloadCMS
payloadWordPressMigrator({
enableMediaDownload: true,
maxMediaFileSize: 50 * 1024 * 1024, // 50MB
allowedMediaTypes: [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'video/mp4',
],
})Auto-import features:
- Featured images — detected via WordPress
_embeddata, downloaded and linked - Content media — scans HTML for
wp-content/uploadsURLs, imports automatically - MediaBlock conversion — HTML
<img>tags converted to PayloadCMS MediaBlocks
Recommended workflow: Migrate posts/pages only — media referenced in content will be auto-imported.
Advanced Configuration
Complete Example
payloadWordPressMigrator({
wpSiteUrl: process.env.WORDPRESS_API_URL,
wpUsername: process.env.WORDPRESS_USERNAME,
wpPassword: process.env.WORDPRESS_APP_PASSWORD,
collections: {
posts: {
wpPostType: 'post',
enableBlocks: true,
importContentMedia: true,
customFields: ['_yoast_wpseo_title', '_yoast_wpseo_metadesc'],
fieldMapping: {
'title.rendered': 'title',
'content.rendered': 'content',
'excerpt.rendered': 'excerpt',
},
},
pages: { wpPostType: 'page', enableBlocks: true, importContentMedia: true },
media: { wpPostType: 'media' },
categories: { wpPostType: 'category' },
products: {
wpPostType: 'product',
customFields: ['_price', '_stock_status', '_featured'],
},
},
migrationBatchSize: 25,
migrationConcurrency: 3,
enableMediaDownload: true,
maxMediaFileSize: 50 * 1024 * 1024,
allowSelfSignedCerts: process.env.NODE_ENV === 'development',
// Restrict to admin users only
access: ({ req }) => req.user?.role === 'admin',
})Field Mapping
Dot-notation paths are supported:
fieldMapping: {
'title.rendered': 'title',
'content.rendered': 'content',
'meta._yoast_wpseo_title': 'seo.title',
'acf.hero_image': 'hero.image',
}Programmatic API
The plugin exports utilities for scripted migrations outside the dashboard.
WordPress Client
import { WordPressClient } from 'payload-wordpress-migrator/dist/utils/wordpress/index.js'
const client = new WordPressClient({
wpSiteUrl: 'https://example.com',
wpUsername: 'admin',
wpPassword: 'xxxx xxxx xxxx xxxx',
})
const contentTypes = await client.discoverContent()
const fields = await client.fetchContentFields('post')HTML to Lexical
import { convertHtmlToLexical } from 'payload-wordpress-migrator/dist/utils/lexical/index.js'
const lexicalContent = convertHtmlToLexical('<p>Hello <strong>world</strong></p>')Content Transformation
import { transformWordPressContent } from 'payload-wordpress-migrator/dist/utils/content/index.js'
const payloadData = await transformWordPressContent(
wpItem,
'post',
jobConfig,
payload,
pluginOptions,
)Note: These are internal paths (
dist/utils/...). The public package export exposes only the plugin function and config types. Internal APIs may change between minor versions.
Migration Best Practices
Recommended migration order:
- Categories and Tags (establishes taxonomy)
- Media (creates media library)
- Pages (static content)
- Posts (blog content with relationships)
- Users (author information)
- Custom Post Types
Performance tips:
- Start with
migrationBatchSize: 10for testing, increase for production runs - Use
migrationConcurrency: 3-5for faster processing - Use dry run mode to preview before committing
- Use resume for interrupted migrations
Troubleshooting
| Problem | Cause | Solution |
| ------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Authentication failed" | Wrong credentials, or REST API disabled | Verify you're using an Application Password (not your login password). Confirm the REST API is accessible at https://your-site.com/wp-json/ |
| Content types not discovered | WordPress permalinks set to "Plain" | In WordPress Admin > Settings > Permalinks, switch to any structure other than "Plain" |
| Custom post types not found | CPT not exposed to REST API | Add 'show_in_rest' => true to your register_post_type() call |
| SSL certificate errors | Self-signed or expired certificate | Set allowSelfSignedCerts: true for development. In production, use a valid certificate |
| Media download fails | WordPress behind CDN or firewall | Ensure media file URLs (wp-content/uploads/...) are accessible from the server running Payload |
| "The following path cannot be queried: migratedFromWordPress" | Target collection not in plugin config | Add the collection to the collections option so the metadata fields get injected |
| Large migration runs out of memory | Batch size too high for available RAM | Reduce migrationBatchSize (try 5) and migrationConcurrency (set to 1) |
| Lexical content looks wrong | HTML conversion edge case | Set disableHtmlConversion: true on the collection mapping as a workaround, and report the specific HTML that fails |
| Duplicate items after resume | migratedFromWordPress.wpPostId field missing | Ensure the collection is listed in the plugin collections config — the field is auto-added |
| ACF fields not migrated | Field mapping not configured | Add ACF field names to customFields in the collection mapping, or configure explicit fieldMapping in the job |
| "MIME type not allowed" or "File too large" | Media file exceeds limits | Expand allowedMediaTypes and increase maxMediaFileSize in plugin config |
Requirements
- Node.js ^18.20.2 || >=20.9.0
- PayloadCMS ^3.29.0
- WordPress 5.6+ with REST API enabled
License
MIT — see LICENSE for details.
Author: Igor Abdulovic at Brightscout
Repository: GitHub
