Moving a WordPress site into a subscription platform is mostly a data problem — get the content across cleanly, keep the URLs alive, then decide which pages earn money.
This is a practical walk-through for solo developers and agencies migrating a content-heavy WordPress site into VBWD and putting the good parts behind a subscription. VBWD is self-hosted and source-available under the Business Source License 1.1 — free for commercial use until your VBWD-attributable annual sales exceed the value of 6.7 BTC, and it converts to Apache-2.0 on its change date. The stack is Python/Flask, PostgreSQL, Redis, and a Vue 3/TypeScript frontend, all in Docker, built on an agnostic core with a plugin system. That last part matters here: the WordPress importer is a plugin, not a core hack.
Be honest with yourself up front. A WordPress site is three things layered together: content, presentation, and interactive behavior. Only the first layer moves cleanly.
If you plan the migration around "content moves, everything else is a rebuild," you will not be surprised later. The mistake agencies make is quoting a migration as if it were a copy job, then discovering that a client's site leaned on twelve shortcodes and a bespoke theme. Scope the content import and the rebuild as two separate deliverables, because they carry different risk and take different skills.
The importer is delivered as the wp-import plugin. You can read the full plugin catalog on the plugins page, but the short version is that it maps WordPress content into VBWD's CMS:
cms_post records.cms_term records — categories and tags are two flavors of the same term entity.The single most useful property of the importer: it UPSERTs on (type, slug). Re-running the import does not create duplicates. It updates the existing record with the matching type and slug. This makes the import idempotent, which changes how you work.
Because re-running is safe, you migrate iteratively instead of in one nervous big-bang cutover:
wp-import plugin.A rough enable-and-import loop looks like this:
# bring the stack up
docker-compose up -d
# enable the importer plugin (writes to the shared plugins dir)
curl -X POST \
http://localhost:8081/api/v1/admin/frontend-plugins/backend/wp-import/enable \
-H "Authorization: Bearer $ADMIN_TOKEN"
# run the import; re-run freely — it UPSERTs on (type, slug)
flask wp-import run --source ./export/wordpress.xml
Treat the first few runs as disposable. You are tuning the mapping, not committing to it. A workable rhythm is to import, run a small script that diffs the count of source posts against cms_post rows, eyeball a dozen bodies for broken markup, and adjust. Because the import is idempotent, you can wire this into a Makefile target and run it on every change to your export without fear of accumulating junk. When the diff is zero and the spot-checks pass, you're ready to think about URLs.
This is where migrations quietly go wrong. A content migration that changes every URL is not a migration — it's a new site that happens to have your old words, and Google treats it that way. Traffic you spent years earning can evaporate in the week after a careless cutover.
The rule is simple: keep your slugs. The importer maps WordPress slugs directly onto cms_post and cms_term records, and VBWD routes pages by slug. If /how-to-brew-cold-brew/ was your URL in WordPress, it should still be your URL in VBWD. Match your permalink structure so the path — not just the trailing slug — lines up. A site on /blog/2021/03/my-post/ and one on /my-post/ are different URL spaces, and the difference is redirects you have to write.
VBWD's CMS carries per-entity SEO fields, so the metadata that earned your rankings comes with the content:
meta_title and meta_descriptionog_*) for how links render on social platformscanonical URLSet the canonical explicitly on migrated pages. If any URL does change during the move, that canonical tag is what stops the old and new versions from competing. If your WordPress SEO metadata lived in a plugin like Yoast or Rank Math, confirm it survived the export — a bare WXR file may not carry those custom fields, in which case you map them separately rather than losing your descriptions.
There's one more piece that people miss when moving from WordPress (server-rendered) to a Vue single-page app. Search crawlers need HTML, not an empty shell that fills in via JavaScript. VBWD runs an SEO renderer sidecar that produces crawlable pages for the SPA, so migrated content stays indexable instead of dropping out of results the week you cut over. The architecture overview covers how that renderer fits alongside the frontend, and it's worth understanding before launch so you can verify a crawler sees real markup — fetch a page with curl and a normal browser user agent and check the body is populated, not empty.
Whatever you do, if you change a slug, add a redirect and test it. A 301 from the old path to the new one preserves the link equity; a broken URL throws it away. Build the redirect map from your export before cutover, test every entry against the staging instance, and keep the old URLs in a sitemap until the new ones are fully indexed.
Now the business half. The reason to move off a plain blog is usually to charge for some of it.
VBWD separates "who can see this" from "who can do admin things" through distinct role systems. For content gating, the one you care about is user access levels, which gate the /user surface. You combine that with the subscription plugin to tie access to an active plan.
The flow is:
A visitor without an active subscription sees the public posts and a gate on the premium ones; a subscriber's access level clears the gate. Because publishing a public page in VBWD is a matter of a layout plus a widget, a plugin can expose these gated pages without anyone editing the frontend router by hand. That keeps your gating logic in one place instead of scattered across templates the way WordPress membership plugins tend to leave it.
A practical decision you'll face is where to draw the line between free and paid. A common pattern is to keep older or introductory posts public — they still pull in search traffic and feed the top of your funnel — while gating your newest, deepest, or most reference-worthy material. Because premium status is a per-post flag, you can move that line later without re-importing anything.
If your model is metered rather than all-you-can-read — pay per article, per download, per API call — VBWD also has a token and credit system for metered access. That fits cases like a research archive where a subscriber gets a monthly allowance of unlocks rather than the whole library. The billing documentation details how plans, subscriptions, and tokens relate, and the pricing model for the platform itself is worth reading before you commit.
Make it concrete. Say you run a woodworking site: 300 free tutorials built up over six years, plus a plan to sell a members-only library of downloadable cut plans.
The migration goes like this. You export the WordPress site to WXR, stand up VBWD in Docker on a staging box, and enable wp-import. The first run brings in all 300 tutorials as cms_post records, with your "Joinery," "Finishing," and "Tools" categories landing as cms_term records. You notice the image URLs still point at the old wp-content/uploads path, so you move the media, rewrite the URLs in your export, and re-run — the UPSERT overwrites the earlier rows, no duplicates. You keep the existing /tutorials/dovetail-joint/ slugs unchanged, so no redirects are needed for the tutorials, and you carry each post's meta_description across so nothing drops in search.
For the paid side, you define two plans in the subscription plugin — a free tier and a "Workshop" tier — and map the Workshop tier to a user access level. The cut-plan downloads are new posts marked premium against that access level. Free visitors read every tutorial and see a gate on the cut plans; Workshop subscribers download freely. The whole thing runs on one Docker host you control, and the only recurring cost is the box it runs on.
Before you point DNS at the new instance, you crawl the staging site to confirm the SEO renderer serves real HTML for the tutorials, run the redirect map for the handful of slugs you did change, and check that a logged-out visitor genuinely hits the gate on a premium post rather than seeing its body flash before the SPA hides it. Those three checks are the difference between a launch and an incident.
No migration guide is complete without the parts that will cost you an afternoon.
[gallery] or [some_plugin] shortcodes, those come across as text. Decide whether to strip them, replace them, or build a widget that renders them.None of these are blockers. They're line items. Put them in the plan instead of discovering them in production.
A WordPress-to-VBWD migration is three phases, not one. First, get content across with the idempotent wp-import plugin and iterate until it's clean. Second, protect your search rankings by keeping slugs, setting canonical and meta fields, and leaning on the SEO renderer so the SPA stays crawlable. Third, decide which pages are premium and gate them with user access levels and the subscription plugin. The rebuild work — theme, interactive features, media — is real, but it's bounded and it's predictable once you name it.
Start on staging, run the import as many times as you need, and don't move DNS until your redirects test green.
Ready to try it? Spin up a local instance and enable the wp-import plugin from the getting-started docs.