Contensio logo

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.