How to Move WordPress to Phoenix LiveView with Mishka CMS

A step-by-step guide to the Mishka CMS Migration extension: connect WordPress over REST or an export file, run a dry run, settle the decisions, import, and keep every old URL with 301 redirects.

How to Move WordPress to Phoenix LiveView with Mishka CMS

You can move a WordPress site into Mishka CMS from the admin, without writing a script. The Mishka CMS Migration extension reads your WordPress site, shows what it would create, asks you about every conflict, and imports only after you press the button. This post follows the real screens, step by step.

Time left until the offer ends
119
D
08
H
25
M
59
S
Claim your seat · $250One payment, no subscription. Questions first? Contact us.

Mishka CMS stays free, and so does this extension. A founding seat pays for the time we spend on both, and it gives you lifetime access, priority support and every paid extension we release.

Here is what you get at the end: your posts, categories, tags, authors and comments in Mishka Blog, your pages, menus and media, and a 301 redirect for every old WordPress address that moved. The site runs on Elixir, Phoenix LiveView and Ash.

Read it top to bottom once, or jump to the step you are on. Each step ends with what you should see, so you know it worked.

Newsletter

Mishka CMS releases. Once a month.

New Mishka CMS releases, Elixir and Phoenix patterns, and case studies from the team. No spam, unsubscribe anytime.

✓ once a month✓ no spam

The whole move on one page

A migration has seven steps, and the first five write nothing to your site. You install the extension, connect WordPress, choose what to bring, run a dry run, settle the decisions and only then import.

Diagram of the seven steps to move WordPress into Mishka CMS: install, connect, choose, dry run, decide, import and check, with the dry run steps marked as writing nothing

The whole move. Only the import writes to your site.

Animation of the Mishka CMS Migration admin: New import step 1 Where from, step 2 What to bring, the Review step, the Dry run ready report, the Decisions panel and the Redirects planned

The flow in six frames, from New import to the decisions. Frames are real screens of the admin.

2sources: REST or export file
4steps in the New import window
0records written by a dry run
301for every address that moved

Before you start

You need a Mishka CMS site, an admin who may run imports, and one way to read WordPress. Check this list first and the rest goes fast.

On the CMS sideA site with Mishka BlogPosts, categories, tags, authors and comments go into Mishka Blog. Without it they wait.
On the admin sideOne permissionadmin:dashboard:migration, plus the permission of each kind the import writes.
On the WordPress sideA login or a fileAn administrator with an application password, or the export file with All content.

Step 1. Install the extension

Mishka CMS Migration is an Elixir package, so you add it to your host, register it and run one migration. A project cloned from the Mishka CMS repository may already list it. The guide Install the migration extension has the full version.

1. Add the package to mix.exs:

elixir
# mix.exs
defp deps do
  [
    # ...your other deps
    {:mishka_cms_migration, "~> 0.0.3"}
  ]
end

2. Register it. Add the Ash domain to your domains and the web module to the core's extensions, and give Oban a migration queue with one worker:

elixir
# config/config.exs (keep the entries you already have)
config :mishka_cms,
  ash_domains: [MishkaCmsMigration, MishkaBlog, MishkaCmsCore.Runtime, MishkaCmsCore.Accounts]

config :mishka_cms_core,
  extensions: [MishkaCmsCoreWeb, MishkaBlogWeb, MishkaCmsMigrationWeb]

# The dry run and the import are jobs on this queue, one at a time
config :mishka_cms, Oban,
  queues: [migration: 1]

3. Create the tables for the runs, items and log:

mix deps.get
mix ash.codegen add_cms_migration
mix ash.migrate

4. Set it up on a site from the CMS installer, as Install an extension shows. Then give the admin who will import the admin:dashboard:migration permission.

Step 2. Connect WordPress

Pick the REST source for a live site, or the export file for a site with translations. Both end in the same dry run. Press New import on the card you want.

Source 1WordPress, over its REST APIYou give the site address, a WordPress user and an application password. The run reads only what that user may see.
Source 2WordPress export fileYou drop the .xml file from Tools, Export with All content chosen (up to 100 MB). It carries every translation, drafts, private posts and comments.
The first step of the New import window in Mishka CMS: Site, the WordPress Address, User and Application password fields

Step 1 of the window: where the content comes from.

For the REST source, make the application password in WordPress under Users, Profile, Application Passwords. Then fill in the three fields. The user is the WordPress user the password belongs to, such as admin.

When you press Next, the window tries the connection before it moves on. It reads /wp-json/ first and then signs in at /wp-json/wp/v2/users/me?context=edit, so the reason it gives is the first thing that is wrong. You can run the same check yourself:

# Does WordPress answer, and does the application password sign in?
curl -s -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
  "https://example.com/wp-json/wp/v2/users/me?context=edit"

Step 3. Choose what to bring

Step 2 of the window is a list of switches, and every one is on except Comments. Switch off what you do not want. Something you switch off is listed as Left out by you, and you can bring it back later on the Items page.

The second step of the New import window, What to bring, with a switch for each kind of content

Step 2: switch off what you do not want to move.

SwitchDefaultMoves
PostsOnPosts, into Mishka Blog
PagesOnPages, as drafts unless you choose otherwise
Pictures and filesOnFiles, downloaded into the Media library
Categories and tagsOnCategories and tags, into Mishka Blog
Authors and usersOnAuthors, as Blog bloggers tied to your users
MenusOnThe site's menus
CommentsOffComments, into Mishka Blog

Step 3 asks about languages. Leave it empty to keep the languages WordPress gives. Pick one to put everything that has no language of your site's into it. The list comes from Settings, Languages, so add a language there first. On a Polylang or WPML site, also set the site's language addresses before you import, as Set a site's languages explains.

Step 4. Choose the addresses and start the dry run

The Review step says in words what the run will do, and its Advanced options hold the choice that matters most for SEO: Post addresses. The default keeps your WordPress addresses, so a post stays at /2025/12/hello/.

The Review step with Advanced options open on the Post addresses, Posts list page and Blog pages choices

Step 4, Review: Advanced options hold the Post addresses choice.

DefaultKeep the WordPress addressesPosts are served at the address WordPress gave them. What moved gets a 301.
Option/blog/<slug> with 301sPosts move under /blog and every old address is redirected.
Option/blog/<slug>, no redirectsPosts move under /blog and old addresses are not redirected.

The other choices stay at their defaults for a first run. Run options lists every one. Press Start the dry run. The run connects first, then saves itself and starts the dry run, which writes nothing to your site.

Step 5. Read the report

A dry run reports what an import would create, update and skip. Open the run's details with the arrow at the start of its row. The head shows how many items it found and a bar of what came of them.

A run's details after a dry run: Dry run ready, 360 items found, 276 created, 77 skipped and 7 that need your decision, with a card for each kind

The report of a dry run. Nothing is written yet.

Our test export gave 360 items: 276 would be created, 77 skipped and 7 need your decision. Most of the 360 are redirects, one for each old address the run found.

Chart of one dry run: 360 items found, 276 would be created, 77 skipped and 7 need a decision, and the count of each kind from 266 redirects to 2 settings

What one dry run of a test export found.

Read three things before you go on:

  • The cards. There is one per kind, with a bar and the reason most skipped items give.
  • The warnings. A banner such as Imported links may not match the site's language addresses says what to fix first.
  • About the source. It names the post addresses the run found, and how many posts sit outside that shape.

No decisions waiting and no failure you care about? Press Import. Otherwise go to the next step. The guide Read a dry run explains every part of the report.

Step 6. Settle what needs your decision

The importer never picks for you when two answers are both reasonable. It marks the item Needs your decision and waits. Press the Items icon on the run's row, and the Decisions panel sits above the table.

The Decisions panel of the Items page in Simple mode: 7 items need your decision, with a card for each conflict and its choices

The Decisions panel: one card per conflict, the recommended choice first.

01

Read a card. Each names one conflict, says how many items wait, and lists the choices. The recommended one comes first, with a star.

02

Look first if you want. Press Show them to narrow the table to the items that wait.

03

Press a choice. The page asks to answer N items with it. Confirm, and the whole group is decided at once.

04

Run again, then Import. A decision only records your answer. The next dry run follows it, and the import writes it.

These are the conflicts you will meet most often:

The item saysChoices
The site already has one at this addressConnect to the existing record, or import as new with another slug
Choose where this menu goesHeader links, Footer links, A component, or Leave out
Contains scripts or inline event handlersRemove them, or keep them if you trust this content
A page of the site is at this post's addressImport with another slug, or Leave out
Same file as anotherKeep both, or use the original
The source does not say its languageImport it in the site's main language, or wait for the export file

A decision belongs to the item, not to the run, so it holds for every later dry run and import. Advanced mode lets you decide by rule: one choice per kind and conflict, a preview of how many items each rule decides, and one Apply. For single items, see Work on single items.

Step 7. Import, then open your old addresses

Press Run again, then Import and confirm. The import writes each item through the CMS's own actions, as the admin who pressed the button, parents before their children. The CMS's permissions and validations decide what it accepts. When it ends, the status reads Imported.

Diagram of five old WordPress addresses and what the site does with each: one served at the same address, four redirected with a 301

Old addresses become redirect items. The dry run lists them first.

Now check the redirects. The run's details say how many rules it wrote under Redirects written, and the Redirects page in the admin lists each one. Then open an old address, as a visitor would. This is a real answer from a local test site that was filled by an earlier run:

curl -sI 'http://localhost:4030/?p=526' | grep -i "^HTTP\|^location"
HTTP/1.1 301 Moved Permanently
location: /blog/from-rails-to-phoenix-a-year-of-honest-notes-2
01

Open three old posts at their WordPress address. Each answers there, or redirects with a 301.

02

Open a short link such as /?p=526, a feed and a category. See Redirect an old address to manage the rules.

03

Publish your pages. They come in as drafts unless you chose Live at their old addresses.

04

Tell your users. If the import created users, their WordPress passwords cannot come along. They sign in with Forgot password or a social sign-in.

Running it again is safe

Run again updates what WordPress changed and keeps what you changed in the CMS. The extension remembers every item by site, host, kind and source id, and that memory is what makes a second run safe.

It updatesWhat the source changedA post you edited in WordPress since the last import.
It keepsWhat you changed hereA record you edited, deleted or archived stays as you left it.
It neverOverwrites a clashThe same address as another record fails the item or waits for you.

One site can also take content from several sources, such as two WordPress blogs. Each run keeps its own items, and the same id on two hosts is two items. The page How a migration works explains the memory, and what happens to the application password: it is sealed with the CMS's secret key and kept until you cancel the run.

What to expect

Most sites move cleanly, and a few things need a human. These are the ones we know about, so none of them surprises you:

  • A post that is pending, private, password protected or scheduled at the source comes in as a draft, and its item says why.
  • A file on another host, such as a CDN, is skipped and stays where it is.
  • A page built with Elementor widgets that Mishka CMS has no match for waits for your decision.
  • Only WordPress is supported today. Joomla, Drupal and Ghost are planned.

Where to go next

You now have your content and your old addresses in Mishka CMS. Here is what to read next:

If you want us to run the move for you, hire us. You can also back the project through the sponsor page, or take the founding seat below. The package is on Hex and the code is on GitHub.

Time left until the offer ends
119
D
08
H
25
M
59
S
Claim your seat · $250One payment, no subscription. Questions first? Contact us.
Newsletter

Mishka CMS releases. Once a month.

New Mishka CMS releases, Elixir and Phoenix patterns, and case studies from the team. No spam, unsubscribe anytime.

✓ once a month✓ no spam

Questions people ask about moving WordPress to Mishka CMS

Install the mishka_cms_migration extension, press New import under Migration, connect WordPress over its REST API or with an export file, run the dry run, settle the items that need your decision, then press Import.

Either works. The REST source needs the site address, a user and an application password. The export file comes from Tools, Export with All content chosen, and it carries translations, drafts, private posts and comments, so use it for a site with translations.

No. A dry run reads the whole source and reports what an import would create, update and skip. It writes nothing, so you can repeat it as often as you like.

Yes, by default. The run serves posts, categories, tags and pages at their WordPress addresses and redirects every address that moved with a 301. The dry run lists these redirects before the import writes them.

The import never picks for you when two answers are reasonable, such as a category the site already has or a menu with no fixed place. It marks the item and waits. You answer one card for a whole group, and the answer holds for every later run.

Yes. Running again updates what WordPress changed and keeps what you changed in the CMS: a record you edited, deleted or archived stays as you left it. A clash is never overwritten.

Thank you

Mishka CMS Migration stands on Elixir, Phoenix LiveView and Ash Framework, and on WordPress, whose open REST API and export format make a move like this possible. Thank you to everyone who builds them, and to you for reading this far.

Contributors Mishka Group Core
Shahryar Tavakkoli
LEAD
Shahryar Tavakkoli @shahryar-tavakkoli
Elixir · Phoenix · LiveView

Shahryar Tavakkoli writes about Elixir and Phoenix LiveView: Mishka Chelekom releases, GuardedStruct validation, static sites, service workers and SEO.

Comments

Sign in to comment

No comments yet.