Configuration
Plans, not settings
Passer is configured per migration plan, in the control panel under Passer. A plan holds the source connection, the mapping and the phase selection. It is saved, re-runnable and exportable, because a real migration is never run once: it is run against a copy, examined, corrected, and run again over weeks while the WordPress site is still being edited.
The config file
Plugin-wide defaults can be set under Settings → Passer, or in config/passer.php so they can
differ per environment:
<?php
return [
// Records per batch. Lower it on constrained hosts.
'batchSize' => 50,
// Seconds before a request to the source gives up
'requestTimeout' => 30,
// Download attachments into a volume, and the largest file to accept
'downloadMedia' => true,
'maxAssetSizeMb' => 128,
// Rewrite links to the old site as Craft reference tags, and expand shortcodes
'rewriteUrls' => true,
'expandShortcodes' => true,
// Keep going past a record that fails, and list it in the report
'continueOnError' => true,
// Where the finished report is emailed
'reportEmail' => 'migrations@example.com',
];
Settings only the config file can set
Two settings are never shown in the control panel or saved from it, because they decide what the server will connect to:
<?php
return [
// Let media downloads and the REST source reach private addresses — a WordPress on the
// same LAN, in Docker, or on localhost. Off by default.
'allowPrivateNetwork' => true,
// Source settings the connect form doesn't offer, applied whenever that kind of source
// is used and never stored in a plan.
'sourceOverrides' => [
'wpcli' => ['sshOptions' => 'ProxyJump=bastion.example.com ConnectTimeout=10'],
'database' => ['sslCa' => '/etc/ssl/certs/rds-ca.pem'],
],
];
sshOptions accepts connection settings — ProxyJump, ConnectTimeout, ServerAliveInterval,
Compression, IdentitiesOnly and the like — as Key=Value pairs. Options that run a command,
such as ProxyCommand or LocalCommand, are refused wherever they come from.
Choosing destinations
During plan, each domain gets a destination. Passer detects what is installed and offers the preferred target, falling back to something that needs nothing but Craft:
| Domain | Preferred | Fallback |
|---|---|---|
| SEO | SEOmatic, ether/seo | Plain Craft fields Passer creates |
| Redirects | Retour, Venveo Redirect, Friends | Craft's own config/redirects.php (Craft 5.6+) |
| Menus | FreeNav, Verbb Navigation | A structure section of link entries |
| Forms | Formie | Bandage, or a written specification |
| Comments | Verbb Comments | Counted and reported |
| Widgets | — | Global sets, or a widgets section |
| Commerce | Craft Commerce | Products as plain entries |
Mapping
The plan proposes a full mapping you can override row by row:
- which post type becomes which section and entry type
- where each meta key lands, or that it is deliberately dropped
- which taxonomy becomes which category group
- which ACF field group becomes which field layout
The proposal comes out of analyse, so it is shaped by what is actually in the database rather than by what WordPress claims is installed.
The wp-import bridge
If craftcms/wp-import is installed, Passer reuses its ACF adapters and Gutenberg block
transformers for anything it does not handle itself — including adapters you wrote in
config/wp-import/.
The coupling is deliberately loose. wp-import ships as dev-main with no tagged releases, so a
bridge that cannot load leaves Passer's own adapters in place rather than failing the migration.