Migration
Migrating a blog is the moment when a platform earns trust or loses it. VeloCMS ships importers for the three most common sources — WordPress, Substack, and Ghost — and every importer handles the two things that migrations most often break: images (moving them to R2 without losing them) and URLs (preserving redirects so your SEO equity doesn't evaporate). This guide covers each platform's import path.
Importing from WordPress
WordPress exports in a format called WXR (WordPress eXtended RSS) — it's XML. The VeloCMS WordPress importer parses this file, converts the post HTML to the TipTap-compatible content format, downloads all referenced image URLs and re-uploads them to your R2 bucket, and creates a redirect map from your old WordPress slug structure to the new VeloCMS slugs.
- Go to WordPress Admin → Tools → Export → All content. Download the .xml file.
- In VeloCMS Admin → Tools → Import → WordPress, upload the .xml.
- The importer runs in the background — large sites (2000+ posts) may take 10-15 minutes.
- After completion, download the redirect map CSV and implement it via Cloudflare Redirect Rules or nginx.
# For migrations with 1000+ posts, use the CLI importer directly
# (bypasses the 30s admin timeout)
POCKETBASE_URL=https://your-pb.up.railway.app \
[email protected] \
POCKETBASE_ADMIN_PASSWORD=yourpassword \
R2_BUCKET_NAME=velocms-media \
node scripts/import-wordpress.mjs --file ./export.xml --tenant-id <tenant-id>WordPress URL mapping
WordPress uses several slug formats depending on your permalink settings. The importer detects your old structure from the export and generates 301 redirect rules for Cloudflare:
| WordPress format | Example | Redirects to |
|---|---|---|
| /%postname%/ | /my-post/ | /blog/my-post |
| /%year%/%monthnum%/%postname%/ | /2023/04/my-post/ | /blog/my-post |
| /?p=123 | /?p=123 | /blog/my-post |
| /category/tech/my-post/ | /category/tech/my-post/ | /blog/my-post |
Image upload to R2
Every image referenced in WordPress post content is downloaded and re-uploaded to your Cloudflare R2 bucket during import. The importer replaces the original WordPress domain URLs in the post content with your R2 public URL. If an image download fails (404, rate limit), it's logged to import-errors.json and the post is imported with the original URL intact — you can re-run the image sync later.
# After the main import, sync any failed images
node scripts/import-wordpress.mjs \
--file ./export.xml \
--tenant-id <id> \
--images-only \
--retry-errors ./import-errors.jsonImporting from Substack
Substack exports two files: posts as a CSV (metadata + HTML content) and a separate subscribers CSV. VeloCMS handles both — posts go into your posts collection, subscribers go into blog_members. The post HTML from Substack is reasonably clean and imports without needing a custom converter.
- In Substack → Settings → Exports, request your export. You'll receive an email with a download link.
- The ZIP contains posts.csv and subscriber-list.csv.
- In VeloCMS Admin → Tools → Import → Substack, upload both files (or just one if you only need posts or subscribers).
Importing from Ghost
Ghost exports a single JSON file that includes posts, pages, tags, and members in one document. The VeloCMS Ghost importer handles the full export — it converts Ghost's Mobiledoc/Lexical format to TipTap JSON for rich editing, maps Ghost tags to VeloCMS categories, and imports members with their tier assignments.
- In Ghost Admin → Settings → Labs → Export your content. Download the ghost-export.json.
- In VeloCMS Admin → Tools → Import → Ghost, upload the JSON file.
- Ghost's paid member tier maps to VeloCMS 'paid'; free members map to 'free'.
- Ghost custom domains are preserved in the redirect map.
# Programmatic Ghost import with redirect map output
POCKETBASE_URL=https://your-pb.up.railway.app \
[email protected] \
POCKETBASE_ADMIN_PASSWORD=yourpassword \
node scripts/import-ghost.mjs \
--file ./ghost-export.json \
--tenant-id <id> \
--output-redirects ./ghost-redirects.csvPreserving redirects
Every importer outputs a redirects CSV in the format source,destination,status_code. After import, implement these redirects at the CDN layer (Cloudflare Redirect Rules) or your server (nginx/Caddy). Implementing them in Next.js next.config.ts is an option but not recommended for large blogs — a thousand redirect rules in next.config.ts bloats the build artifact.
# Upload bulk redirects to Cloudflare using the Bulk Redirects API
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/rules/lists" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"velocms-migration-redirects","kind":"redirect"}'
# Then upload the redirect items CSV via:
# https://developers.cloudflare.com/rules/bulk-redirects/