Importing from WordPress

Lamb ships a CLI script that reads a WordPress WXR export and feeds each published post and page through Lamb’s existing post-creation pipeline. The importer is fully offline — no credentials, no API access — and re-running it is safe.

What you get

The first-pass scope is intentionally small:

  • Published Posts and Pages are imported. Drafts, private posts, custom post types, comments, menus and theme settings are skipped.
  • HTML post bodies are sanitised (<script>, <style>, <iframe> and on* event attributes are stripped) and converted to Markdown.
  • Categories and tags become inline #hashtags at the end of the body — Lamb’s tag index picks them up automatically.
  • Every image referenced in the body is downloaded into src/assets/YYYY/MM/ (using the post’s own creation date), re-encoded to WebP and scaled down to the upload pipeline’s max edge (details), and the body links are rewritten to point at the local copies. Coverage includes <img src>, data-full-url (gallery blocks ship the original full-resolution URL there and a downscaled copy in src — the importer prefers the full one), data-src (lazy-loaded), and <a href> that points at an image file (so “view full size” links survive the migration). Images on a different host than <wp:base_blog_url> are pulled in too — a typical WP migration ships images from a CDN or a previous domain, and the downloader’s content-type and extension checks keep arbitrary URLs safe. Downloads are capped at 20 MB and deduped by URL hash within a month, so a logo used across many posts is fetched once. Failed downloads, data: URIs and unresolvable relative URLs are left as-is.
  • Imported posts are silent: the importer calls the low-level save pipeline directly, so no outbound webmentions or WebSub hub pings are emitted. The content already exists somewhere else.

Exporting from WordPress

In your WordPress admin, go to Tools → Export and download All content. You’ll get a .xml file (the WXR format).

Running the importer

From the project root:

# Preview what would be imported without writing anything
php import-wordpress.php /path/to/wordpress.WordPress.xml --dry-run

# Run it for real
php import-wordpress.php /path/to/wordpress.WordPress.xml

The script prints one line per item (imported: or would import:) plus a final summary with the totals (created, existed, skipped). An item that was already imported in a previous run is recognised by its feeditem_uuid (md5 of 'wordpress-' + guid) and left alone.

After the import

The importer writes directly to the same database your site uses, so the posts are visible immediately. There is no separate review queue.

A few things worth checking manually:

  • Slugs and redirects. Page-like posts keep their original WordPress permalink leaf (<wp:post_name>), written into front matter as slug: so the URL is identical to the WP one (relative to your new domain). Titleless WordPress status posts whose old URL was /status/<id>/ are imported as Lamb status posts and get an automatic 301 redirect from the old WordPress path to the new local /status/<local-id> URL. Where a WP slug collides with an existing Lamb post or a reserved route, Lamb appends the post id (matching the standard create flow), so that particular post’s URL will differ — set up a 301 if it matters.
  • Embedded shortcodes. WordPress shortcodes ([caption …], gallery shortcodes, etc.) are not expanded — they appear verbatim in the Markdown. Edit any you care about after the import.
  • Media. Run a quick git status on src/assets/ to see what was downloaded.
  • Cross-posting — outbound syndication, the opposite direction.
  • Feeds — how Lamb publishes content for other readers to ingest.
  • Media — how uploaded images are stored and converted.

This site uses Just the Docs, a documentation theme for Jekyll.