htaccess redirect generator
Built in your browser · nothing is uploaded
Turns a list of old and new paths into Apache RewriteRule lines. A pair written as /old-page | /new-page comes out as RewriteRule ^old-page$ /new-page [R=301,L], under a single RewriteEngine On. The leading slash is dropped on purpose, because a per-directory pattern never sees one.
How to use the htaccess redirect generator
The generated pattern has no leading slash, and the reason is a rule of Apache’s rather than a style choice. In per-directory context the documentation is explicit: "the directory-path to which the rule applies is stripped from the currently mapped filesystem path before comparison", "the removed prefix always ends with a slash", and consequently "a Pattern with ^/ never matches in per-directory context". A rule written as ^/old-page$ in a .htaccess file matches nothing at all, silently. This tool strips the slash so the rules work; the corollary is that they assume the file sits at the document root, because a .htaccess inside /shop/ has /shop/ removed before matching, and ^old-page$ there means /shop/old-page.
The pattern never sees the query string. Apache says to use a RewriteCond on %{QUERY_STRING} if you want to match against it. Two consequences follow, one convenient and one destructive. The convenient one: /old-page?ref=newsletter still matches ^old-page$, so tracking parameters do not defeat your redirects, and by default the query string is carried across to the new URL untouched. Add [QSD] if you want it dropped, or [QSA] to merge it with parameters you are adding.
The destructive one is a redirect loop, and it catches people who are being careful. Write a rule sending /a to /a?utm_source=x and the browser requests /a?utm_source=x, Apache strips the query before matching, the path is still a, the rule fires again, and the loop runs until the browser gives up. Any rule whose target differs from its source only in the query string will do this. The fix is a RewriteCond %{QUERY_STRING} !utm_source guard above it.
The $ anchors the end of the path, so matching is exact. /old-page matches; /old-page/ does not, and neither does /old-page/child. If both slash forms were in circulation, write both pairs, or drop the $ and accept that the rule then also catches anything beginning with that path. Matching is case-sensitive too, so /Old-Page needs its own line or an [NC] flag added by hand.
On the status codes, RFC 9110 is precise where SEO advice usually is not. A 301 response "is heuristically cacheable", and so is a 308: with no explicit cache headers, clients may keep them for as long as their own heuristics allow, which in practice means a mistaken permanent redirect keeps sending returning visitors to the wrong place long after the server is fixed. You cannot recall one already cached. You can bound the next one, by sending an explicit Cache-Control with the redirect response, and using 302 or 307 whenever reverting is even slightly possible.
The 307 and 308 pair exists for a different reason again: the method. RFC 9110 notes that for 301 and 302 "a user agent MAY change the request method from POST to GET", while a 307 recipient "MUST NOT change the request method". Redirecting a form endpoint with a 301 can therefore turn a POST into a GET and lose the body. Use 308 for a permanent move of anything that receives POSTs, keeping in mind the RFC’s own caution that 308 "is much younger (June 2014) than its sibling codes and thus might not be recognized everywhere".
Two operational notes. Chains cost real time: A to B to C is two round trips before anything renders, and each hop is another chance for a client to stop following. When you retire B, rewrite the A rule to point at C rather than leaving the chain in place. And .htaccess itself is not free. Apache’s own documentation is blunt that enabling it "causes a performance hit, whether or not you actually even use them", because httpd looks for the file in the requested directory and every directory above it, on every request. Redirects in the virtual host config cost nothing per request; the trade you are making is that .htaccess needs no restart and no root, which is often the reason it exists.
What people use it for
- Moving a set of old URLs after a site restructure
- Writing the RewriteRule lines for a batch of retired pages
- Choosing 301 or 302 for a page that will come back
- Redirecting a form endpoint without turning its POST into a GET
- Collapsing an old redirect chain into single hops
- Producing rules that survive tracking parameters on the incoming URL
Questions
Because Apache strips the directory path, trailing slash included, before matching in per-directory context. Its documentation states that a pattern with ^/ never matches there, so the slash would break every rule.
These rules assume so. A .htaccess inside /shop/ has /shop/ removed before matching, so ^old-page$ in that file means /shop/old-page.
301 for a move you will not reverse, 302 for anything provisional. RFC 9110 makes 301 heuristically cacheable and says nothing of the kind about 302, so the cost of guessing wrong is asymmetric.
Clients that already cached it keep going to the wrong place, and you cannot recall it. You can limit the damage on future responses by sending an explicit Cache-Control header with the redirect.
There is no fixed answer. With no cache headers a 301 is heuristically cacheable, so each client applies its own policy, and some keep it until site data is cleared.
Preserving the method. RFC 9110 allows a client to turn a POST into a GET on a 301 or 302 and forbids it on a 307 or 308, which matters for anything receiving form submissions.
On modern browsers, yes. RFC 9110 still notes it is much younger than the others, dating from June 2014, and might not be recognised everywhere, so older clients and bespoke HTTP libraries are the risk.
No. RFC 9110 calls a 308 heuristically cacheable in the same terms as a 301. It solves the method question and nothing else.
No. The pattern sees only the path, so /old-page?ref=x still matches ^old-page$. To match on parameters you need a RewriteCond on %{QUERY_STRING}.
It is carried over to the new URL by default. Add [QSD] to discard it, or [QSA] to combine it with parameters in your target.
Almost always because the target differs from the source only in its query string. Apache strips the query before matching, so the rule fires again on its own output. Guard it with a RewriteCond on the parameter you are adding.
No. The $ anchors the end of the path, so /old-page matches and /old-page/ does not. Add a second pair for the slash form if both are in circulation.
Yes. Add [NC] inside the flag brackets for a case-insensitive rule; this tool does not write that flag for you.
Drop the $ so the pattern matches a prefix, or use mod_alias, whose Redirect directive is prefix-based and appends whatever followed the matched part.
For a straight path-to-path move, Redirect from mod_alias is shorter, takes a leading slash and needs no RewriteEngine. RewriteRule is for anything conditional, pattern-based or flag-dependent.
Yes. Each hop is another round trip before the page starts loading, and clients limit how many they will follow. Point the original rule at the final destination rather than stacking hops.
A .htaccess file in the directory it applies to, on an Apache server with mod_rewrite loaded and AllowOverride permitting FileInfo. Nginx uses entirely different syntax.
No, and skipping the restart is the main reason to use .htaccess at all: the file is read on every request, so changes take effect immediately and need no privileged access.
Apache says enabling .htaccess "causes a performance hit, whether or not you actually even use them", because it looks for the file in the requested directory and all of its parents on every request.
Yes. It is a switch rather than a per-rule directive, so paste the RewriteRule lines into a file that already has it rather than repeating it.
With curl -I on each old URL, reading the status line and the Location header. A browser follows the chain and hides which hops happened, and its own cache can mask a rule that never fired.