Sometimes you need two or three redirects and installing a whole plugin for it feels like too much. That is fair. A WordPress redirect without a plugin is a few lines of code, and once you know where those lines go, it takes about a minute.
In this post you will learn three ways to do it: PHP inside your site, Apache rules in .htaccess, and Nginx rules in your server config. You will also learn how to redirect to another page programmatically based on conditions, like sending logged out visitors somewhere else.
The code here uses current WordPress functions and works on WordPress 6.x and 7.x. I have used all three methods on live sites.
One warning first. Do not paste redirect code at the top of a template file or anywhere output has already started. PHP can only send a redirect header before the page begins printing, and doing it late gives you the “headers already sent” error. Use the hook I show in method 1 and you will never hit that problem.
Method 1: Redirect with PHP in WordPress
This is the version I reach for first when I need a WordPress redirect without a plugin, because it lives inside WordPress and needs no server access.
Here is the smallest useful example. It sends anyone visiting the old services page to the new one.
add_action( 'template_redirect', 'fs_redirect_old_services' );
`
function fs_redirect_old_services() {
if ( is_page( 'old-services' ) ) {
wp_safe_redirect( home_url( '/services/' ), 301 ); //301 means permanent
exit;
}
}What I did there, line by line. I hooked into template_redirect, which fires after WordPress has figured out what the visitor asked for but before any HTML is printed. That timing is the whole trick. Then is_page( 'old-services' ) checks the slug of the current page. wp_safe_redirect() sends the header, and 301 marks it as permanent so search engines move their ranking to the new URL. The exit stops PHP right there.
That exit is not optional. WordPress will happily keep building the rest of the page if you leave it out, and you get strange behaviour that is hard to debug. The official reference for wp_redirect says the same thing.
Why wp_safe_redirect and not wp_redirect
Both functions take the same arguments: the location, the status code, and an optional label for the X-Redirect-By header.
The difference is safety. wp_redirect() sends the visitor anywhere you tell it, including another domain. If the target ever comes from user input, like a query string, you have just built an open redirect that phishing pages can abuse. wp_safe_redirect validates the host first and falls back to your admin URL when the target is not local.
So the rule I follow: use wp_safe_redirect() by default, and only use wp_redirect() when you are deliberately sending traffic to another site and the URL is hard coded by you.
How do I redirect to another page programmatically?
That first snippet handles one page. Real sites usually have a list. Here is the pattern I use, which only runs when the request would have been a 404 anyway.
add_action( 'template_redirect', 'fs_redirect_map' );
function fs_redirect_map() {
if ( ! is_404() ) {
return; //do nothing on pages that exist
}
$map = array(
'old-services' => '/services/',
'team' => '/about/',
'2019/06/hello-world' => '/blog/hello-world/',
);
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
$path = trim( (string) wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );
if ( isset( $map[ $path ] ) ) {
wp_safe_redirect( home_url( $map[ $path ] ), 301 );
exit;
}
}What is happening here. The early return means the code does nothing on normal page loads, so there is no cost on the 99 percent of requests that are fine. Then I pull the path out of the request with wp_parse_url(), strip the slashes off both ends, and look it up in the map. wp_unslash() is there because WordPress adds slashes to superglobals and you do not want those in your comparison.
Adding a new redirect later means adding one line to the array. That is why I like this shape better than a stack of if statements.
Conditional redirects
The same hook handles logic that a static rule cannot. For example, sending logged out visitors from a members page to the login screen, and back again after they sign in.
add_action( 'template_redirect', 'fs_members_only' );
function fs_members_only() {
if ( is_page( 'members' ) && ! is_user_logged_in() ) {
wp_safe_redirect( wp_login_url( get_permalink() ), 302 ); //302, this is temporary
exit;
}
}Note the 302 there. The visitor is being bounced only because they are logged out, so the redirect is temporary and search engines should not remember it. Getting this backwards is a common mistake. Permanent means the URL moved for good, and browsers cache it hard.
You can swap in any conditional tag: is_single(), is_category(), is_front_page(), or your own check on a query parameter. The template_redirect hook docs explain why this is the right place for all of it.
Where to put this code
Not in the parent theme’s functions.php. The next theme update wipes it.
You have two decent options. A child theme’s functions.php works if you already run one. Better, drop it in a must use plugin, which is a single PHP file in wp-content/mu-plugins/ that WordPress loads automatically and cannot be deactivated by accident.
<?php
/**
* Plugin Name: Site Redirects
* Description: Custom redirect rules for this site.
*/
// paste your redirect functions hereI use the must use plugin on client sites. Redirects are site logic, not theme design, so they should survive a redesign.
Method 2: Redirect in .htaccess on Apache
If your host runs Apache, this is the fastest option, because the request never reaches PHP or your database.
Open .htaccess in your site root and put your rules above the # BEGIN WordPress block. WordPress rewrites everything inside that block whenever permalinks change, and your rules would vanish.
# single URL
Redirect 301 /old-services-page /services
# whole section, with the rest of the path carried over
RedirectMatch 301 ^/blog/(.*)$ /articles/$1Redirect takes an old path and a destination. RedirectMatch takes a regular expression instead, so ^/blog/(.*)$ captures everything after /blog/ and $1 drops it into the target. That second line moves an entire section in one rule.
Back the file up before you touch it. A typo in .htaccess returns a 500 error on every page of the site, and you fix it over FTP with a shaky hand. I have done this at a bad hour and I do not recommend it.
Method 3: Redirect in Nginx
Nginx does not read .htaccess at all, so if you are on Nginx those rules do nothing. Redirects go in your server block, then you reload the config.
location = /old-services-page {
return 301 /services;
}
rewrite ^/blog/(.*)$ /articles/$1 permanent;return 301 is the cheap and direct way for a single URL. rewrite ... permanent handles the pattern case. On managed hosting you often cannot edit this file yourself, so method 1 is your realistic option there.
You might not need a redirect at all
Worth knowing before you write anything. WordPress already tracks old post slugs. If you change a post’s slug, core keeps the old one and redirects it to the new URL on its own. So a single renamed post usually needs zero work from you.
Where it does not help: pages you deleted, permalink structure changes across the whole site, and URLs from an old site you migrated. Those need one of the three methods above.
Which method should you use?
For a handful of rules on a normal site, method 1. The code is version controlled with the rest of your project, it works the same on Apache and Nginx, and you can add conditions that a server rule cannot express.
Move to server level rules when the list gets long or the redirect must run before WordPress boots, like forcing a domain change. The performance gap does not matter at three redirects. It matters at three hundred.
And if you are managing dozens of URLs, hand editing arrays stops being fun. That is the point where a redirect plugin with a 404 log earns its place, even though this post is about avoiding one.
Test it before you walk away
One command tells you if the redirect is live and which code it returned.
curl -I https://example.com/old-services-pageLook for 301 or 302 in the first line and a Location header pointing at the new URL. If PHP handled the redirect you will also see an X-Redirect-By header, which is a handy way to tell whether your code or your server config did the work.
Wrapping up
That is a WordPress redirect without a plugin, three ways: PHP on the template_redirect hook, Apache rules in .htaccess, and Nginx rules in your server block. Start with the PHP method, keep it in a must use plugin, and remember to exit after every redirect.
These snippets use functions that have been stable in core for years, so they will keep working across WordPress updates.
Thanks for reading. If a redirect is looping or not firing on your setup, leave a comment with the code and I will help you trace it. More WordPress and web development posts are on my blog.