We have migrated eleven WordPress installations to JekCMS over the past year. The largest had 3,247 posts, 18 active plugins, and a custom post type structure that did not map cleanly to JekCMS's data model. This is an honest account of where automation works and where it does not.
Standard WordPress data — posts, pages, categories, tags, authors, and featured images — migrates cleanly. The migration script reads directly from the WordPress database using the configured table prefix and writes to the JekCMS schema in a single transaction. Both databases can run on the same server or different ones; the script uses separate PDO connections. Average migration time for 3,000 posts is 4–6 minutes.
Custom Post Types: The Main Complication
Custom post types are the main complication. JekCMS has a single post type with a flexible meta key-value system; WordPress's custom post types correspond to entirely different editorial workflows. For each CPT, you need to decide whether it becomes a JekCMS post with specific meta keys, a custom database table, or a set of JekCMS categories. The decision depends on whether the CPT content needs to appear in the main post feed or exists as a standalone section.
Mapping ACF Fields
ACF fields require explicit mapping in a configuration file. Create a field_map.php in the migration tools directory that maps ACF field names to JekCMS meta keys: 'acf_subtitle' => 'post_subtitle'. Boolean ACF fields become 0/1 integer values in JekCMS post meta. Repeater fields are JSON-encoded as a single meta value — build a custom query helper if your templates need to iterate over them.
Post-Migration URL Audit
After migration, run parallel crawls of both sites using Screaming Frog. Compare status codes and redirect chains. Every WordPress URL that returns 404 in JekCMS needs a redirect entry. JekCMS's config/redirects.php file accepts an array of source → destination pairs and supports regex patterns for bulk redirects like ^/\?p=(\d+)$ => /posts/$1.
The Migration Script Architecture
The migration tool is a standalone PHP script that runs from the command line. It connects to both databases simultaneously using separate PDO connections. The entire migration runs inside a database transaction; if any step fails, everything rolls back cleanly.
php migrate.php \
--wp-host=localhost \
--wp-db=wordpress_site \
--wp-prefix=wp_ \
--jek-host=localhost \
--jek-db=jekcms_site \
--field-map=field_map.php \
--dry-run
The Dry Run Flag
Always run with --dry-run first. This executes every step except the final commit, logging exactly what would be written. On our largest migration (3,247 posts), the dry run took 2 minutes and identified 47 unmappable ACF fields that required manual configuration.
Image Migration: The Slowest Step
Downloading and re-processing WordPress media attachments accounts for 70-80% of the total migration time. The script reads attachments, downloads each file, converts it through the AVIF pipeline, and stores the result. For 3,000 posts with an average of 1.4 images per post, this means processing approximately 4,200 images.
Parallel Image Download
The --parallel=N flag downloads and converts images in parallel batches. Setting --parallel=4 reduced image processing from 45 minutes to 12 minutes on our test server. Higher values have diminishing returns because AVIF conversion is CPU-bound.
Handling WordPress Shortcodes
[gallery]— converted to a JekCMS gallery block by reading attached image IDs[embed]— replaced with the raw URL, which the oEmbed system converts automatically- Plugin shortcodes — logged as warnings; these require manual review and typically affect 5-15% of posts
SEO Data Preservation
If the source site uses Yoast SEO or RankMath, the script reads their custom meta fields and maps them to JekCMS SEO meta keys. The mapping covers title tags, meta descriptions, canonical URLs, robots directives, and Open Graph overrides.
301 Redirect Generation
The script automatically generates a redirects.php file containing every URL pattern difference. Common patterns requiring redirects:
/YYYY/MM/DD/slug/(date permalinks) to/slug(flat URLs)/category/name/to/category/name(trailing slash removal)/?p=123(default permalinks) to the actual slug/wp-content/uploads/image URLs to/uploads/paths
On average, a 3,000-post migration generates 3,200-3,800 redirect entries. Google typically re-indexes the new URLs within 2-4 weeks based on our observations across 11 migrations.
Post-Migration Validation
After the migration script completes, a validation pass compares row counts between the WordPress source and JekCMS target databases. The expected variance is 0-2% because some WordPress revisions and auto-drafts are intentionally excluded. The validator also checks for orphaned relationships: categories with no posts, post meta entries pointing to deleted post IDs, and media records with missing physical files.
Content Integrity Checks
- HTML structure validation: ensures all opened tags are properly closed after shortcode conversion
- Internal link rewriting: WordPress
/?p=and/archives/URLs are rewritten to JekCMS slug-based paths - Embedded media audit: confirms that
<img>and<video>sources point to files that actually exist in the uploads directory - Character encoding: detects and repairs Latin-1 encoded Turkish characters that were stored in a UTF-8 column
- Duplicate slug detection: appends
-2,-3suffixes when two WordPress posts share the same sanitized slug
Rollback Strategy
Every migration creates a full SQL dump of the JekCMS database before writing any data. If validation fails or the client reports issues within the first 48 hours, restoring from this dump takes under 30 seconds for a typical 3,000-post site.
The migration script also logs every INSERT and UPDATE it performs to a migration_log.json file, making it possible to selectively undo specific operations without a full rollback. In practice, selective rollbacks are needed in roughly 8% of migrations, most commonly when a custom post type mapping produces unexpected results.