Skip to content

Importing

Theodor Dimitriou edited this page Sep 24, 2026 · 2 revisions

How to bring existing content into AstroBaaS, and how to get your own content out again. This page covers the WordPress importer (in the admin and on the command line), the WooCommerce importer, the Markdown importer, and CSV/JSON export and import of collections. It is for operators moving a site onto AstroBaaS.

Source Tool Where
WordPress posts and pages (WXR export) npm run import:wp, or Admin → Import a site CLI or admin
WooCommerce shop (products, orders, customers, blog) npm run import:woo CLI only
A folder of Markdown files npm run import:md CLI only, JSON database only
Your own posts, pages and collections (CSV or JSON) Admin → Import a site → Your own content Admin or API

All four share one rule: an import cannot be undone. Take a backup first (see Backups and storage), and rehearse before you commit. The WordPress, WooCommerce and CSV/JSON importers all rehearse by default and write nothing until you confirm.

WordPress (WXR)

In WordPress, go to Tools → Export → All content and download the .xml file.

From the admin

Admin → Import a site (/admin/import, admin only). Upload the export and choose options:

Option Default Meaning
Pages as well as posts on Import WordPress pages as AstroBaaS pages
Download images and files from the old site off Fetch the media library. The old site must still be online. At most 100 files per run from the admin
Include content WordPress had in the bin off Also import trashed items
Custom post types empty Comma-separated type names (such as portfolio, recipe) to import as posts

See what would happen runs a dry run and shows a summary: counts, the authors found, what will not be imported and why, and any failures. The button that actually writes appears only after you have seen that summary.

Admin limits:

Limit Value
Export file size 24 MB (larger files must use the CLI)
Media files per run 100. The rest are reported as skipped, with the cap named
Import requests 10 per hour

The endpoint behind this screen is POST /api/import/wordpress (multipart). It needs the import_content capability, which only admins have. It does a dry run unless you send dry_run=false. Source: src/pages/api/import/wordpress.ts.

From the command line

npm run import:wp -- ./export.xml                  # rehearse, write nothing
npm run import:wp -- ./export.xml --apply          # perform it
npm run import:wp -- ./export.xml --apply --media  # ...and fetch the files
Flag Meaning
--apply Perform the import. Without it, nothing is written
--media Fetch the media library from the old site
--trash Include content WordPress had in the bin
--no-pages Import posts only
--types a,b Also import these custom post types, as posts
--author <email> Put imported content under this account. The default is the oldest administrator

Use the CLI for big sites. It has no upload limit or HTTP timeout, accepts exports up to 200 MB, and fetches up to 2000 media files per run. The CLI and the admin use the same code in src/lib/import/, so they make the same decisions. Both write through the normal storage layer, so they work on every database driver. Source: scripts/import-wp.mjs.

What is imported

WordPress AstroBaaS
Post Post (/blog/<slug>)
Page Page (/<slug>)
Listed custom post type Post
Title, content, excerpt, slug Same. Content is HTML-sanitised on the way in
Publish date (GMT preferred) Publish date
First category The post's category, created if missing. The display name is kept
Tags Tags
Attachments (with media on) Media library items, through the same upload checks as a normal upload
Old permalink that moved A 301 redirect to the new address

WordPress statuses are converted like this:

WordPress AstroBaaS
publish published
pending review
future scheduled (keeps its date, and the scheduler publishes it)
private draft, so private content is not published by the migration
anything else draft

Redirects are written after the content, and only when the URL actually changed. WordPress's default /YYYY/MM/slug/ becomes /blog/slug. A page at /slug/ usually stays put and gets no rule. If a slug collided with existing content and was renamed (for example -2), the redirect points at the renamed address. A path that already has a redirect is left alone. New rules take effect immediately, without a restart.

What is not imported

  • User accounts. Authors found in the export are listed, but no accounts are created. Invite those people yourself, then reassign their posts. All imported content belongs to one account (see --author).
  • Comments, custom fields (postmeta) and menus.
  • Products, orders and customers. WooCommerce keeps these outside the WXR export, so use the WooCommerce importer below.
  • Featured images. The importer reads which attachment was the featured image, but does not attach it to the imported post.
  • Image URLs inside post content are not rewritten to the imported media copies. They still point at the old site.
  • Trashed items (unless you asked for them), auto-drafts WordPress never used, and custom post types you did not list. Each is reported as skipped, with a reason.
  • Media URLs pointing at private or loopback hosts, and files larger than the media library's 10 MB limit. These are refused and reported.

Running it again

Re-running an import is safe. Each imported record is stamped with its WordPress id (wp_id). Anything already imported, including media, is skipped with "already imported by an earlier run". If a media fetch stopped half-way, run the same command again.

WooCommerce

A WXR file does not contain products, orders or customers. The WooCommerce importer reads normalised JSON instead: one directory containing any of these files:

products.json   categories.json   brands.json
customers.json  orders.json       blog.json

A missing file is fine and is skipped with a note. A corrupt one stops the import. The repository does not include a tool that produces these files from a WooCommerce database. The expected shape is shown by the synthetic sample data in data/import.example/ and the input types in src/lib/import/woo.ts.

npm run import:woo -- data/import.example                 # rehearse with sample data
npm run import:woo -- /path/to/my-export --apply          # perform it
npm run import:woo -- /path/to/my-export --apply --media ./wp-content/uploads
Flag Meaning
--apply Perform the import. Without it, nothing is written
--media <dir> After importing, add product images from this folder (your extracted wp-content/uploads) to the media library and point products at the stored copies
--no-posts Shop only. Skip blog.json
--author <email> Put imported posts under this account. The default is the oldest administrator
--site-stopped Required with --apply on the doc-blob driver (DATABASE_URL set without DATABASE_DRIVER=relational). It confirms the site is stopped, because on that driver the site and the import would overwrite each other's writes

Keep real exports out of version control. They contain customer names, emails, phone numbers and addresses. data/import/ is gitignored for this reason.

What it does:

  • Prices must be whole numbers of cents (or strings of digits). A float, a comma decimal, null or a negative value is refused and reported, never rounded.
  • Categories keep their parent. Brands keep their spelling in any script and get a transliterated slug.
  • Customers get their email, name, phone and address. Passwords are not imported.
  • Orders keep their number, currency, total, line items and contact details, and are linked to the imported customer. WooCommerce statuses map to AstroBaaS statuses of the same name: completed, processing, on-hold, pending, cancelled, refunded, failed. Any unrecognised status becomes on-hold, so no automatic process touches it until a person looks.
  • Blog posts from blog.json come in as published or draft posts.
  • The shop is switched on if it was off, when products or orders are imported. It is never switched off.
  • References are resolved after their targets exist, so an order never points at a customer that does not exist.
  • Re-running is safe. Records are recognised by their wp_id, and brands also by name.

Source: scripts/import-woocommerce.mjs. See Commerce.

Markdown

npm run import:md -- ./content/posts

Each .md or .markdown file in the folder becomes a post. YAML frontmatter is read for these keys:

Key Use
title Title. Without it, the filename (dashes and underscores become spaces)
slug Slug. Without it, a slug made from the title
status draft, review, scheduled, published or trashed. Any other value becomes draft
excerpt Excerpt
tags An inline list: [a, b, c]
category Category name. Created if missing
date Sets the created and updated dates
featured_image Stored as the featured image URL

Other keys are ignored. The frontmatter parser handles plain values (text, numbers, true/false) and inline lists only.

The Markdown converter is deliberately minimal. It handles paragraphs, headings, code blocks, lists, links, bold, italic and inline code. For anything richer, put HTML in the file.

Limitations:

  • It works on the JSON file database only. It writes the db.json file directly (honouring DB_PATH), and refuses to run when the site uses libSQL or a relational database. The server keeps its own copy of the database in memory, so stop the site while this runs.
  • There is no dry run. Files whose slug already exists are skipped with a warning.
  • Posts go to the first user in the database. They are always articles, never pages.
  • The content bypasses the API, so it is not HTML-sanitised or added to the change feed, and no webhooks fire.

Source: scripts/import-md.mjs.

Exporting and re-importing your own content

The lower half of Admin → Import a site, Your own content, moves posts, pages and any content type out as a file and back in.

Export

Choose a collection and a format, then click Export:

Format Notes
CSV Opens in Excel. Lists and repeaters are JSON in a single cell, booleans are true/false, dates are ISO-8601. Cells that would start a spreadsheet formula are escaped
JSON The same rows as JSON

Post and page columns: id, title, slug, status, excerpt, content, category_id, tags, featured_image, locale, translation_of, meta_title, meta_description, focus_keyphrase, noindex, pinned, menu_order, publish_date. View counts and created_at are left out on purpose. Content types export id plus their declared fields, and _status if they are moderated.

Directly: GET /api/transfer/<collection>?format=csv|json, where <collection> is post, page or a content type name. Any role that can write posts can export (admin, editor, author, manager). You only get what your role can see, so an author does not receive colleagues' drafts.

Import

Choose a file and click Check the file. The preview lists how many rows will be created, updated or skipped, with a reason for each skip. Import for real then applies it.

  • Rows are matched by id first, then by slug, never by position or title. A sorted spreadsheet imports correctly.
  • An id that matches no record here is skipped. It is not created, so a file from another site cannot quietly duplicate everything.
  • A row that matches nothing and has a slug is created. A new post or page needs a slug.
  • Updates are partial: only the columns in the file change.
  • Every row is validated against the collection's rules, and invalid rows are skipped.
  • The limit is 10,000 rows per file.

Directly: POST /api/transfer/<collection> with { "body": "<file text>", "format": "csv" | "json", "apply": true }. Without "apply": true, the response is the preview. Import needs import_content (admin only). Source: src/lib/content-transfer.ts.

Other ways to get data out

  • Whole-site backup: Admin → Tools → Download backup (the database plus uploads, as one JSON file). See Backups and storage.
  • One person's data: Admin → Data requests exports everything held about an email address. See Security.
  • The API: every collection is readable over REST. See Headless and REST API.

See also

Clone this wiki locally