Appearance
Import & Export
Import and export of content as a WXR file, compatible with WordPress Tools → Import/Export.
kitloom/import-export · admins: kitloom/import-export-filament, kitloom/import-export-nova
Video lessons
Recorded in the Filament admin; the Nova admin works the same way.
Import and export of site content as a WXR file — the format of WordPress Tools → Import/Export. A file exported here opens in WordPress's importer, and a file exported from WordPress is imported here.
The admin screens are in kitloom/import-export-filament (Import and Export in the Import & Export section, after Tools) and kitloom/import-export-nova (the import history with an Export action, in the Import & Export section).
What goes into the file
As in WordPress: authors of the exported posts, terms (parents before children, with termmeta), then each post with its meta, terms and comments (without spam). Menus, forms, sliders, field groups and products are posts too, so they travel the same way. Options, widgets and settings are not in the file — they aren't in WordPress's either.
php
$exporter = app(WxrExporter::class);
$exporter->export($stream); // all content
$exporter->export($stream, new ExportOptions(postType: 'page', status: 'publish'));
$exporter->export($stream, ExportOptions::fromArray([
'content' => 'post', 'category' => 3, 'start_date' => '2024-01', 'end_date' => '2024-06',
]));The file is written to the stream post by post, never held in memory; $exporter->download($options) gives it as a streamed response for an admin's route. ExportChoices lists what the export screen offers: post types, months, statuses and authors that actually occur.
Import
php
$result = app(WxrImporter::class)->import($path, new ImportOptions(
defaultAuthor: $adminId,
downloadAttachments: true,
updateChanged: true,
));
$result->summary(); // "Posts: 12 created, 3 updated, 40 unchanged, 0 skipped. …"
$result->messages; // what failed, per postThe admin screen puts a run into the content_imports history and runs it on the queue (ImportJob); the uploaded file is deleted afterwards.
What the importer does the way WordPress's does:
- a term that already exists (same taxonomy and slug) is reused; a term an item mentions but the file does not declare is created;
- authors are matched by login, then email; users are not created — posts of unknown authors go to
defaultAuthor; - ids from the source site are translated once everything is in:
post_parent,_thumbnail_id,_edit_last, menu items (_menu_item_object_idby type,_menu_item_menu_item_parent), comment replies; - term counters, comment counters and sticky posts are brought up to date.
Where it differs:
- A post is recognised by its guid first, and only then like
post_exists()— same type, title and date. Menu items have no title, and WordPress doubled them on every re-import. The exporter gives a post without a guid one ({app.url}/?p={ID}), so the next import recognises it. - A recognised post is updated when it changed. Its columns, the meta keys the file has and its terms are compared with the file on the spot — nothing is stored for that. If they differ, the columns are rewritten, the file's meta keys replace the stored ones (other keys stay) and the terms become the file's. The author and the modification date are not compared. With
updateChanged: falsea recognised post is skipped, as in WordPress. Comments that are not there yet are added either way. - Each post is its own transaction: a broken one is reported, the rest go in.
- Attachments without downloading are imported as records with the file path of the source site — for when the uploads folder is copied by hand. WordPress skips them instead.
Downloading attachments needs kitloom/media. Files go into the month folder of the attachment's date; their URLs in the imported content are replaced with the new ones. The download goes through the media library's guard: only public hosts are contacted (redirects included) and only the file types the library takes — or the shorter list in kitloom-import-export.attachments.extensions — are accepted: the address comes from an uploaded file. Image sizes are not downloaded — the site uses the original until they exist.
Not translated, as in WordPress: ids inside other meta, e.g. relationship fields of custom fields. Existing terms are not updated.
An imported record gets no revision for arriving: the import holds revisions back while it announces the records it wrote.
Commands
bash
php artisan import-export:export site.xml --post-type=page --status=publish
php artisan import-export:import site.xml --author=1 --attachments
php artisan import-export:import site.xml --skip-existingThe import command runs right away, without the queue and the history.
AI agents
With kitloom/ai installed, an agent whose user has import reads the history of imports (imports_list; import_get — one run with its counters and messages) and starts an import from a WXR file at a public address (import_start): the same choices as the screen — whose the posts of unknown authors become (the agent's user by default), whether attachments are downloaded, whether changed posts are updated — and the same run in the history, on the queue. The file comes through the media library's guard, as the media library's downloads do: public hosts only, redirects included, an .xml name, up to max_upload_size; a file that is not a WordPress export is refused before a run starts. Starting needs kitloom/media; without it an agent gets the history only.
An export is not offered to agents: the file is the whole site's content, and there is no way to hand it over that would not publish it. kitloom-import-export.ai.register false — no import tools.
Configuration
config/kitloom-import-export.php (php artisan vendor:publish --tag=kitloom-import-export-config): the disk and folder for uploaded files, post types never exported, limits for downloaded attachments, filament.register and nova.register to turn the admin screens off, nova.menu for the package's own Nova sidebar section.