Complete .htaccess Redirect Guide for WordPress (2026)

Complete .htaccess Redirect Guide for WordPress (2026)

Last verified: October 17, 2026
21 min read
Reference
500+ WP projects
Core Web Vitals

#How .htaccess redirects work with mod_rewrite

The .htaccess file is one of the most powerful tools in a WordPress administrator’s arsenal. This configuration file allows you to override server settings at the directory level, giving you granular control over URL redirections, access restrictions, and performance optimizations.

#What is mod_rewrite

mod_rewrite is an Apache module that provides a rule-based rewriting engine to rewrite requested URLs on the fly. It’s the engine behind WordPress’s pretty permalinks and is essential for modern SEO-friendly URL structures.

Key benefits:

  • SEO Preservation: Redirect old URLs to new ones without losing search rankings
  • User Experience: Ensure visitors always reach the correct content
  • Security: Force HTTPS and block malicious requests
  • Performance: Implement caching headers and compression

#When to use 301 vs 302 redirects

Understanding HTTP status codes is crucial for proper SEO implementation:

Redirect TypeUse CaseSEO Impact
301 PermanentContent moved permanently, domain changes, HTTPS migrationGoogle uses it as a canonical signal and shows the target URL in results
302 FoundTemporary maintenance, A/B testing, short-term content movesGooglebot follows it, but it is not a canonical signal, so the original URL usually stays in results
307 TemporaryStrict HTTP/1.1 temporary redirect, POST data preservationSame as 302 but preserves HTTP method
308 PermanentStrict HTTP/1.1 permanent redirect, POST data preservationSame as 301 but preserves HTTP method

Golden rule: If the change is permanent, always use 301. With a 302 for a permanent move, the old URL may stay in search results instead of the new one (Redirects and Google Search).


#Common .htaccess 301 redirect examples

#1. Redirect a single page

The simplest form of redirection moves one specific URL to another:

# Redirect a single page (simple syntax)
Redirect 301 /old-page.html https://www.domain.com/new-page.html

# Alternative using RewriteRule
RewriteEngine on
RewriteRule ^old-page\.html$ /new-page.html [R=301,L]

When to use each:

  • Use Redirect 301 for simple, exact URL matches
  • Use RewriteRule when you need pattern matching or conditions

#2. Redirect an entire site to a new domain

Moving your entire website to a new domain requires careful handling:

# Redirect entire site to new domain
Redirect 301 / https://www.newdomain.com/

# Preserve full URL structure
RewriteEngine on
RewriteBase /
RewriteRule (.*) https://www.newdomain.com/$1 [R=301,L]

Important: Always test with a few URLs first before implementing site-wide redirects.

#3. Redirect a subfolder to root (or vice versa)

Common when restructuring content:

# Redirect entire site to a subfolder (skips URLs already in it, so it does not loop)
RedirectMatch 301 ^/(?!subfolder/)(.*)$ https://www.domain.com/subfolder/$1

# Redirect subfolder to another site (trailing slash on both sides avoids a double slash)
Redirect 301 /subfolder/ https://www.domain.com/

# Using RewriteRule with full path preservation
RewriteEngine on
RewriteRule ^subfolder/(.*)$ /$1 [R=301,L]

A plain Redirect 301 / https://www.domain.com/subfolder/ on the same host loops: Redirect is a prefix match, and the target path also starts with /. And because Redirect appends the path after the matched part, Redirect 301 /subfolder https://www.domain.com/ would turn /subfolder/page into https://www.domain.com//page (mod_alias).

#4. Redirect .html to .php and remove extensions

When migrating from .html to .php or implementing pretty permalinks:

# Redirect .html to .php
RedirectMatch 301 (.*)\.html$ https://www.domain.com$1.php

# Remove .php extension (make URLs cleaner)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^([^\.]+)$ $1.php [NC,L]

#5. Force HTTPS (SSL)

Essential for security and SEO:

# Force HTTPS - Method 1 (simple)
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# Force HTTPS - Method 2 (with www handling)
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.
RewriteRule ^(.*)$ https://www.domain.com/$1 [L,R=301]

#6. Force www or non-www

Consistency in URL structure matters for SEO:

# Force www
RewriteEngine on
RewriteCond %{HTTP_HOST} ^domain.com [NC]
RewriteRule ^(.*)$ https://www.domain.com/$1 [L,R=301]

# Force non-www (naked domain)
RewriteEngine on
RewriteCond %{HTTP_HOST} ^www\.domain\.com [NC]
RewriteRule ^(.*)$ https://domain.com/$1 [L,R=301]

#RewriteRule 301 redirects with query strings and regex

#How to redirect URLs with query strings

Handling URLs with parameters requires special attention:

# Redirect specific query parameter
RewriteEngine on
RewriteCond %{QUERY_STRING} (^|&)id=1(&|$)
RewriteRule ^index\.php$ /path-to-new-location/? [R=301,L]

# Map a parameter value into the path
RewriteEngine On
RewriteCond %{QUERY_STRING} ^category=([^&]+)
RewriteRule ^index\.php$ /category/%1/? [R=301,L]

# Remove query strings entirely
RewriteEngine On
RewriteCond %{QUERY_STRING} .
RewriteRule ^$ /? [R=301,L]

Note: The trailing ? in the destination URL strips the original query string. Since Apache 2.4.0 the [QSD] flag does the same job (RewriteRule flags). A bare id=1 condition would also match id=10, id=123 and pid=1; the (^|&) and (&|$) anchors limit it to the exact parameter.

#Regex pattern matching in RewriteRule

Regular expressions provide powerful pattern matching:

# Redirect date-based URLs
RewriteEngine On
RewriteRule ^blog/([0-9]{4})/([0-9]{2})/(.+)$ /news/$1/$2/$3 [R=301,L]

# Redirect with wildcards
RewriteEngine On
RewriteRule ^products/(.*)$ /shop/$1 [R=301,L]

# Multiple pattern matches
RewriteEngine On
RewriteRule ^(blog|news|articles)/(.+)$ /content/$2 [R=301,L]

#Domain change with path preservation

When migrating to a completely new domain:

# Old domain to new domain with full path
RewriteEngine On
RewriteCond %{HTTP_HOST} ^old-domain.com$ [OR]
RewriteCond %{HTTP_HOST} ^www.old-domain.com$
RewriteRule (.*)$ https://www.new-domain.com/$1 [R=301,L]

# With subdirectory adjustment
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/blog/(.*)$
RewriteRule ^(.*)$ https://www.newdomain.com/news/%1 [R=301,L]

#How to test .htaccess redirects before going live

#What to check before a redirect goes live

  1. Use 302 Temporary First: Test with 302 redirects before making them permanent
  2. Check Redirect Chains: Ensure you’re not creating redirect loops
  3. Test Multiple Scenarios: Check with and without www, HTTP and HTTPS
  4. Validate with Tools: Use online redirect checkers

#Test a 301 redirect with curl

# Using curl to check headers
curl -I http://yourdomain.com/old-page

# Follow redirects with curl
curl -L http://yourdomain.com/old-page

# Check specific status codes
curl -I -L http://yourdomain.com/old-page 2>&1 | grep HTTP

#Online redirect checker tools

  • Redirect checker: httpstatus.io
  • Google Search Console: Check for crawl errors after implementation

#Troubleshooting redirect loops

#What causes a redirect loop

  1. Circular References: Rule A redirects to B, Rule B redirects back to A
  2. Protocol Mismatches: HTTP rule conflicting with HTTPS forcing
  3. WWW Conflicts: www and non-www rules fighting each other
  4. WordPress Conflicts: Plugin redirects conflicting with .htaccess

#Log mod_rewrite to find the loop

# Server or virtual host config: LogLevel is not allowed in .htaccess
LogLevel alert rewrite:trace3

RewriteLog and RewriteLogLevel were removed in Apache 2.4 and replaced by the per-module LogLevel setting (mod_rewrite). The same page warns that levels above trace2 slow the server dramatically, so use them only for debugging and remove the line afterwards. To see only the rewrite entries: tail -f error_log | fgrep '[rewrite:'.

#How to stop a redirect loop in .htaccess

# Prevent loops by checking if already redirected
RewriteEngine On
RewriteCond %{ENV:REDIRECT_STATUS} 200
RewriteRule ^ - [L]

# Specific protocol handling to avoid loops
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

#How Google treats 301, 302, 307 and 308 redirects

Google’s documentation on redirects (Redirects and Google Search) splits redirects into two groups. Permanent redirects (301 and 308) are followed by Googlebot, and the indexing pipeline uses them as a signal that the target should be canonical, so the new URL is what appears in results. Temporary redirects (302, 303 and 307) are followed too, but they are not used as a canonical signal, so the source URL usually stays in results. Google also ranks the methods by how reliably it can interpret them: a server-side redirect such as one from .htaccess comes first, ahead of meta refresh and JavaScript.

This matters for .htaccess because both Apache modules default to the temporary code. Redirect without a status argument sends a 302 (mod_alias), and RewriteRule with a bare [R] flag also sends a 302 (RewriteRule flags). A migration written with [R,L] instead of [R=301,L] ships temporary redirects without any error message.

Chains have a hard limit too. Google’s crawlers follow up to 10 redirect hops by default (HTTP status codes), and the site-move guide asks for no more than 3 hops and fewer than 5 at most, because each hop adds latency (site moves with URL changes). The same guide says to keep migration redirects for as long as possible, generally at least one year.

#Why a RewriteRule with a leading slash does not match in .htaccess

The most common reason a rule copied from a server config does nothing in .htaccess is the leading slash. In per-directory context (.htaccess and <Directory>), Apache strips the directory path, up to and including the trailing slash, before it compares the pattern. The mod_rewrite documentation states it directly: the string never has a leading slash, so a pattern starting with ^/ never matches in per-directory context.

# Virtual host config: matches /old-page/
RewriteRule ^/old-page/$ /new-page/ [R=301,L]

# .htaccess in the document root: same redirect
RewriteRule ^old-page/$ /new-page/ [R=301,L]

The pattern also never sees the host name or the query string. To test those, use a RewriteCond on %{HTTP_HOST} or %{QUERY_STRING}, and use %{REQUEST_URI} when you need the full path. Two server-side settings decide whether the rules run at all: rewriting in .htaccess needs Options FollowSymLinks or SymLinksIfOwnerMatch, and both RewriteRule and Redirect need AllowOverride FileInfo. The Apache .htaccess tutorial gives a quick test: put a nonsense word such as TestMe in the file and reload the page. If there is no 500 error, AllowOverride None is in effect and Apache ignores the file.

#Difference between the L and END flags in .htaccess

In .htaccess, [L] does not mean “stop for good”. The flags documentation explains that in per-directory context [L] ends the current pass through the rules, but the rewritten request may be processed again from the top, which can cause loops. [END] stops all further rewrite processing for the request, and the documentation calls it the preferred flag for most per-directory rules. In server or virtual host context the two behave the same.

Three more details from the same page explain many broken redirects:

  • [R] without [L] passes the rewritten URL to the next rule, which can produce “Invalid URI in request” warnings, so write [R=301,L].
  • With a status outside 300 to 399, for example [R=410], the substitution is dropped and the target URL is never sent. [G] and [F] express that intent more clearly.
  • [QSD] (Apache 2.4.0 and later) discards the query string, and when [QSD] and [QSA] are combined, [QSD] wins.

#When Apache recommends Redirect instead of RewriteRule

The Apache team’s own guide, When not to use mod_rewrite, calls mod_rewrite a last resort and says simple redirects of one URL or a class of URLs should use Redirect or RedirectMatch. Redirect keeps path information, so Redirect "/one/" "http://one.example.com/" also moves /one/two.html. It matches only complete path segments: /service does not catch /servicefoo.txt. It passes GET parameters to the target, and POST bodies are discarded (mod_alias).

For a canonical host name the same guide uses an <If> block instead of RewriteCond:

<If "req('Host') != 'www.example.com'">
    Redirect "/" "https://www.example.com/"
</If>

It also notes that the front-controller pattern WordPress writes into .htaccess (route every non-existent file and directory to index.php) can be expressed as FallbackResource /index.php, which works in .htaccess too. In a WordPress install, leave the generated block alone, because WordPress rewrites everything between # BEGIN WordPress and # END WordPress. The practical rule is to keep one module per URL: do not let a Redirect and a RewriteRule both match the same path.

#Where to store thousands of redirects instead of .htaccess

A long redirect list in .htaccess is paid for on every request. According to the .htaccess tutorial, when .htaccess files are allowed, httpd looks for them in every directory and in all parent directories, and reloads the file each time a document is requested, including images, CSS and JavaScript. The same page recommends the main server configuration whenever you have access to it.

Apache’s answer for large mappings is RewriteMap. It can only be declared in server or virtual host context, never in .htaccess (RewriteMap guide). A plain-text map holds one space-separated key and value per line, and httpd caches the looked-up keys until the file’s modification time changes. A dbm: map built with httxt2dbm is indexed, so lookups are faster than in a text file:

# Virtual host config, not .htaccess
RewriteEngine On
RewriteMap redirects "dbm:/etc/apache2/redirects.map"
RewriteCond "${redirects:%{REQUEST_URI}}" !=""
RewriteRule "^" "${redirects:%{REQUEST_URI}}" [R=301,L]
# redirects.txt: one "old-path new-path" pair per line
# /old-page/ /new-page/
httxt2dbm -i redirects.txt -o redirects.map

The condition skips the redirect when the path is not in the map, so unmapped URLs fall through to WordPress as usual.


#Performance impact of .htaccess rules

#What .htaccess costs on every request

The cost starts before the first rule runs. When AllowOverride allows .htaccess files, httpd looks for one in every directory and in all parent directories on every request, and loads the file each time a document is requested. The .htaccess tutorial gives an example: a request for a file in /www/htdocs/example costs four extra file-system lookups. Redirect chains add their own cost: Google’s site-move guide says chaining adds latency for users and asks for ideally no more than 3 hops and fewer than 5 (site moves with URL changes).

#Rule order and regex for faster redirects

  1. Order Matters: Place most frequently matched rules first
  2. Use Specific Patterns: More specific patterns are faster
  3. Limit Regex Complexity: Avoid greedy quantifiers where possible
  4. Consider Server-Level Config: For high-traffic sites, use httpd.conf instead
# Optimized ordering example
RewriteEngine On

# Most common redirects first
RewriteRule ^wp-admin$ /wp-admin/ [R=301,L]

# Then specific patterns
RewriteRule ^old-product/(.*)$ /shop/$1 [R=301,L]

# Generic patterns last
RewriteRule ^blog/(.*)$ /news/$1 [R=301,L]

#Enable mod_expires and compression

# Enable mod_expires for better caching
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresDefault "access plus 1 month"
</IfModule>

# Enable compression
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/css application/javascript
</IfModule>

#WordPress .htaccess 301 redirect rules

#Custom redirects before the WordPress block

WordPress uses its own rewrite rules in .htaccess. Always place custom rules before the WordPress block:

# BEGIN Custom Redirects
RewriteEngine On
# Your custom rules here
# END Custom Redirects

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

#Redirect attachment, author and category archives

# Redirect attachment pages to the homepage
RewriteEngine On
RewriteRule ^attachment/(.*)$ / [R=301,L]

# Redirect author archives to homepage
RewriteEngine On
RewriteRule ^author/(.*)$ / [R=301,L]

# Redirect category archives to specific page
RewriteEngine On
RewriteRule ^category/old-category$ /new-section/ [R=301,L]

#Protect wp-config.php and block malicious requests

#Protect sensitive WordPress files

# Block access to sensitive WordPress files
<FilesMatch "^\.">
  Require all denied
</FilesMatch>

# Protect wp-config.php
<Files wp-config.php>
  Require all denied
</Files>

# Block access to .htaccess itself
<Files .htaccess>
  Require all denied
</Files>

Older guides use Order allow,deny and Deny from all. That is Apache 2.2 syntax: in 2.4 it only works through mod_access_compat, and the upgrade guide discourages it. Require all denied is the 2.4 form.

#Block malicious query strings in .htaccess

# Block SQL injection attempts
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} GLOBALS(=|\[|\%[0-9A-Z]{0,2}) [OR]
RewriteCond %{QUERY_STRING} _REQUEST(=|\[|\%[0-9A-Z]{0,2})
RewriteRule ^(.*)$ index.php [F,L]

#Redirect vs RedirectMatch vs RewriteRule syntax

#Redirect directive syntax

# Basic 301 redirect
Redirect 301 /old /new

# 302 temporary redirect
Redirect 302 /maintenance /temp-page

# Redirect with full URL
Redirect 301 /page http://example.com/new-page

#RedirectMatch syntax

RedirectMatch (Apache mod_alias) works like Redirect, but takes a regular expression for the path:

# Pattern-based redirect
RedirectMatch 301 ^/blog/(.*)$ http://example.com/news/$1

# Case-insensitive
RedirectMatch 301 (?i)^/old-folder/(.*)$ /new-folder/$1

#RewriteRule syntax

# Basic structure
RewriteRule Pattern Substitution [Flags]

# Common flags:
# [R=301] - Permanent redirect
# [R=302] - Temporary redirect
# [L] - Last rule (stop processing)
# [NC] - No case (case-insensitive)
# [QSA] - Query string append
# [NE] - No escape

#.htaccess redirect checklist

Before implementing redirects:

  • Test redirects with 302 before making them 301
  • Verify no redirect loops exist
  • Check mobile and desktop behavior
  • Test with both www and non-www URLs
  • Verify HTTPS enforcement if applicable
  • Monitor Google Search Console for crawl errors
  • Update internal links to point to new URLs
  • Create a backup of your .htaccess file

Remember: a well-planned redirect map preserves your SEO rankings and prevents the bounce-rate spike that comes with users hitting a 404 during a migration or restructure.


#.htaccess redirects on Nginx, logging and plugins

#How to convert .htaccess redirects to Nginx

While .htaccess is Apache-specific, understanding how rules translate to NGINX is crucial for developers working across platforms:

Apache (.htaccess) to NGINX Conversion:

Apache DirectiveNGINX Equivalent
Redirect 301 /old /newreturn 301 /new;
RewriteRule ^old$ /new [R=301,L]location = /old { return 301 /new; }
RewriteCond %{HTTPS} offif ($scheme != 'https')

NGINX server block example:

server {
    listen 80;
    server_name example.com;

    # Permanent redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    # WordPress rewrite rules
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

#Redirects by language, device, time and load balancer

1. Multilingual Site Redirects

# Redirect based on Accept-Language header
RewriteEngine On
RewriteCond %{HTTP:Accept-Language} ^de [NC]
RewriteRule ^$ /de/ [L,R=302]

RewriteCond %{HTTP:Accept-Language} ^fr [NC]
RewriteRule ^$ /fr/ [L,R=302]

Google advises against redirecting users automatically to a language version based on what you think their language is, because it can stop users and search engines from reaching all versions of the site. Link to the other language versions instead (managing multi-regional sites).

2. Mobile Device Detection

# Detect mobile user agent
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge\ |maemo|midp|mmp|mobile.+firefox|netfront|opera\ m(ob|in)i|palm\ (os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows\ ce|xda|xiino [NC,OR]
RewriteCond %{HTTP_USER_AGENT} ^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a\ wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|l\ |nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1\ u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g\ |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/ [NC]
RewriteRule ^$ /mobile/ [L,R=302]

3. Time-Based Redirects

# Redirect during maintenance window
RewriteEngine On
RewriteCond %{TIME_HOUR}%{TIME_MIN} >0800 [NC]
RewriteCond %{TIME_HOUR}%{TIME_MIN} <1700 [NC]
RewriteRule ^$ /maintenance/ [L,R=302]

4. HTTP/HTTPS Detection for Load Balancers

# Handle X-Forwarded-Proto header from load balancers
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

#Debug .htaccess with rewrite logging

Enable detailed rewrite logging:

# Virtual host config, not .htaccess (Apache 2.4+)
LogLevel alert rewrite:trace3

# Log to file
ErrorLog ${APACHE_LOG_DIR}/rewrite.log

Common debugging commands:

# Check whether mod_rewrite is loaded
apache2ctl -M | grep rewrite

# Test the main server config (it does not read .htaccess)
apache2ctl configtest

# Enable mod_rewrite (Debian/Ubuntu), then reload Apache
a2enmod rewrite

# View Apache error logs
tail -f /var/log/apache2/error.log

apache2ctl configtest checks only the main configuration. Apache reads .htaccess on each request (.htaccess tutorial), so an error in that file shows up as a 500 error when a page is requested.

#Migration checklist for large sites

When migrating a large WordPress site with thousands of redirects:

  1. Inventory Phase

    • Export all URLs from Google Analytics
    • Crawl existing site with Screaming Frog
    • Export all indexed pages from Google Search Console
    • Document all 404 errors
  2. Planning Phase

    • Map old URLs to new URLs
    • Group redirects by pattern
    • Prioritize high-traffic pages
    • Plan redirect chains to minimize hops
  3. Implementation Phase

    • Test redirects on staging environment
    • Implement in batches
    • Monitor server response times
    • Check for redirect loops
  4. Verification Phase

    • Use online redirect checkers
    • Monitor Search Console for crawl errors
    • Check server response times
    • Check that the new URLs replace the old ones in search results
  5. Cleanup Phase

    • Remove temporary 302 redirects (convert to 301)
    • Remove unnecessary rules
    • Document all redirects for future reference
    • Update XML sitemap with new URLs

#WordPress plugin alternatives

For non-technical users or complex redirect management:

Redirection

  • Free, widely used
  • Supports regex patterns
  • 404 logging
  • Import/export functionality

Yoast SEO Premium

  • Built-in redirect manager
  • Automatic 404 monitoring
  • Tight WordPress integration with no extra plugin glue
  • Regex support

Safe Redirect Manager

  • WordPress.org plugin
  • Whitelist-based redirects
  • HTTP status code selection
  • Multisite compatible

Rank Math SEO

  • Comprehensive SEO suite
  • 301/302/307 redirects
  • 404 monitoring
  • 301 bulk import

Conclusion

Mastering .htaccess redirects is essential for any WordPress developer or site administrator. The key takeaways are:

  1. Always use 301 for permanent moves to preserve SEO value
  2. Test thoroughly before deployment using 302 redirects first
  3. Avoid redirect chains that create unnecessary hops
  4. Order rules logically with most specific patterns first
  5. Monitor performance as complex rules can impact page load times
  6. Document all redirects for future maintenance and troubleshooting

With the techniques and examples in this guide, you can confidently handle any redirect scenario, from simple page moves to complex multi-domain migrations.


#Default WordPress .htaccess and custom rules

#Default WordPress .htaccess file

WordPress generates a default .htaccess file when you enable pretty permalinks. Understanding its structure is crucial for adding custom rules:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

#Where to add custom rules without breaking WordPress

When adding custom rules, always place them before the WordPress rules:

# BEGIN Custom Rules
Redirect 301 /old-page /new-page
# END Custom Rules

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

#WordPress Multisite .htaccess rules

WordPress Multisite requires additional configuration:

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]

#Block directory browsing and restrict wp-admin by IP

#Block wp-config.php, .htaccess and directory browsing

# Block access to wp-config.php
<Files wp-config.php>
Require all denied
</Files>

# Block access to .htaccess itself
<Files .htaccess>
Require all denied
</Files>

# Block directory browsing
Options -Indexes

#IP-based access control

<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_URI} ^(.*)?wp-admin(.*)? [OR]
RewriteCond %{REQUEST_URI} ^(.*)?wp-login\.php(.*)?
RewriteCond %{REMOTE_ADDR} !^192\.168\.1\.100$
RewriteRule .* https://%{HTTP_HOST}/ [R=301,L]
</IfModule>

#Browser caching and GZIP rules in .htaccess

#Browser caching headers

<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType text/javascript "access plus 1 month"
</IfModule>

# GZIP Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript
</IfModule>

#Redirects for HTTPS, domain and URL structure migrations

#HTTP to HTTPS migration

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

#Domain migration

RewriteEngine On
RewriteCond %{HTTP_HOST} ^old-domain\.com$ [NC]
RewriteRule (.*)$ https://www.new-domain.com/$1 [R=301,L]

#URL structure changes

RewriteEngine On
RewriteRule ^blog/([0-9]{4})/([0-9]{2})/(.+)$ /articles/$3 [R=301,L]

#Mobile redirects

RewriteCond %{HTTP_USER_AGENT} (android|bb\d+|meego)|mobile|up\.browser [NC]
RewriteRule ^(.*)$ https://mobile.example.com/$1 [R=301,L]
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https://(www\.)?yourdomain\.com [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp)$ - [F]

The !^$ condition lets requests with no Referer through, so direct visits and browsers that suppress the header are not blocked. The Referer header is optional and can be spoofed (When not to use mod_rewrite), so this rule cannot serve as access control.

#Force file downloads

<FilesMatch "\.pdf$">
Header set Content-Disposition attachment
</FilesMatch>

Explore our WordPress speed optimization to take your project further.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Frequently asked questions

Practical answers to apply the topic in real execution.

When should you use RedirectMatch in .htaccess?#
RedirectMatch comes from Apache mod_alias and matches the URL path against a regular expression. Use it for pattern-based 301 redirects that depend only on the path. When the rule has to look at the query string, the host or a header, you need mod_rewrite with RewriteCond.
What's the difference between Redirect 301 and RewriteRule?#
Redirect 301 is simpler syntax for basic redirects. RewriteRule uses regex pattern matching and is more powerful for complex URL manipulations. Use Redirect for simple cases, RewriteRule for pattern-based redirects.
Why am I getting a redirect loop error?#
Redirect loops occur when rules reference each other (A redirects to B, B redirects to A) or when protocol mismatches exist. Check for conflicting rules and use RewriteCond to prevent loops.
Do .htaccess redirects affect website speed?#
Minimal impact for a few redirects. However, performance degrades with many RewriteRule directives because Apache processes them sequentially for every request. Consider implementing redirects at the server config level for high-traffic sites.
Should I use 301 or 302 redirect?#
Use 301 for permanent changes (domain moves, HTTPS migration, permanent URL changes): Google uses it as a signal that the new URL is canonical and shows the new URL in results. Use 302 only for temporary situations like maintenance or A/B testing: Google follows it, but the old URL usually stays in results.
How do I redirect with query strings?#
Use RewriteCond %{QUERY_STRING} to match query strings, then RewriteRule with [R=301,L,QSD] flags. The QSD flag (Query String Discard, Apache 2.4.0 and later) removes the original query string from the destination.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles