I moved around 3,000 posts from my own WordPress sites into jekcms. The SQL-to-CSV import flow, plus what actually hurt: encoding, images, slugs.
The importer this post describes is one I wrote myself, and the roughly 3,000 posts that went through it came from my own WordPress sites. Not a client project, not a lab test — my own archive, moved onto jekcms over a stretch of evenings and weekends. You might assume that writing the importer guarantees a smooth migration. It mostly guarantees you discover its rough edges personally, at 1 a.m., staring at a CSV full of broken Turkish characters.
Today my network runs 14 sites on jekcms, the largest with 2,300+ posts and well over 5,500 across the fleet. This post is about the move itself: how the SQL-to-CSV import actually works, step by step, and the four things that genuinely hurt — encoding, image paths, slug collisions, and category mapping.
Why SQL and CSV instead of a WXR file
WordPress's native export produces a WXR file — XML, often enormous, and painful to process on shared hosting: memory limits, timeouts, and the awkward fact that the interesting bits (featured images, term relationships) still have to be resolved through postmeta anyway. jekcms takes a different route. The admin Import screen hands you a ready-made SQL query; you run it against your WordPress database in phpMyAdmin or any MySQL client, export the result as CSV, and upload that CSV to jekcms. Three tabs — Posts, Pages, Comments — each with its own query and its own upload form.
Two properties of this design matter in practice. First, no server-to-server access: your WordPress site can live anywhere, including on a host you're leaving on bad terms, and you never have to expose its database to the internet. Second, the query is a plain SELECT you can read. If you need an extra column, you add it before running. Nothing here is a black box.
What the queries pull — and what they deliberately skip
The posts query selects title, slug, content, excerpt, status and publish date from wp_posts, joins the author's name, slug and email from wp_users, resolves the featured image through _thumbnail_id in wp_postmeta, and collects categories and tags via wp_term_relationships, wp_term_taxonomy and wp_terms. Pages use the same shape minus the taxonomy. Comments come from wp_comments, approved ones only by default.
What it does not select: arbitrary postmeta. No ACF fields, no page-builder data, no Yoast or RankMath titles and descriptions. That's deliberate — WordPress postmeta is a swamp of serialized arrays and plugin prefixes, and importing it blindly creates more mess than it saves. I lost my old SEO plugin descriptions in the move and rewrote the ones that mattered in jekcms's own SEO fields. Tedious, but most of them had been written for the search results of a different decade anyway.
Encoding bit me first
My oldest WordPress tables predated utf8mb4. Some were plain utf8 — the three-byte MySQL kind — and one site still carried latin1 residue from a server move sometime in the early 2010s. The Turkish alphabet is a merciless test for this: ı, İ, ş, ğ and ö turn to mojibake the moment any link in the chain disagrees about the charset.
Check before you export, not after:
-- what charset are the source tables really in?
SHOW TABLE STATUS WHERE Name IN ('wp_posts', 'wp_comments');
-- upgrade an old three-byte utf8 table before exporting
ALTER TABLE wp_posts CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Two more rules I learned the annoying way. Export the CSV from phpMyAdmin with UTF-8 explicitly selected. And never open that CSV in Excel "just to have a look" — Excel will cheerfully re-save it in a Windows codepage and destroy every non-ASCII character in the file. Inspect it in a proper editor instead. One thing no importer can fix: old three-byte utf8 tables silently truncated four-byte emoji at write time, years ago. If an old post shows ???? where an emoji used to be, that data is simply gone.
Image paths are where the days go
With the "Import images" option checked, jekcms fetches the featured image and every image referenced inside post content over HTTP and saves them locally, instead of leaving them pointing at the old domain. This is by far the slowest part of a large migration — you're downloading thousands of files, not copying database rows — and it's also where a decade of sloppy habits comes due.
Mine came due like this: hotlinked images on domains that no longer resolve, a CDN subscription I'd cancelled years earlier, and http:// URLs pointing at my own site through a redirect chain. Every dead source is a missing image after import. The "Set featured image" option — it promotes the first image found in the content for posts that never had one set explicitly — papered over some of the gaps, but only some.
What I'd do again: import in batches, and after each batch, check for missing files while the batch is still small enough to reason about. Once the images are local, jekcms's media pipeline takes over and converts them to AVIF/WebP with a 1920px cap — I've written up how that pipeline works separately.
Expect slug collisions — and count your rows
I was consolidating posts from several WordPress sites, so duplicate slugs across sources were inevitable. jekcms can skip posts whose slug already exists instead of creating duplicates. That's exactly what you want for safe re-runs — an interrupted import just gets started again — but a silent skip is also a silently missing post. My habit: count the rows in the CSV, count the posts after import, and treat the difference as a work list, not a rounding error.
Watch for WordPress's own -2 and -3 suffixes, and for ancient slugs with odd transliteration. Resist the urge to "clean them up" mid-migration: the slug is the URL, and the URL is where your rankings live. Renaming slugs during a move is one of the classic CMS mistakes that kill Google rankings.
Categories are a pre-migration job
Categories and tags are matched by slug: if a category already exists in jekcms, imported posts are filed into it, and WordPress's default "Uncategorized" bucket is skipped rather than imported. Authors get similar treatment — matched by email first, then by slug, and on a match jekcms only fills in empty profile fields instead of overwriting anything you've already set.
Slug-matching is sensible, but it cannot repair ten years of taxonomy sprawl. On one of my sites, "reviews", "review" and "product-reviews" had all accumulated posts over the years — and they arrive as three separate categories, because to a computer they are three. Merge them in WordPress before exporting, or edit the category column in the CSV. Doing it in jekcms afterwards means reassigning posts by hand, and no query knows which of your near-duplicate categories was the "real" one.
Shortcodes get a best-effort cleanup
The optional "Format content" step handles the common WordPress residue: [caption] becomes proper <figure>/<figcaption> markup, [gallery] is stripped, [embed] is unwrapped to a bare URL, and leftover Gutenberg block comments are removed. Page-builder markup — Elementor, Divi and their relatives — is not rendered and needs a manual pass through the affected posts. My sites had been plain-editor from the start, so I largely dodged this one; the [caption] converter alone saved me a pile of manual edits.
Prefer not to touch SQL at all?
There's an official WordPress plugin for that: jekcms-migrator, currently in the wordpress.org plugin review queue. It runs inside wp-admin and pushes your content across without you writing a single query. jekcms also ships a migration wizard on the receiving end. For a small, tidy site those are the comfortable path; for a large or messy database I still prefer SQL-to-CSV, because you can see and edit every row before it enters the new system. The documentation covers all three routes, and if you're still weighing the move itself, I've written separately about why I left WordPress.
Before you call it done
- Back up the jekcms database first. The import is additive, but you want a clean restore point before 3,000 rows land.
- Test with a trimmed CSV. The import runs off a file you control — cut it down to a few dozen rows and dry-run the whole flow, images included.
- Plan redirects by hand. Date-based permalinks,
/?p=123URLs and old category paths are not redirected automatically. Decide which ones matter and add them in the redirect manager before DNS moves. - Resubmit the sitemap. Push the new
/sitemap.xmlin Search Console instead of waiting for a natural recrawl. - Spot-check comments. If you imported them, some spam that WordPress missed came along for the ride.
Three thousand posts later, my honest summary: the migration took longer than the feature list implies and less time than I feared. What it mostly demanded was attention — to charsets, to file paths, to slugs. The importer moves the data. The judgment is still your job.