This guide helps you resolve common issues with Safe Publish. See the Debugging Tools section below for further diagnostic steps.
Symptoms: Cannot connect to source site, authentication error message
Solutions:
-
Verify Safe Publish is configured on both sites:
- Safe Publish must be installed and active on both sites.
- Sync Mode and Connected Site URL must be set with correct values on both sites.
-
Verify shared secret matches on both sites:
- Check
wp-config.phpon both sites. - Secret must be identical (case-sensitive).
- No extra spaces or quotes
- Check
-
For basic auth:
- Verify username and password are correct.
- Ensure user has
edit_postscapability. - Check basic auth plugin is installed on source site.
-
Check HTTPS:
- Production domains must use HTTPS (HTTP is allowed for local development domains like
.test,.local,.dev). - Verify SSL certificates are valid.
- Test site URL in browser.
- Production domains must use HTTPS (HTTP is allowed for local development domains like
Symptoms: Request hangs then fails with timeout error
Solutions:
-
Check network connectivity:
- Can you access the source site in a browser?
- Ping the source site domain.
- Check for firewall rules blocking the connection.
-
Increase PHP timeout limits:
// In wp-config.php set_time_limit( 300 ); // 5 minutes
-
Check server resources:
- High CPU/memory usage can cause timeouts.
- Monitor server during import.
-
Try fewer posts:
- Reduce "Number of Posts" setting.
- Import in smaller batches.
Symptoms: The connection test reports the site could not be reached, yet the site loads in a browser.
Solution: If the message says the connected site was reached but is not fully configured, the far side responded but has no Safe Publish shared secret or connected-site URL set. Complete the Safe Publish configuration on that site and retry. Otherwise, work through the timeout steps above.
Symptoms: Authentication request rejected with a 401 or 403 response
A 401 or 403 has two distinct causes, and the Test Connection result tells them apart:
- Safe Publish rejected the request — the response carries a
safe_publish_auth_*code. This is a genuine credential problem: the shared secret does not match, the connected-site URL configured on the far side does not match this site, or the two sites' clocks have drifted. - Something upstream blocked the request — a security plugin, theme, mu-plugin, or host/WAF rule returned the response before Safe Publish's authenticator ran. It carries a foreign code such as
rest_forbidden, or no JSON at all.
Solutions:
- For a genuine Safe Publish rejection:
- Verify
SAFE_PUBLISH_SHARED_SECRETmatches on both sites (case-sensitive, no stray spaces). - On the connected site, set its connected-site URL to this site's home URL.
- If the message mentions a clock difference, synchronize both servers' clocks with NTP.
- Confirm Safe Publish is installed on the source site with Sync Mode set to
Export.
- Verify
- For an upstream block:
- Allowlist the
wp/v2andsafe-publish/v1REST namespaces for signed requests — they carry theX-Safe-Publish-Signatureheader. - Review security-plugin, firewall, and host rules that restrict REST API access.
- See The connected site blocks the request before Safe Publish can authenticate for host-side allowlisting options and a deferral snippet.
- Allowlist the
Symptoms: The connection test reports that the connected site blocked the request before Safe Publish could authenticate it. On the connected site, Safe Publish's Audit Log shows no auth-channel entry for the attempt.
Cause: A security plugin, theme, mu-plugin, or WAF on the connected site gates the REST API — typically through the rest_authentication_errors filter — and rejects the request before Safe Publish's authenticator runs. When it uses that filter, WordPress core stops processing the request as soon as the filter returns an error, so the authenticator never sees it.
Fix: Let Safe Publish's signed requests reach its own authenticator. It reads posts, pages, and media through core's wp/v2 endpoints, and serves its catalog from safe-publish/v1, so both namespaces must be reachable. Its requests carry the X-Safe-Publish-Signature header. Use Option 1 when you cannot edit the gate, Option 2 when you can.
Option 1 — allowlist the REST namespaces. In the gate's settings, allow the wp/v2 and safe-publish/v1 namespaces through. Use this for a third-party security plugin you can configure but not edit.
Option 2 — defer signed requests in code. In a gate you control, return the incoming result unchanged for a signed request to those namespaces, so it reaches Safe Publish's own authenticator. Add the guard at the top of your rest_authentication_errors callback, before the code that blocks:
add_filter(
'rest_authentication_errors',
static function ( $result ) {
// The REST route WordPress will dispatch, e.g. /wp/v2/posts.
$route = $GLOBALS['wp']->query_vars['rest_route'] ?? '';
$is_safe_publish_route = 0 === strpos( $route, '/wp/v2/' )
|| 0 === strpos( $route, '/safe-publish/v1/' );
// Defer Safe Publish's signed requests to its own authenticator.
if (
isset( $_SERVER['HTTP_X_SAFE_PUBLISH_SIGNATURE'] )
&& $is_safe_publish_route
) {
return $result;
}
// Your existing REST gate logic goes here, for example:
// return new WP_Error( 'rest_forbidden', 'REST API disabled.', array( 'status' => 401 ) );
return $result;
}
);Note
This does not expose private content: Safe Publish still validates the HMAC signature before serving edit-context or import data, so a forged or absent signature retrieves nothing beyond WordPress' default anonymous REST access. For unsigned requests, the allowlisted wp/v2 namespace exposes only what WordPress already serves anonymously, including the author data at wp/v2/users.
Symptoms: 404 error when connecting to source site
Solutions:
-
Verify REST API is enabled:
- Visit
https://your-site.com/wp-json/in browser. - Should return JSON, not 404.
- Visit
-
Check permalink structure:
- Source site must have permalinks enabled.
- Go to Settings → Permalinks and save.
-
Disable or configure conflicting plugins:
- Some security plugins block the REST API.
- Allowlist the
wp/v2andsafe-publish/v1namespaces for signed requests (they carry theX-Safe-Publish-Signatureheader), or temporarily disable the plugin and test.
-
Check .htaccess:
- Corrupted .htaccess can block REST API.
- Try resaving permalinks.
Symptoms: Connection works but no posts are displayed
Solutions:
-
Check post status:
- By default, the REST API returns only published posts (authenticated requests with
context=editmay include other statuses). - Verify posts exist and are published on source site.
- By default, the REST API returns only published posts (authenticated requests with
-
Verify post type is exposed in REST API:
- Custom post types must support
'show_in_rest' => true. - Check post type registration.
- Custom post types must support
-
Check post permissions:
- Authenticated user must have read access to posts.
- Review user roles and capabilities.
-
Increase "Number of Posts" setting:
- Default is 10, try increasing to 50.
- Maybe your posts are pagination past first page.
Symptoms: Post imports but images are missing
Solutions:
-
Check image URLs are accessible:
- Copy image URL and open in browser.
- Must return 200 OK, not 404 or 403.
-
Verify image URLs are absolute:
- Relative URLs may not work.
- Images must be publicly accessible.
-
Check file size limits:
- PHP
upload_max_filesizeandpost_max_size - WordPress
WP_MEMORY_LIMIT - Reduce image sizes if needed.
- PHP
-
Check disk space:
- Ensure destination site has sufficient disk space.
- Check server quotas.
-
Review upload directory permissions:
wp-content/uploads/must be writable.- Check file permissions (755 for directories, 644 for files).
Symptoms: Validation fails, content structure error
Solutions:
-
Check for broken HTML:
- Edit source post in WordPress.
- Look for validation errors in block editor.
- Fix any invalid HTML or blocks.
-
Verify Gutenberg blocks are valid:
- Switch to code editor view.
- Check block comments are closed.
- Look for corrupted block syntax.
-
Test with simple content:
- Create a test post with simple content.
- If it imports, issue is with complex content.
- Simplify problematic content.
-
Check for unsupported blocks:
- Some third-party blocks may not transfer.
- Try converting to core blocks.
Symptoms: Import process completes but no draft post appears
Solutions:
- Check user permissions: The importing user must have permission to create posts of that post type.
- Check for database errors: Enable
WP_DEBUG_LOGand reviewwp-content/debug.logfor insert failures. - Verify post type is registered on the destination site — custom post types must exist on both sites.
Symptoms: ACF or Secure Custom Fields (SCF) values were imported as post meta, but the destination editor does not show the matching field controls.
Solutions:
-
Use the migration recipe: The recommended path is the filter-based ACF/SCF recipe, which reads values from the source
acfobject and, for complex fields, writes them through ACF on the destination. -
Verify the source REST response:
- Fetch the source post with
context=edit. - Confirm the expected values appear under the core
metaobject, or under the top-levelacfobject when using the recipe.
- Fetch the source post with
-
Check the imported destination meta:
- Confirm the destination post has the value key, such as
hero_title. - Confirm it also has the companion reference key, such as
_hero_title, when editor rendering is required.
- Confirm the destination post has the value key, such as
-
Check the destination field definitions:
- ACF or SCF must be active on the destination.
- The destination must have matching field groups and field keys.
- Safe Publish stores the meta values but does not create or sync ACF/SCF field groups.
-
Resolve the mismatch:
- Add or sync the matching field group definitions on the destination.
- If editor rendering is not needed, no action is required; the values are already stored as post meta.
Symptoms: A post's Yoast meta description, SEO title, or other Yoast settings are empty on the destination after import.
Solutions:
-
Understand the cause: Yoast stores these under protected
_yoast_wpseo_*meta keys, and WordPress core's REST API omits a meta key from themetaobject unless it is registered withshow_in_rest. Yoast registers only a few of these keys, so Safe Publish — which imports only themetaobject — does not receive most of them by default. -
Use the migration recipe: Register the Yoast keys for REST on the source so they enter the
metaobject. See the Yoast SEO recipe. No destination-side code is needed for scalar keys. -
Verify the source REST response: Fetch the source post with
context=editand confirm the_yoast_wpseo_*keys appear under the coremetaobject after registering them. -
Check source-specific values:
_yoast_wpseo_canonicaland_yoast_wpseo_primary_categorycarry source URLs and term IDs; remap or omit them rather than importing verbatim.
Symptoms: Same post imported multiple times
Solutions:
Safe Publish tracks imported posts using the safe_publish_source_post_id meta key and automatically detects already-imported content. Posts that already exist locally are shown with an Update action instead of Import.
If duplicates still occur:
- Check the Imports → Posts tab to see whether the post was imported from different sessions.
- Delete duplicate drafts manually.
This is a known limitation of WordPress' embed cache when imported posts reference each other while still in draft. See Embedded posts may render as plain links for the cause and recovery steps.
Links inside post body content are migrated by host swap only, preserving the path, so a link can break when the target's destination slug or permalink differs from the source. See Internal body links may 404 or open the wrong page for the cause and what to review.
A navigation link or submenu whose target was a draft at import keeps the host-swapped source path instead of being re-derived, so it can break under a slug collision or a different permalink structure. See Navigation links to draft targets may 404 or open the wrong page for the fix.
Real source media-library images bring their alt text, title, caption, and description to the destination attachment, including images inserted at an intermediate size and responsive srcset sub-sizes. The exception is a file linked but not held in the source library, which has no source record to copy. See Some sideloaded files carry no source library metadata.
The taxonomy is not registered on the destination site, so its terms could not be attached. The post imports without them and the gap is recorded on the Needs attention tab, naming the taxonomy and the terms that were dropped. Register the taxonomy on the destination (see Custom Post Types), then re-import the post: the terms attach and the degradation clears. This degradation offers no Retry, since no import can register a taxonomy.
| Error code | Cause | Solution |
|---|---|---|
invalid_url |
URL not valid or accessible | Check URL format and ensure the site is reachable |
request_failed |
HTTP request to the source site failed | Check network connectivity and site availability |
meta_update_failed |
One or more post meta keys failed to save | Check destination site database permissions |
source_author_unresolved |
Source post has no author or its author was deleted | Restore the source author or attribute the post manually |
source_author_not_found |
Source author's email has no match on the destination | Create a user with the same email on the destination |
Symptoms: Imports take a very long time to complete
Solutions:
-
Reduce content size:
- Fewer images = faster imports
- Smaller images = faster downloads
- Optimize images before importing.
-
Increase PHP limits:
// In wp-config.php or php.ini max_execution_time = 300 memory_limit = 256M
-
Check network speed:
- Slow network connection affects image downloads.
- Consider importing during off-peak hours.
-
Import in smaller batches:
- Don't bulk import too many posts at once.
- Try 5-10 posts at a time.
-
Enable object cache:
- Install Redis or Memcached.
- Significantly improves performance.
Symptoms: Browser freezes during bulk import
Solutions:
-
Don't bulk import too many posts:
- Limit to 5-10 posts at a time.
- Browser needs to process responses.
-
Close unnecessary browser tabs:
- Frees up memory.
- Improves responsiveness.
-
Use modern browser:
- Chrome or Firefox recommended
- Keep browser updated.
-
Increase browser memory (advanced):
- Some browsers allow memory limit increases.
- Or use different browser.
Symptoms: Posts list doesn't appear, loading spinner forever
Solutions:
-
Check JavaScript console:
- Open browser DevTools (F12).
- Look for JavaScript errors.
- Report errors with details.
-
Clear browser cache:
- Hard refresh (Ctrl+Shift+R / Cmd+Shift+R).
- Clear cache and cookies.
-
Check for JavaScript conflicts:
- Disable other plugins temporarily.
- Test if issue persists.
-
Verify assets are loading:
- Check Network tab in DevTools.
- Look for 404 errors on JS/CSS files.
Symptoms: Changes to settings don't persist
Solutions:
-
Check for PHP errors:
- Enable WP_DEBUG in wp-config.php.
- Check debug.log for errors.
-
Verify user permissions:
- User must have
manage_optionscapability. - Check user role.
- User must have
-
Check for plugin conflicts:
- Disable other plugins.
- Test if settings save.
-
Database issues:
- Check wp_options table is writable.
- Review database errors in logs.
Add the WordPress debug constants to wp-config.php, then check wp-content/debug.log for error messages.
- Open DevTools (F12).
- Check Console tab for JavaScript errors.
- Check Network tab for failed requests.
- Review request/response details.
Install Query Monitor for advanced debugging:
- Database queries
- HTTP requests
- PHP errors
- Hook execution
Use the Test Connection button in settings to test authentication independently of imports.
Check the Imports → Needs attention tab for:
- Error messages recorded at import time
- Source URL of each failed attempt
- Timestamp of the attempt
Recovery is fixing the underlying issue and re-importing from Source Posts. Once a failure is no longer needed, use the Remove action (per-row or bulk) to clear it from the tab; this only deletes the record and does not affect the source.
If you still can't resolve the issue:
- Check the Imports → Needs attention tab for detailed error messages.
- Enable debug mode and collect error logs.
- Reproduce the issue in a clean environment if possible.
- Report the issue via GitHub Issues with:
- WordPress and PHP versions
- Steps to reproduce
- Error messages
- Screenshots if applicable
- Debug log excerpts
- GitHub Issues: Bug reports and feature requests
- Documentation: Check docs for detailed guides
- Local Development: Use dev environment for testing
If you need to start fresh:
// Using WP-CLI
wp option delete safe_publish_connected_site_url
wp option delete safe_publish_sync_mode
wp option delete safe_publish_basic_auth_username
wp option delete safe_publish_basic_auth_passwordImport history is stored in two custom tables (safe_publish_imports and safe_publish_import_items). Individual rows can be rolled back from the Imports → Posts tab (per-row or bulk). To clear history entirely, use the Complete Reset below.
Delete the options listed above using WP-CLI, then deactivate and reactivate the plugin. Reactivation restores default option values.
- Local Development - Set up debugging environment
- Authentication - Detailed auth setup
- Import Process - How imports work
- Hooks and Filters - Customize behavior