How to do 301 redirects with template_redirect in PHP in WordPress

Contents

Introduction

This article explains, in exhaustive detail, how to implement HTTP 301 (permanent) redirects in WordPress using the template_redirect hook in PHP. It covers when to use template_redirect, best practices, common pitfalls (including redirect loops), the differences between wp_redirect and wp_safe_redirect, full example implementations for typical use cases, debugging tips, and SEO considerations. Every code example is provided and ready to paste into a themes functions.php file or a small plugin.

What is template_redirect and when it runs

template_redirect is a WordPress action that fires after the main WP query is set up (after WP has populated conditional tags like is_single(), is_page(), is_category(), is_404(), etc.), and before WordPress loads the theme template. That timing makes it ideal for safe, query-aware redirects because you can make routing decisions based on the parsed request without letting the theme render a page.

Key points:

  • Runs only for front-end requests: template_redirect does not run for admin screens (is_admin()) and typically does not run for AJAX requests if you check for them.
  • Runs after the main query: you can use conditional tags like is_page(), is_singular(), is_category(), is_404(), etc.
  • Use for redirects that depend on the requested content or URL: canonical redirections, legacy URL mapping, forcing HTTPS, removing attachment pages, redirecting old slugs to new slugs, etc.

Why use template_redirect for 301 redirects

  • It gives you access to WP conditional tags and query variables, enabling context-aware decisions.
  • It runs at the correct moment: after WP has determined what the request is, but before any headers are sent by theme code.
  • It keeps redirect logic centralized and independent of theme template files.

When not to use template_redirect

  • When you need to perform redirects very early (before WP parsing) — consider using parse_request or hooking into the webserver (nginx/apache) instead.
  • When handling administrative or REST API/AJAX requests — check is_admin(), wp_doing_ajax() or rest_do_request() as needed to avoid interfering with those flows.

Core functions and best practices

Here are the functions and patterns you will use for reliable, secure 301 redirects.

  • wp_redirect(location, status) — performs a redirect. The second parameter is the HTTP status code (use 301 for permanent, 302 for temporary). It sets headers you should call exit after it to stop execution.
  • wp_safe_redirect(location, status) — similar to wp_redirect but validates the target host against the allowed hosts. Use this when redirecting to external domains to avoid open redirect vulnerabilities.
  • status code — use 301 for permanent redirects so search engines transfer link equity and update indexes.
  • exit after redirect — always call exit after calling wp_redirect/wp_safe_redirect to prevent further code running or accidental output.
  • check for admin/AJAX/CRON contexts — avoid running template_redirect logic in these contexts: use !is_admin(), !wp_doing_ajax(), and !wp_doing_cron() as safeguards.
  • avoid output before headers — do not echo/print before the redirect header. Template_redirect normally runs before theme output, but plugin/theme code might have produced output earlier ensure clean logic placement.

Security: wp_redirect vs wp_safe_redirect

wp_redirect Redirects to any Location. Use only for internal or strictly-controlled external URLs.
wp_safe_redirect Validates host against allowed redirect hosts (home_url() host and site host by default). Use when redirect target may be external or user-supplied.

Preventing redirect loops

A common cause of problems is redirect loops. Always check that the requested URL differs from the target URL before issuing a redirect. For example, compare sanitized current URL with destination using parse_url or WP functions. Be mindful of trailing slashes and identical host/scheme.

Minimal safe pattern

A minimal pattern to attach your redirect logic is this. It includes common checks to ensure it runs only on front-end requests, avoids AJAX/admin contexts, and prevents loops.

lt?php
add_action( template_redirect, my_template_redirect_handler, 10 )

function my_template_redirect_handler() {
    // Avoid admin, AJAX, or CLI requests
    if ( is_admin()  defined( DOING_AJAX )  DOING_AJAX ) {
        return
    }

    // Get current URL (scheme   host   path   query)
    current_url = ( is_ssl() ? https:// : http:// ) . _SERVER[HTTP_HOST] . _SERVER[REQUEST_URI]

    // Example target
    target = home_url( /some-new-path/ )

    // Avoid redirect loops: compare normalized URLs
    if ( untrailingslashit( current_url ) !== untrailingslashit( target ) ) {
        wp_safe_redirect( target, 301 )
        exit
    }
}
?gt

Examples

Below are concrete, copy-paste-ready examples for common redirect needs. Put them in a themes functions.php or in a small plugin file (with plugin header).

Example 1 — Simple internal 301 redirect for a known old path

Redirect a known old URL path (for example /old-page/) to a new path (/new-page/).

lt?php
add_action( template_redirect, redirect_old_page_to_new, 10 )

function redirect_old_page_to_new() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    // Get request path only (without query)
    request_path = parse_url( _SERVER[REQUEST_URI], PHP_URL_PATH )

    if ( rtrim( request_path, / ) === /old-page ) {
        new_url = home_url( /new-page/ )
        wp_redirect( new_url, 301 )
        exit
    }
}
?gt

Example 2 — Redirect a specific post ID to another URL

If a post has been removed and you want to redirect by post ID to a different URL:

lt?php
add_action( template_redirect, redirect_post_id_123, 10 )

function redirect_post_id_123() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    if ( is_singular()  get_queried_object_id() === 123 ) {
        destination = home_url( /replacement-page/ )
        wp_redirect( destination, 301 )
        exit
    }
}
?gt

Example 3 — Redirect to an external domain safely

Use wp_safe_redirect when the target might be external. If you need to allow additional hosts, use the allowed_redirect_hosts filter.

lt?php
add_action( template_redirect, redirect_to_partner_site, 10 )

function redirect_to_partner_site() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    // Example: redirect /partner to https://partner.example.com/
    path = parse_url( _SERVER[REQUEST_URI], PHP_URL_PATH )

    if ( rtrim( path, / ) === /partner ) {
        destination = https://partner.example.com/
        // Allowing partner.example.com via allowed_redirect_hosts filter:
        add_filter( allowed_redirect_hosts, function( hosts ) {
            hosts[] = partner.example.com
            return hosts
        } )

        wp_safe_redirect( destination, 301 )
        exit
    }
}
?gt

Example 4 — Force HTTPS and preserve path query

This example redirects any HTTP request to HTTPS on the same host and preserves the full path and query string. It detects SSL properly and avoids loops.

lt?php
add_action( template_redirect, force_https_redirect, 1 )

function force_https_redirect() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    // If already HTTPS, nothing to do
    if ( is_ssl() ) {
        return
    }

    // Build HTTPS URL for current request
    host  = _SERVER[HTTP_HOST]
    path  = isset( _SERVER[REQUEST_URI] ) ? _SERVER[REQUEST_URI] : /
    https_url = https:// . host . path

    // Redirect permanently
    wp_redirect( https_url, 301 )
    exit
}
?gt

Example 5 — Redirect non-www to www (or vice-versa)

Redirects based on host. Use this carefully when behind proxies — check HTTP_X_FORWARDED_HOST if needed, or configure your server to set the proper host environment variables.

lt?php
add_action( template_redirect, redirect_non_www_to_www, 1 )

function redirect_non_www_to_www() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    // Use server host (ensure this is accurate in your environment)
    host = _SERVER[HTTP_HOST]

    // Target host: add www if missing
    if ( strpos( host, www. ) !== 0 ) {
        target_host = www. . host
        request_uri = isset( _SERVER[REQUEST_URI] ) ? _SERVER[REQUEST_URI] : /
        target = ( is_ssl() ? https:// : http:// ) . target_host . request_uri
        wp_redirect( target, 301 )
        exit
    }
}
?gt

Example 6 — Regex/Pattern redirects (legacy URL patterns)

If you have many old URLs following a pattern, you can use preg_match to map them.

lt?php
add_action( template_redirect, legacy_path_pattern_redirects, 10 )

function legacy_path_pattern_redirects() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    path = parse_url( _SERVER[REQUEST_URI], PHP_URL_PATH )

    // Example: /old-category/123-title -> /new-category/123-title (simple example)
    if ( preg_match( #^/old-category/(. )#, path, matches ) ) {
        slug = matches[1]
        target = home_url( /new-category/ . slug )
        wp_redirect( target, 301 )
        exit
    }

    // Another pattern: redirect numeric legacy IDs to a new slug (custom mapping)
    if ( preg_match( #^/legacy-item/([0-9] )#, path, m ) ) {
        id = (int) m[1]
        // Map id to slug or URL (preferably lookup via a DB table or transient)
        new_slug = get_post_meta_by_legacy_id( id ) // pseudo function
        if ( new_slug ) {
            wp_redirect( home_url( /item/ . new_slug ), 301 )
            exit
        }
    }
}
?gt

Example 7 — Redirect attachment pages to their parent post

Attachment pages often have poor SEO redirect them to the parent post or the file URL.

lt?php
add_action( template_redirect, redirect_attachment_to_parent, 10 )

function redirect_attachment_to_parent() {
    if ( is_admin()  ( defined( DOING_AJAX )  DOING_AJAX ) ) {
        return
    }

    if ( is_attachment() ) {
        post = get_queried_object()

        if ( post  ! empty( post->post_parent ) ) {
            parent_url = get_permalink( post->post_parent )
            if ( parent_url ) {
                wp_redirect( parent_url, 301 )
                exit
            }
        } else {
            // If no parent, optionally redirect to the file itself
            file = wp_get_attachment_url( post->ID )
            if ( file ) {
                wp_redirect( file, 301 )
                exit
            }
        }
    }
}
?gt

Diagnosing and debugging redirects

  • Use browser dev tools: Network tab will show redirect status codes and locations.
  • Use command line tools: curl -I https://example.com/old-path shows headers and status.
  • Log decisions: during development, temporarily write to error_log() with details (remove later).
  • Check server-level redirects: redirects in .htaccess/nginx config may run before WordPress. Conflicts can cause unexpected behavior.
  • Ensure headers not already sent: calling redirect after output will cause warnings. Use output buffering only if you must — better to fix placement.

Performance considerations

  • Keep redirect logic cheap: avoid slow queries in template_redirect for every page load. Cache mapping lookups in transients or an option, or use a performant mapping table.
  • Prefer webserver redirects for high-traffic, simple host-scheme/path-only redirects (e.g., www/non-www, HTTPS enforcement). They are faster than PHP-level redirects.
  • Use priority parameter to ensure your redirect runs before/after other callbacks as required: add_action( template_redirect, fn, 1 ) for early, higher number to run later.

SEO considerations

  • Use 301 (permanent) when the resource has permanently moved. This signals search engines to update their index and transfer ranking signals.
  • Chain redirects harm crawl budget and SEO. Prefer a single redirect step from the old URL to the final URL.
  • Test with Google Search Console and fetch-as-Google or use a crawler to verify correct behavior and status codes.
  • For many legacy URLs, consider submitting a sitemap of new URLs and removing old ones from index after redirects are in place.

Common pitfalls how to avoid them

  1. Redirect loops: Always compare full normalized URLs before redirecting include scheme, host, path consistency and remove trailing slash differences.
  2. Open redirect vulnerabilities: Validate or allowlist external hosts before redirecting user-supplied URLs. Use wp_safe_redirect for untrusted destinations.
  3. Running in admin or AJAX contexts: Prefix logic with is_admin(), wp_doing_ajax(), wp_doing_cron() checks to prevent interfering with non-frontend requests.
  4. Server-level overrides: If you have .htaccess or nginx rules, they may take precedence. Confirm where redirects are best implemented (server vs app).
  5. Missing exit/die: Not calling exit after wp_redirect may allow further code to run, causing unpredictable results or additional headers/output.

Hook ordering and priority

template_redirect callbacks run in the order of their priority values (default 10). If multiple plugins/themes use template_redirect, set an appropriate priority for ordering: e.g., early enforcement of HTTPS could run at priority 1, while nuanced content-based redirects can run at 10 or 20. Use remove_action if you must disable a previously added redirect.

Example: forcing early precedence

lt?php
// Run at priority 1 (very early) so it happens before most other template_redirect callbacks.
add_action( template_redirect, force_https_redirect, 1 )
?gt

Summary

template_redirect is the correct, flexible place in WordPress to implement 301 (permanent) redirects that depend on the parsed request and WordPress query. Use wp_redirect for internal, trusted targets and wp_safe_redirect for external or potentially untrusted destinations. Always avoid redirect loops, call exit after the redirect, protect admin/AJAX contexts, and consider performance and SEO consequences. The code examples above cover most common use cases — adapt and test them in a staging environment before deploying to production.

Useful official references: the WordPress reference pages for template_redirect, wp_redirect, and wp_safe_redirect.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *