Though there are plenty of redirection plugins available for WordPress, I needed a solution that makes it especially easy for content editors to enter outdated post paths directly to the relevant post edit screen while migrating content manually from an already existing site.
Add a Custom Text Area Field for Old Paths
Add a custom text area field to all relevant post edit screens, e.g. via the ACF or Meta Box plugin. Multiple paths can be entered, one path per line, without any separating characters.
Here is my ACF version of the field in JSON format, including German instructions for the user:
[
{
"key": "group_63e8f08931ffb",
"title": "Migration",
"fields": [
{
"key": "field_63e8f0896a970",
"label": "Alte Pfade",
"name": "bub_migrate_urls",
"aria-label": "",
"type": "textarea",
"instructions": "Falls Daten von einer bestehenden Website migriert werden, geben Sie hier den alten Adresspfad des Inhalts an. Für mehrere Adresspfade setzen Sie jeden weiteren Pfad in eine neue Zeile (ohne Anführungszeichen, Kommas o.ä.).<br><br>\r\n\r\nBeispiel: Für die Adresse (URL) \"https:\/\/www.mydomain.com\/produkte\/camus-der-fremde\/\" lautet der anzugebende Pfad: \"\/produkte\/camus-der-fremde\/\"<br><br>\r\n\r\nDiese Angabe wird genutzt, um im Fall einer Abweichung automatisch eine Weiterleitung von der alten zur neuen Adresse zu setzen. Auf diese Weise können bereits etablierte Links und Suchergebnisse auch weiterhin funktionieren. Sobald die Migration abgeschlossen ist, können Sie diese Box ausblenden.",
"required": 0,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"relevanssi_exclude": 0,
"default_value": "",
"acfe_textarea_code": 0,
"maxlength": "",
"rows": 1,
"placeholder": "",
"new_lines": ""
}
],
"location": [
[
{
"param": "post_type",
"operator": "==",
"value": "post"
}
],
[
{
"param": "post_type",
"operator": "==",
"value": "page"
}
],
[
{
"param": "post_type",
"operator": "==",
"value": "bub_event"
}
],
[
{
"param": "post_type",
"operator": "==",
"value": "bub_content"
}
],
[
{
"param": "post_type",
"operator": "==",
"value": "bub_party"
}
],
[
{
"param": "post_type",
"operator": "==",
"value": "product"
}
]
],
"menu_order": 0,
"position": "acf_after_title",
"style": "default",
"label_placement": "left",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "all",
"show_in_rest": 0,
"acfe_autosync": [
"json"
],
"acfe_form": 0,
"acfe_display_title": "",
"acfe_meta": "",
"acfe_note": ""
}
]
Add the Redirection Function
The following PHP function, added to your theme or plugin, will try to redirect the old paths to the current post’s path, if they would otherwise cause a 404 error.
add_action( 'wp', 'bubdev_fields_migrate_urls' );
/**
* Old Path Redirection
*
* Lets content editors enter old post paths directly to the post edit screen
* while migrating content manually from an already existing site.
*
* Requires a custom text area field (e.g. ACF) on all relevant post edit
* screens (one path per line, no separating character). The function will then
* try to redirect the old paths to the current post's path, if they would
* otherwise cause a 404 error.
*/
function bubdev_fields_migrate_urls() {
$old_paths_field_name = 'bub_migrate_urls';
// Get array of relevant post types (all custom post types plus 'post' and 'page').
$public_custom_post_types_arr = get_post_types( array(
'public' => true,
'_builtin' => false
));
$relevant_post_types_arr = array_merge( array( 'post', 'page' ), $public_custom_post_types_arr );
// Get posts that have a an 'old URL' field value.
$migrant_post_ids = get_posts( array(
'orderby' => 'none',
'no_found_rows' => true,
'update_post_term_cache' => false,
'fields' => 'ids',
'numberposts' => 10000,
'post_status' => 'publish',
'post_type' => $relevant_post_types_arr,
'meta_key' => $old_paths_field_name,
));
if ( ! $migrant_post_ids ) return;
// Create array with old and new URL paths: Sub-arrays having key-value pairs
// for 'old_path' and 'new_path'.
$paths_arr = array();
foreach( $migrant_post_ids as $migrant_post_id ) {
$curr_oldpaths_txt = get_post_meta( $migrant_post_id, $old_paths_field_name, true );
if ( ! $curr_oldpaths_txt ) continue;
// Create old paths array from newline separated list.
$curr_oldpaths_arr = explode( "\n", trim( $curr_oldpaths_txt ) );
// Handle wrong entries: Extract paths from full URL entries. Add missing
// leading slashes.
$curr_oldpaths_arr = array_map( function( $path ) {
$path = parse_url( trim( $path ), PHP_URL_PATH );
$path = ( substr( $path, 0, 1) !== '/' ) ? '/' . $path : $path;
return $path;
}, $curr_oldpaths_arr );
// Get new path (the current post path).
$curr_newurl_str = get_permalink( $migrant_post_id );
$curr_newpath_str = parse_url( $curr_newurl_str, PHP_URL_PATH );
// Map old paths to new paths.
$curr_paths_arr = array();
foreach( $curr_oldpaths_arr as $curr_oldpath_str ) {
if ( ! $curr_oldpath_str ) continue;
$curr_array_item = array(
'old_path' => $curr_oldpath_str,
'new_path' => $curr_newpath_str
);
array_push( $curr_paths_arr, $curr_array_item );
}
// Combine old-new path arrays.
$paths_arr = array_merge( $paths_arr, $curr_paths_arr );
}
// Try redirecting old to new paths only if the request URI causes a 404 error.
add_action( 'template_redirect', function() use ( $paths_arr ) {
if ( ! is_404() ) return;
$protocol = ( ( ! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' )
|| $_SERVER['SERVER_PORT'] == 443 ) ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$request_uri = $_SERVER['REQUEST_URI'];
foreach ( $paths_arr as $path_arr ) {
if ( trailingslashit( $request_uri ) === trailingslashit( $path_arr['old_path'] ) ) {
wp_redirect( $protocol . $host . $path_arr['new_path'], 301 );
exit;
}
}
});
}