Skip to content

Migration overhaul: guided wizard, migration key, and complete-site export (comments, menus, identity, CPT taxonomies)#8

Open
swissky wants to merge 8 commits into
emdash-cms:mainfrom
swissky:feat/migration-wizard
Open

Migration overhaul: guided wizard, migration key, and complete-site export (comments, menus, identity, CPT taxonomies)#8
swissky wants to merge 8 commits into
emdash-cms:mainfrom
swissky:feat/migration-wizard

Conversation

@swissky

@swissky swissky commented Jul 5, 2026

Copy link
Copy Markdown

What does this PR do?

This PR turns the exporter plugin into a guided, complete-site migration tool. The goal: a WordPress user should be able to move their whole site to EmDash in minutes, without reading docs, and without discovering missing content after the switch.

It is the plugin half of a two-part effort; the EmDash core counterpart (import-side) is emdash-cms/emdash#1830.

Guided migration wizard (UX)

  • New wizard under Tools → EmDash Migration that walks the user through the migration instead of leaving them with a bare settings page.
  • Preflight site checks before anything else: permalinks, REST loopback, Authorization header pass-through, Application Passwords availability, public reachability. Each check explains how to fix a failure — the top support issues (no routes mapping in lokal installations #5, Doesn't seem to detect sites managed with Local #7 territory) are diagnosed up front instead of failing later with cryptic errors.
  • "What will be migrated" overview: post types with counts, taxonomies with counts, media, menus, comments, ACF fields, SEO metadata, and site identity are listed as Included — and known gaps as Not included — so expectations are set before the user commits.
  • One-click migration key: creates a revocable Application Password and packages URL + credentials into a single em1. copy-paste string. On the EmDash side, pasting the key starts the import directly — no manual URL/username/password juggling.
  • Deploy to Cloudflare shortcut for users who don't have an EmDash site yet, with a note about plan requirements and a checklist for the common deploy-400 pitfalls.

Complete-site export (new/extended endpoints)

  • /comments (new): approved and pending comments with authors, dates, threading, and status — paginated. Comments were previously silently left behind.
  • /options (extended): custom logo and site icon with resolved URLs, so EmDash can take over title, tagline, logo, and favicon instead of keeping starter-template placeholders.
  • /taxonomies (extended): each taxonomy now carries its singular label and registered post types (object_type), so EmDash can auto-create custom post type taxonomies (e.g. a company taxonomy on a company CPT) scoped to the right collections, instead of dropping them as "missing".
  • /menus (new): navigation menus with hierarchy and theme locations.
  • Per-post permalink for SEO-preserving redirect maps, and WPML/Polylang info (per-post locale/translation_group, site-level i18n) for multilingual migrations.

Hardening

  • Export endpoints now require the export capability (previously edit_posts was enough).
  • Server file paths removed from media responses; unserialization of meta values hardened.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactoring / code quality

Related issues

Testing

Tested end-to-end against two live WordPress sites: a small business site (posts/pages/CPT with ACF + Yoast) and a large blog (~1.8k posts, 3.5k media files, 300+ comments, Rank Math, custom taxonomies). Content, comments, menus, taxonomies, SEO fields, and site identity all arrive in EmDash.

Checklist

  • Tested against live WordPress installs (see above)
  • readme.txt changelog updated (1.2.0)
  • Version bumped to 1.2.0

AI-generated code disclosure

  • Parts of this PR were written with AI assistance (Cursor, Claude). All changes were reviewed and tested against live WordPress sites by a human.

swissky added 5 commits July 5, 2026 20:12
…rt, and hardening

- Tools → EmDash Migration wizard: preflight health checks (permalinks,
  REST loopback, Authorization header, app passwords, reachability) with
  actionable fixes, one-click migration key, deploy-to-Cloudflare branch,
  honest what-gets-migrated overview
- Migration key: programmatic revocable Application Password packaged as
  em1.<base64url(JSON)> so users never type URLs or credentials
- New GET /menus endpoint matching EmDash importMenusFromPlugin() format
- Per-post permalink for redirect maps; WPML/Polylang locale +
  translation_group; i18n info in probe/analyze
- Security: require export capability (was edit_posts), remove file_path
  from media responses, unserialize with allowed_classes=false, 403 via
  rest_authorization_required_code()
- Release workflow: installable zip on tags and PR artifacts
…aid plan

The blog-cloudflare template ships a worker_loaders binding (Dynamic
Workers, EmDash's plugin sandbox), which the Cloudflare API rejects with
HTTP 400 on the Free plan (upstream emdash issues #31/#149). Warn users
under the deploy button and point to the free-plan workaround.
- New /comments endpoint: approved and pending comments, paginated oldest-first,
  with author, email, plain-text body, UTC date, threading, and status.
- /options now includes custom_logo_url and site_icon_url so EmDash can take
  over the site logo and favicon.
- Wizard overview lists comments (with count) and site identity as included.
…ate CPT taxonomies

/taxonomies now includes each taxonomy's registered post types (object_type)
and singular label. EmDash uses this to create matching taxonomy definitions
scoped to the right collections during import, instead of skipping custom
post type taxonomies as "missing".
$response = wp_remote_get(rest_url('emdash/v1/header-check'), [
'timeout' => 10,
'sslverify' => false,
'headers' => ['Authorization' => 'Basic ' . base64_encode('emdash:header-check')],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very much outside my wheelhouse, but would sending a Basic auth header not risk actually triggering application password checks even on a public route, which would then fail as it's invalid? It would be safer to use Bearer, which the agents inform me is ignored by application password

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or even a custom type, like Authorization: EmDashCheck test)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and it's worse than a risk — two concrete failure modes: if the site happens to have a user named emdash, Application Password validation rejects the whole REST request (401) and the wizard falsely reports "your server strips the Authorization header"; and security plugins (Wordfence & co.) can count the fake Basic credentials as a failed login attempt from the site's own loopback IP.

Went with your custom-scheme suggestion in bf3539d: the probe now sends Authorization: EmDashCheck header-check. Skipped Bearer deliberately — JWT/OAuth plugins hook Bearer the same way Application Passwords hook Basic, so a custom scheme is the only value nothing tries to authenticate. Since we only test whether the header string survives the server config, any value works.

While in there I also added a getallheaders() fallback to the check endpoint: under mod_php the header can be visible to Apache without being mirrored into $_SERVER['HTTP_AUTHORIZATION'], which would have made the probe a false negative on setups where Application Passwords actually work.

swissky added 2 commits July 6, 2026 11:35
…t fake Basic credentials

Review feedback from emdash-cms#8: a Basic header with dummy credentials triggers
Application Password validation — if a user named 'emdash' exists the
whole REST request 401s (false "header stripped" result), and security
plugins can count it as a failed login. Bearer risks the same collision
with JWT/OAuth plugins, so use "EmDashCheck header-check" — we only test
whether the header string survives the server config.

The endpoint now also falls back to getallheaders(): under mod_php the
header can be visible to the Apache API without being mirrored into
$_SERVER['HTTP_AUTHORIZATION'], which would have made the custom-scheme
probe a false negative.
…d readme.md

Upstream PR emdash-cms#4 renamed readme.txt to readme.md and reformatted it as
Markdown. Our changelog, wizard/comments/menus features, endpoint docs,
and the release-download install step are now folded into readme.md in
the new format; readme.txt is gone.
@swissky swissky requested a review from ascorbic July 6, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants