As a website grows, its URL structure may need to change for a new framework, cleaner navigation or reorganised categories. This requires more than rewrite rules: old URLs need appropriate destinations, and internal links and indexing signals need updating. Careful planning reduces the risk of broken links and lost organic traffic, but does not guarantee unchanged rankings.
This article focuses on the practical middle ground: how to change URL structures while reducing SEO risk, using clean 301 redirects on both Apache (.htaccess) and Nginx. We will look at the SEO rules that really matter, the most common redirect patterns (HTTP→HTTPS, non‑www→www, trailing slash, moved folders, removed file extensions), and how to translate them into working configuration examples. We will also cover how to test, debug and clean up once your changes go live, so you can identify migration problems and respond promptly.
Why URL Structure Changes Matter for SEO
Folder names alone do not make a URL migration successful; consistency, crawlability and canonical signals matter. When you change URL structures, search engines need to associate the old addresses with the new ones. That can be positive or negative depending on how you handle redirects and internal links.
From an SEO and hosting perspective, three things matter most:
- Status codes: 301 and 308 signal permanent moves; 302 and 307 indicate temporary moves. Return 404/410 for removed content without a relevant replacement. Our HTTP status code guide explains the distinction.
- Direct destinations: Multiple hops add latency and make redirect paths harder to maintain. Send old URLs directly to the final matching content where possible.
- Consistent signals: Choose the canonical host, scheme and page-URL style, then align links, canonical annotations, language alternates and sitemaps.
These principles reduce migration risk, but complete URL mapping, testing and post-launch monitoring are still necessary.
Planning an SEO‑Safe URL Migration
Before we touch .htaccess or Nginx, we need a plan. Missing or incomplete mappings can leave old URLs without appropriate destinations even when the redirect syntax is correct.
Step 1: Inventory your current URLs
Export as many existing URLs as you can:
- From your CMS (posts, pages, categories, products)
- From your web server logs (high‑traffic URLs over the last 6–12 months)
- From tools like Search Console and crawlers
Prioritise URLs that already receive organic traffic or backlinks. Map each to its moved equivalent where one exists; deleted content without an appropriate replacement should return 404 or 410 rather than redirecting to an unrelated page.
Step 2: Define the new structure and mapping table
Create a spreadsheet with at least two columns:
- Old URL path (e.g. /blog/2022/09/seo-tips.html)
- New URL path (e.g. /blog/seo-tips/)
Whenever possible, use predictable patterns instead of one‑off rules. For example, if all your blog posts will move from /blog/YYYY/MM/slug/ to /blog/slug/, you can cover hundreds of URLs with a single regex redirect, provided slugs remain unique and every destination exists. Handle exceptions with explicit mappings.
Step 3: Decide on your canonical rules
Decide once and apply everywhere:
- Will you use www or bare domain as canonical?
- Will page URLs end with or without trailing slash?
- Will you show file extensions in URLs (.php, .html) or hide them?
- Are you enforcing HTTPS everywhere?
Once you decide, your redirect configuration should funnel every variant into a single canonical version (for example, http://example.com/blog to https://www.example.com/blog/).
Step 4: Test on staging if you can
If your hosting setup includes dev or staging environments (for example on a separate VPS or subdomain), deploy your URL changes there first. That is the safest way to catch redirect loops and broken patterns before they affect real users. If you do not yet have a multi‑environment workflow, our article on hosting architecture for dev, staging and production environments is a good place to start.
301 Redirect Patterns in Apache .htaccess
The following are alternative patterns for Apache 2.4 in the document-root .htaccess file. They require mod_rewrite, suitable AllowOverride permissions and FollowSymLinks or SymLinksIfOwnerMatch enabled by the administrator. Adapt one consistent set; do not paste opposite canonicalisation recipes together. These examples target GET/HEAD page navigation: a 301 can result in a POST becoming GET, so method-sensitive APIs and forms need a separate migration decision.
Basic one‑to‑one 301 redirect
For a small number of changed URLs, keep it explicit and readable.
RewriteEngine On
RewriteRule ^blog/2022/09/seo-tips\.html$ https://www.example.com/blog/seo-tips/ [R=301,L]
RewriteRule ^shop/product-123\.html$ https://www.example.com/store/new-product-slug/ [R=301,L]
Notes:
- These anchored RewriteRule patterns match the complete relative path in the document-root .htaccess context; they do not match a suffix or child path. The target host is fixed. Query strings are retained unless explicitly replaced or discarded.
- mod_alias Redirect matches a path prefix and appends additional path information, so Redirect /old /new is not an exact-match recipe. RedirectMatch can anchor a regex, but mixing modules still requires understanding their execution phases.
- Within the same mod_rewrite ruleset, put specific mappings before broader rules and the application front controller. Textual placement of Redirect lines does not control ordering against mod_rewrite. Keep CMS-generated rule blocks intact.
HTTP to HTTPS for one named host
The HTTPS migration guide covers certificates and rollout. The example below assumes Apache itself terminates the public TLS connection, with the virtual host restricted to example.com. A trusted edge proxy should normally perform public HTTPS redirects itself. If Apache receives HTTP behind TLS termination, its local HTTPS=off state does not mean the visitor used HTTP. Do not trust a client-supplied X-Forwarded-Proto header; any forwarded-scheme design needs an origin reachable only through trusted proxies that overwrite that header.
RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteCond %{HTTPS} off
RewriteCond %{THE_REQUEST} "\s(/[^?\s]*)(?:\?|\s)"
RewriteRule ^ https://example.com%1 [R=301,L,NE]
The host is explicitly allowlisted and the destination host is fixed. THE_REQUEST contains the original, undecoded request line; the final condition captures its path without the query, while the default RewriteRule behavior copies the query. NE avoids double-encoding this already encoded path. This origin-form recipe is not equivalent to appending decoded REQUEST_URI with NE. Test encoded spaces, percent signs, question marks, Unicode and encoded-slash policy on the actual server; do not promise byte preservation for arbitrary other rewrite substitutions. See the Apache rewrite flags reference for encoding and processing details.
Non‑www to www (or the reverse)
Pick one as canonical. Example: redirect non‑www to www.
RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteCond %{THE_REQUEST} "\s(/[^?\s]*)(?:\?|\s)"
RewriteRule ^ https://www.example.com%1 [R=301,L,NE]
To go the other way (www → non‑www), invert the condition and target host:
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteCond %{THE_REQUEST} "\s(/[^?\s]*)(?:\?|\s)"
RewriteRule ^ https://example.com%1 [R=301,L,NE]
Combining HTTPS and host canonicalisation
For direct Apache TLS termination, combine the host and scheme checks while retaining an explicit allowed-host condition. The virtual-host/default-host configuration must reject unrelated hosts. If a reverse proxy terminates TLS, implement the public scheme policy there or use a separately validated trusted-proxy design; this local HTTPS check must not be copied unchanged.
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(?:www\.)?example\.com$ [NC]
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.example\.com$ [NC]
RewriteCond %{THE_REQUEST} "\s(/[^?\s]*)(?:\?|\s)"
RewriteRule ^ https://www.example.com%1 [R=301,L,NE]
For the two named hosts on their standard public ports, this sends HTTP bare, HTTP www and HTTPS bare requests to HTTPS www in one hop. HTTPS connections require a valid certificate for the requested name before a redirect can be read. The raw-path capture and NE caveats above apply. Explicit old-to-new content mappings should precede a general canonical rule when both would otherwise redirect the same request.
Removing or adding a trailing slash
Choose one style for application page URLs. The following addition rule excludes existing files and directories and uses a limited 2–6-character extension heuristic; it is not a complete filename detector. Apply it only where that policy matches your routes. One-character routes such as /a are included.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !\.[a-zA-Z0-9]{2,6}$
RewriteCond %{THE_REQUEST} "\s(/[^?\s]*)(?:\?|\s)"
RewriteRule ^(.*[^/])$ https://www.example.com%1/ [R=301,L,NE]
The alternative removal rule also excludes physical directories. Apache mod_dir normally adds a slash to real directories; removing it in another rule can create a loop. Do not disable DirectorySlash as a general workaround.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{THE_REQUEST} "\s(/[^?\s]+)/(?=\?|\s)"
RewriteRule ^(.+)/$ https://www.example.com%1 [R=301,L,NE]
Moving a whole folder (category change)
Suppose /blog/ is being renamed to /articles/ and all posts keep the same slug.
RewriteEngine On
RewriteCond %{THE_REQUEST} "\s/blog/([^?\s]*)(?:\?|\s)"
RewriteRule ^blog/ https://www.example.com/articles/%1 [R=301,L,NE]
The raw request-line capture includes the suffix after /blog/; the query is copied separately by RewriteRule. The target must serve equivalent content. Test the rule alongside host/scheme canonicalisation, slash rules and any earlier internal rewrite; an encoded alternative to the literal /blog/ prefix needs its own decision.
Removing .php file extensions
This example hides .php for existing, top-level PHP files with ASCII letter, digit, underscore or hyphen basenames. It assumes an already configured PHP handler, MultiViews disabled and no conflicting front-controller rules. It does not try to infer every application route or nested script path.
- Redirect only a client request that explicitly names an existing .php file. THE_REQUEST remains the original request line during internal rewrites.
- Then internally route an extensionless path only when no file or directory already exists and the corresponding PHP file does exist. END stops later per-directory rewrite rounds.
Options -MultiViews
RewriteEngine On
RewriteCond %{THE_REQUEST} "\s/+[A-Za-z0-9_-]+\.php(?:\?|\s)"
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^([A-Za-z0-9_-]+)\.php$ https://www.example.com/$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^([A-Za-z0-9_-]+)$ $1.php [END]
With page.php present, /page.php redirects to /page, while /page executes page.php through the existing PHP handler without redirecting back. Queries are retained. Options -MultiViews requires the corresponding server permission; have the administrator set it if .htaccess cannot. Existing static files and directories are excluded. This is not a WordPress/Laravel front-controller configuration.
Handling query strings safely
For a RewriteRule target without a new query, the existing query is copied by default. QSD discards it; a replacement query normally replaces it, while QSA combines queries. The following campaign example intentionally drops every parameter once utm_campaign=old-promo matches, so confirm that no discarded parameter changes the page’s required behavior.
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)utm_campaign=old-promo(&|$)
RewriteRule ^landing\.php$ https://www.example.com/new-landing/ [R=301,L,QSD]
QSD is available in Apache 2.4. This pattern matches a literal query component; encoded names, repeated parameters and application-specific interpretation need a separate rule. The target does not receive the original query.
When to use 410 (Gone) instead of 301
When content is permanently removed and has no relevant replacement, return 404 or 410. Do not redirect it to an unrelated home page, and do not return 410 merely to exclude a still-useful page from search. This exact path example uses the same module as the other rules:
RewriteEngine On
RewriteRule ^old-section/bad-content\.html$ - [G,L]
For more on deciding between 404, 410 and 301, again see our article on HTTP status codes and their SEO impact.
301 Redirect Patterns in Nginx
On VPS, dedicated servers and other environments where you manage Nginx, redirects belong in server configuration rather than .htaccess. sites-available/sites-enabled is one packaging convention; inspect the actual include paths. Configuration access depends on the service plan. Validate syntax before a controlled reload; this alone does not verify redirect behavior.
Basics: return vs rewrite
- return: Finishes processing with a status and target; useful for fixed destinations and variables. It does not automatically append the original query.
- rewrite: Changes a URI using a regex. Flags determine whether it redirects or continues internal processing; changed URI and query handling must be considered.
- Location selection: Exact matches stop the search; regex order matters. An added regex location can bypass the intended PHP handler. Prefer server-level maps/returns or carefully scoped exact locations for these examples.
HTTP to HTTPS redirect server
Common pattern: one server block for HTTP that only redirects, and another for HTTPS that serves content.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
This HTTP-only block combines scheme and host canonicalisation into a fixed HTTPS www target. It does not serve HTTPS or cover an existing HTTPS bare-domain entry point; the next example adds both TLS blocks. Configure default virtual hosts to reject unrelated hostnames.
Non‑www to www (or reverse) on Nginx
This complete routing skeleton assumes Nginx itself terminates TLS. Install a certificate whose SAN covers both example.com and www.example.com, supply its full chain and protected key at the shown paths, and set the actual document root. A wildcard for *.example.com alone does not cover example.com. The final block serves static content only; replace its content handler with the validated application configuration when PHP or a framework is involved.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/tls/example/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
ssl_certificate /etc/nginx/tls/example/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/example/public;
location / {
try_files $uri $uri/ =404;
}
}
To choose bare-domain canonical URLs, swap the two HTTPS host roles and use https://example.com in all return targets. Both public names still need valid TLS coverage. A certificate error occurs before the HTTP redirect and cannot be repaired by it. Behind a TLS-terminating proxy, the trusted edge must own the public scheme decision.
Folder move with regex
Use a raw-request map to move the literal /blog/ prefix while preserving the encoded suffix and query. Put map in the http context and the if/return fragment inside the existing canonical server block. These are two insertion locations, not a complete standalone Nginx configuration.
# http context, outside server blocks
map $request_uri $legacy_blog_target {
default "";
~^/blog/(?<legacy_blog_suffix>.*)$ https://www.example.com/articles/$legacy_blog_suffix;
}
# Inside the existing canonical server block
if ($legacy_blog_target != "") {
return 301 $legacy_blog_target;
}
The captured raw suffix already contains any query, so adding $is_args$args would duplicate it. The map is case-sensitive and does not match /Blog/ or an encoded variant of the /blog/ prefix. Map those intentionally if they existed. Apply final content mappings before any broader return that would introduce another hop; preserve the normal application locations for requests that do not match.
Adding or removing trailing slash
A generic regex location for every extensionless path can intercept PHP, static files or application routing. Instead, this example lists known virtual page routes. Use it only where /about and /help are not physical files/directories and the application supports the slash targets. The map belongs in http, and the return fragment in the canonical server:
map $uri $page_slash_target {
default "";
~^/about$ https://www.example.com/about/;
~^/help$ https://www.example.com/help/;
}
if ($page_slash_target != "") {
return 301 $page_slash_target$is_args$args;
}
For a no-slash policy, use the following alternative map and the same return fragment. Do not install both maps. Keep real directories out of this list; Nginx’s static directory redirect can otherwise add the slash back.
map $uri $page_slash_target {
default "";
~^/about/$ https://www.example.com/about;
~^/help/$ https://www.example.com/help;
}
These anchored, case-sensitive mappings do not create catch-all locations, so unmatched requests continue through the existing handler selection. Queries are explicitly retained. Inventory physical directories, aliases and application-level slash behavior before choosing the mapped virtual routes; file-extension heuristics alone cannot establish that a route is safe to change.
Removing .php extension in Nginx
For one existing public script, use an explicit mapping from /about to /about.php. Insert these locations into a canonical HTTPS server whose root is /var/www/example/public. The PHP-FPM socket must already exist and be accessible to Nginx. Inspect fastcgi_params: it should supply the normal CGI parameters, including SCRIPT_NAME, without a duplicate SCRIPT_FILENAME definition.
location = /about.php {
return 301 https://www.example.com/about$is_args$args;
}
location = /about {
rewrite ^ /about.php break;
try_files $uri =404;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
The internal rewrite uses break, so processing stays in the /about location and reaches its FastCGI handler; it does not select the external /about.php redirect. try_files checks for the script before FastCGI and returns 404 when absent. It must not be replaced with try_files $uri.php in a static location: finding a PHP file there does not automatically select a PHP handler. Apply explicit mappings or the framework’s front controller to other routes, retain existing PHP-access restrictions, and ensure every served PHP path reaches PHP-FPM rather than static delivery. The Nginx try_files documentation explicitly describes processing in the current context.
Redirecting individual pages (clean syntax)
For small sets of changed URLs where you do not need regex, keep it readable with exact locations:
location = /blog/2022/09/seo-tips.html {
return 301 https://www.example.com/blog/seo-tips/$is_args$args;
}
location = /shop/product-123.html {
return 301 https://www.example.com/store/new-product-slug/$is_args$args;
}
location = matches the normalized path; it does not compare the query. These targets explicitly retain the query. An exact PHP location that handles only selected parameters must have a terminal error or its own configured application handler for all other cases; a general PHP regex will not take over afterward.
Handling query strings in Nginx redirects
$request_uri explicitly includes the original path and query. return copies only the URL you supply; a fixed target has no automatic query preservation. When changing a path, append $is_args$args if the query should remain, or deliberately omit it when the whole query should be removed.
# Original path and query are included explicitly
return 301 https://www.example.com$request_uri;
# Alternative: fixed path, no original query
return 301 https://www.example.com/clean-landing/;
For an exact, retired campaign endpoint, a strict full-query match makes the discard policy explicit. This example accepts only utm_campaign=old-promo and returns 404 for every other query:
location = /landing.php {
if ($args = "utm_campaign=old-promo") {
return 301 https://www.example.com/new-landing/;
}
return 404;
}
$arg_utm_campaign can inspect that argument but does not validate the meaning of other or repeated parameters. Here $args matches the full query; extra parameters intentionally do not match. If other requests remain valid, route them to an explicitly configured application handler instead of this terminal 404. These if blocks only return; do not extend them into an unreviewed mixture of rewrites, aliases and handlers.
Testing, Monitoring and Cleaning Up After the Change
Writing redirect rules is only half of the job. The rest is proving they behave as intended and updating all the surrounding SEO signals.
How to test your redirects
- HTTP checks: Inspect representative old addresses in browser developer tools, then automate the entire mapping. Verify initial status, exact Location, hop count, final 200 response and actual destination content.
- GET test:
curl --silent --show-error --location --max-redirs 5 --dump-header - --output /dev/null https://example.com/old-url/follows the chain with GET. A separate -I request uses HEAD, which can behave differently. - Cases: Include known and unknown query values, repeated/encoded parameters, one-character routes, files, real directories, Unicode/encoded path characters, HTTP/HTTPS, both approved hostnames and an unrelated host. Check PHP execution without returning source text.
- Rollout: Test the exact merged server/application configuration. A syntax check cannot prove routing correctness. Browsers and CDNs can cache 301 responses, so reverting a config does not immediately undo every cached redirect.
Fix any 302s where you meant 301, and collapse chains (A→B→C) into a single hop (A→C) whenever you can.
Update internal links, canonicals and sitemaps
Once the new structure is live, update your own links while keeping old-address redirects available:
- Update menus, body links, media URLs and templates to use the final addresses directly. For WordPress data, use backups, a dry run and a serialized-data-aware tool; avoid blind SQL replacements and unnecessary GUID changes.
- Ensure each new canonical page references itself, and update hreflang alternates where applicable.
- Submit a sitemap containing the new canonical URLs. The robots.txt and sitemap guide explains the setup. Remove staging noindex blocks from indexable targets and let crawlers access old redirects.
If you are also consolidating multiple domains (for example redirecting regional domains into one main site), coordinate your redirect rules with canonical tags and Hreflang. Our article on pointing multiple domains to one website with 301 redirects and canonical tags dives into those patterns.
Watch logs and Search Console
After deployment, your server logs are your best friend:
- Filter for 404 responses and see which paths are still being requested. Decide whether each should get a 301 to an equivalent page or remain a 404/410.
- Monitor 5xx errors (500, 502, 504) that might indicate misconfiguration or loops.
Use Page indexing reports and URL Inspection to compare representative old and new URLs. A redirected old URL being excluded from the index is expected; unexpected 404s, soft 404s, blocked targets or incorrect canonicals require investigation. Google processes a move per URL, so there is no fixed recovery deadline. Visibility may fluctuate; larger sites can take longer than the few weeks typical of processing many small-site moves. See Google’s site-move guidance for processing time, redirect retention and migration signals.
Special case: changing domain names
If you are not just reshaping paths, but moving from one domain to another, the redirect rules are similar but the stakes are higher. You will want:
- Map each moved old-domain URL directly to its final equivalent on the new domain, including any path change. Do not deliberately introduce a domain-hop then a path-hop.
- Maintain old-domain DNS and valid TLS for the redirect lifetime; prepare the new domain’s DNS, TLS certificates and dependent services. Email changes are a separate migration with their own MX/authentication/routing requirements.
- Use Search Console’s Change of Address for an eligible domain/subdomain move; it is not needed for a path-only, HTTP-to-HTTPS or same-domain www/non-www change.
We have a dedicated walkthrough on how to change your domain without losing SEO, including hosting‑side and DNS considerations.
Wrapping Up: Practical Checklist and Next Steps
A URL migration succeeds technically when old addresses reach appropriate final content and the application, links and indexing signals agree. Clear mapping, tested redirects and monitoring reduce the risk of lost traffic; they cannot promise unchanged rankings. Removed content without a relevant replacement should return 404/410.
On shared hosting, confirm which .htaccess directives are allowed. On managed or self-managed VPS, dedicated and colocation services, clarify who owns configuration and deployment. Keep redirects for as long as possible, generally at least a year according to Google, and longer where old external links remain useful. Measure configuration cost rather than assuming thousands of rules are free. The website launch checklist helps coordinate the application and hosting work.
When choosing DCHost infrastructure, verify the selected plan’s configuration access and management scope. A concrete URL inventory, deployment plan and post-launch checks make the required work clear without assuming that every hosting service includes application-level migration.





