Content migration
How to move your posts, pages, media, users, and taxonomy terms from WordPress to Contensio.
There is no one-click WordPress importer yet — it is on the roadmap. In the meantime this page describes the manual process and the tools available.
What you're migrating
A typical WordPress site has:
- Posts and pages — content with titles, body, excerpt, publish date, author, featured image
- Custom post types — if you used CPT UI or registered types in code
- Taxonomy terms — categories, tags, custom taxonomies
- Media files — images and attachments in
wp-content/uploads/ - Users — accounts with names, emails, roles
- Comments — approved comments attached to posts
Step 1 — Export from WordPress
Use Tools → Export in WordPress to download a WXR (WordPress eXtended RSS) XML file. Export everything in one file or split by content type — both work.
Alternatively, if you have database access, a direct SQL export gives you more control.
Step 2 — Set up content types in Contensio
Before importing, create content types that match your WordPress post types:
- WordPress Posts → Contensio content type named
post(or keep the default) - WordPress Pages → Contensio content type named
page - Each custom post type → a matching Contensio content type
Go to Content → Content Types and create the types. Add custom fields to match any ACF or meta fields you had in WordPress.
Step 3 — Create taxonomies
Recreate your WordPress taxonomies under Content → Taxonomies:
- Categories → a hierarchical taxonomy named
category - Tags → a flat taxonomy named
tag - Custom taxonomies → matching custom taxonomies
Assign each taxonomy to the appropriate content type.
Step 4 — Migrate media
Copy your WordPress media files from wp-content/uploads/ to Contensio's storage disk. The default disk is public, stored in storage/app/public/.
If you use S3 or another disk, upload directly there.
After copying, register the files in Contensio's media library. You can do this via a one-time Artisan command or script:
use Contensio\Models\Media;
// For each file you've copied:
Media::create([
'disk' => 'public',
'file_path' => 'migrated/image.jpg',
'file_name' => 'image.jpg',
'mime_type' => 'image/jpeg',
'size' => filesize(storage_path('app/public/migrated/image.jpg')),
]);
Step 5 — Import content
Parse the WXR XML file and create Contensio content records. A minimal migration script:
use Contensio\Models\Content;
use Contensio\Models\ContentTranslation;
use Contensio\Models\ContentType;
use Contensio\Models\Language;
$xml = simplexml_load_file('export.xml');
$type = ContentType::where('name', 'post')->first();
$lang = Language::where('code', 'en')->first();
foreach ($xml->channel->item as $item) {
$postType = (string) $item->children('wp', true)->post_type;
if ($postType !== 'post') continue;
$status = (string) $item->children('wp', true)->status === 'publish'
? 'published'
: 'draft';
$content = Content::create([
'content_type_id' => $type->id,
'status' => $status,
'published_at' => $status === 'published'
? \Carbon\Carbon::parse((string) $item->pubDate)
: null,
]);
ContentTranslation::create([
'content_id' => $content->id,
'language_id' => $lang->id,
'title' => (string) $item->title,
'slug' => (string) $item->children('wp', true)->post_name,
'excerpt' => (string) $item->children('excerpt', true)->encoded,
// body goes into blocks — see below
]);
}
Converting post content to blocks
WordPress post content is HTML. Contensio stores content as blocks (JSON). The simplest migration approach is to create a single html block containing the full post HTML — it renders as-is:
$body = (string) $item->children('content', true)->encoded;
// Store as a single raw HTML block
$blocks = [
['type' => 'html', 'data' => ['html' => $body]]
];
$content->update(['blocks' => $blocks]);
This preserves all formatting exactly. You can refine to proper paragraph/heading/image blocks later.
Step 6 — Migrate users
use Contensio\Models\User;
// From WP XML authors section
foreach ($xml->channel->children('wp', true)->author as $author) {
User::create([
'name' => (string) $author->author_display_name,
'email' => (string) $author->author_email,
'password' => bcrypt(\Str::random(32)), // force password reset
]);
}
Send a password reset email to each migrated user so they can set their own password.
Step 7 — Migrate taxonomy terms and assignments
Create terms and attach them to content:
use Contensio\Models\Term;
use Contensio\Models\TermTranslation;
use Contensio\Models\Taxonomy;
$taxonomy = Taxonomy::where('slug', 'category')->first();
foreach ($xml->channel->item as $item) {
foreach ($item->category as $cat) {
$slug = (string) $cat->attributes()->nicename;
$name = (string) $cat;
$term = Term::firstOrCreate(['taxonomy_id' => $taxonomy->id, 'slug' => $slug]);
TermTranslation::firstOrCreate([
'term_id' => $term->id,
'language_id' => $lang->id,
], ['name' => $name, 'slug' => $slug]);
// Attach to the content item
$content->terms()->syncWithoutDetaching([$term->id]);
}
}
Step 8 — Migrate comments
use Contensio\Models\Comment;
foreach ($item->children('wp', true)->comment as $comment) {
if ((string) $comment->comment_approved !== '1') continue;
Comment::create([
'content_id' => $content->id,
'author_name' => (string) $comment->comment_author,
'author_email' => (string) $comment->comment_author_email,
'body' => (string) $comment->comment_content,
'status' => 'approved',
'created_at' => \Carbon\Carbon::parse((string) $comment->comment_date),
]);
}
After migration
- Set up redirects — old WordPress URLs will not match by default. See SEO & redirects.
- Reassign featured images — if you migrated media, link featured images to content via
$content->update(['featured_image_id' => $media->id]). - Test everything — check a sample of posts, taxonomy archives, author pages, and the homepage.
One-click importer
A WordPress importer plugin for Contensio is on the roadmap. It will handle WXR parsing, block conversion, media download, and redirect generation automatically. Watch the changelog for updates.