WordPress import reference
This page lists everything the WordPress importer does: the hatamoto import wordpress command and its flags, the two site modes, what is converted, how re-runs work, the URL audit, the admin wizard's details and the limits. To run an import from the admin, follow Import from WordPress.
Overview
There are two front doors to the importer: the admin wizard at /admin/settings/import/wordpress (owners only) and the hatamoto import wordpress command. Both run the same engine, so a scan, an import and a re-sync behave the same way. Only one import runs at a time per site; the wizard and the command share the lock, and a second start fails with a message that one is already running.
The importer reads the source in one of two ways:
Through the WordPress REST API. An application password (created in WordPress under Users, Profile, Application Passwords) lets it also read drafts, private posts, menus and settings.
From a WXR export file (in WordPress: Tools, Export, All content). Media is fetched from the addresses inside the file.
The command line
The command runs on the server and needs DATABASE_URL, like every operator command, and MEDIA_UPLOAD_DIR (default ./uploads) for downloaded media. It applies pending database migrations first.
Put the application password in the environment, not on the command line, because arguments are visible to every user in the process list.
export WP_APP_PASSWORD="abcd efgh ijkl mnop"
# 1. Look first. Nothing is written except a run record.
hatamoto import wordpress --url https://old.example.com --user admin --scan-only
# 2. Import, then check every old URL against the new site.
hatamoto import wordpress --url https://old.example.com --user admin \
--audit-url https://new.example.com
# From a file: media is fetched from the URLs inside it.
hatamoto import wordpress --wxr old-site.xml
# Continue after an interruption, or re-sync later.
hatamoto import wordpress --url https://old.example.com --user admin --resume <run-id>Exactly one of --url and --wxr is required, unless --resume names an earlier run.
Flag | Type | Default | Meaning |
|---|---|---|---|
| URL | none | REST source: the WordPress site address. |
| text | none | WordPress user for the application password. |
| text |
| The application password. It shows in the process list, so prefer the |
| file | none | WXR source file. Cannot be combined with |
| switch | off | Print the scan report, store a run record and stop. |
| run id | none | Run again under an earlier run id; finished work is not repeated. |
| URL | none | A running Hatamoto site to check every old URL against. Without it the audit is skipped. The audit target may be a private or loopback address. |
| switch | off | Allow a source on a private or loopback address. Off by default, because the importer uses the same guard against requests to internal addresses as the plugin |
| switch | off | Keep this site's URL patterns instead of copying the WordPress permalink structure. Old URLs then redirect. |
| switch | off | On a re-run, leave alone items you edited in Hatamoto since the last import, and list them as kept. |
| switch | off | Print the final report (or the scan report with |
| number |
| Parallel media downloads. |
| number |
| Refuse sources with more posts, pages and custom post items together than this. |
The command exits non-zero when anything failed, when a wp-content URL is left in imported content, when an old URL could not be redirected because a live page answers there, or when the audit found a URL that does not resolve. That makes it safe to use in a script. A failed run prints the --resume command to continue it.
Empty site or site with content
The importer inspects the target before writing, and the scan report states which mode applies.
Empty site. There is no published content except the first-boot home page and the setup wizard's starter content. The WordPress permalink structure becomes the
postandpageURL patterns, and the site title, tagline, time zone, language and home page are set. Each pattern is validated like a schema save, and an invalid one is skipped with a warning.Site with content. The importer never changes a URL pattern, a site setting or the home page. It reports where WordPress differs, such as "site title is X here, Y in WordPress". Imported items take the patterns that exist, and each old URL that moved gets a 301. The wizard does not offer the URL structure choice in this mode.
In both modes a live page always wins. If an imported item's slug is taken (a WordPress about and a Hatamoto about in the same language), the imported item becomes about-2. A redirect from the old URL to the renamed item is written only when no live page answers at the old URL. Otherwise the URL is listed as unresolved in the run report and the audit, and the command exits non-zero. A redirect you created yourself is never overwritten. Redirects from an earlier run that are no longer wanted are removed on the next run. The rules every redirect follows are in Redirect rules.
The scan report
A scan lists counts (posts, pages, custom post types, drafts, private items, media, terms, authors, menus and URLs in the old sitemap) and the permalink structure, including whether it can become a Hatamoto pattern. It also names:
The SEO plugin (Yoast, Rank Math, SEOPress, All in One SEO).
Page builders (Elementor, Divi, WPBakery, Avada, Beaver Builder, Bricks).
Form plugins (Contact Form 7, Gravity Forms, WPForms, Ninja Forms, Formidable).
Multilingual plugins (WPML, Polylang). Every language is imported and tagged with a locale, but only the site language is public, because one install serves one language. Over REST only Polylang is read once per language; WPML over REST is one unfiltered pass and items are tagged only when the response carries their language. A plain WPML export usually carries no language, so its items take the site language. A script-only code such as
zh-hansis the site language when the site shares its primary language and script (zh-CN).Other plugins, listed as unsupported, and every shortcode left in a body.
The report ends with the expected fidelity: the share of items that import without loss, with each medium or low item and the reason. A page builder makes an item low fidelity; removed shortcodes, forms or embeds, and private or password-protected status, make it medium.
What is imported
WordPress | Hatamoto |
|---|---|
Title, tagline, time zone, language | Site settings. The time zone matters because |
Permalink structure such as | The |
Pages (nested) |
|
Posts |
|
Custom post types | A schema per type, created when missing, with its URL structure and archive. |
Authors | People ( |
Media library | Media, downloaded through the internal-address guard and deduplicated by content hash. Every size variant maps to the original. |
Body | The |
SEO title, description, canonical, noindex, social image |
|
Menus | Menus with nesting. Pages become entity links. |
Private, protected and draft content | Drafts, listed in the report. |
Body conversion strips Gutenberg comments, keeps YouTube and Vimeo embeds, removes form markup and shortcodes (and reports them rather than guessing), turns [caption] into a figure and drops any other iframe. No wp-content URL remains in imported content: images and files are rewritten to their new media URL, and the run checks the database at the end. Internal links such as /2024/05/hello/ or ?p=12 become entity links, so they follow later renames.
In the wizard, switching Download images and files off copies no file, and images and attachments are left out of the text.
Re-runs and re-sync
Running the import again changes nothing that is already up to date. Each run is a row of hatamoto_import_runs, and the id of everything imported is kept in hatamoto_import_map, scoped by site. A re-run updates the same rows. Entities are updated in place, media with the same source id or bytes is reused, and a person you edited is left alone.
Content edited in Hatamoto after the import is overwritten by the next sync, so re-sync only before cutover. The report lists every overwritten item. With --keep-edited (in the wizard: On a re-import, leave pages alone that I edited here since the last import) those items are left alone and listed as kept.
The URL audit
After the import, every URL of the old sitemap is requested on the new site without following redirects. A URL passes when it answers 200, or answers one 301 or 308 to a URL on the same site that answers 200. A 302, a chain, a 404, a redirect to another host or a redirect to nowhere fails and is listed. Media files (wp-content paths) are skipped. For a WXR file, the links of the published items are used.
The admin wizard
The wizard lives at /admin/settings/import/wordpress. It has five plain pages that work without JavaScript:
Source: the site address with a username and application password, or an uploaded WXR file up to 256 MB.
Scan report: the same report as above, run as a background job.
Options: which post types, drafts and menus, media on or off, whether to keep this site's URL patterns, and whether to keep items edited since the last import.
Run: a background job with live progress. Stop the import cancels it and keeps what was imported, and a run interrupted by a restart offers Resume.
Report: errors, warnings, redirects, the audit and the pages to review, each linking to its edit screen.
The application password is sealed (encrypted) in the database so a run can resume after a restart, and it is deleted when the run completes. Unfinished runs keep it, and their uploaded file, for 7 days at most. The wizard cannot lift the private-host guard, so a source on a private address must be imported from the command line with --allow-private-hosts.
Limits and errors
Limit or message | Meaning |
|---|---|
WXR file over 512 MB (command) or 256 MB (wizard) | Refused. Export fewer content types, or import from the site's address. |
More than 50 000 items, or | Refused before anything is written. |
WXR nested deeper than 64 levels or with more than 20 million elements | Refused as malformed. |
| Pass one source only. |
| Give the file again when resuming a file import. |
| The report above lists the failures, leftover |
What is not imported or reproduced:
One language per install: items in other languages are stored with their locale but are not public, and get no redirects.
Page-builder layouts are not reproduced. The text is kept and the page is listed for review.
Forms are not converted; recreate them with Hatamoto forms.
WooCommerce orders and coupons and comments are not imported. Products and event posts are ordinary custom types: they come across as plain pages (text and images, no prices, cart, stock or event dates logic).
Sites with plain permalinks (
?p=12) import, but their old URLs are audited from the sitemap only.
See Redirect rules for what happens to old URLs at cutover, including the legacy_wp_redirects setting.