Skip to main content
Being Idea Innovations
.htaccess Rewrite and Redirect Rules: A Practical Reference
Back to Blog

.htaccess Rewrite and Redirect Rules: A Practical Reference

2 June 20186 min readApache
Share:

The .htaccess file lets you configure Apache per-directory without touching the main server config — clean URLs, redirects, caching, security headers. This is a practical reference to the rules worth knowing, with the details that make the difference between a rule that works and a 500 error.

Before anything: is .htaccess even the right tool?

Two things to know up front.

It only works on Apache. nginx ignores .htaccess entirely — its rewrites live in the server config with different syntax. If you are on nginx, none of this applies.

It has a performance cost. Apache checks for .htaccess files in every directory on every request. If you control the main server config (a VPS, not shared hosting), putting these rules in a <Directory> block there is faster. .htaccess earns its place on shared hosting, where you have no other option, and for rules a site owner needs to change without server access.

Requirements

URL rewriting needs mod_rewrite enabled and AllowOverride All set for your directory in the Apache config. Without the latter, your .htaccess is silently ignored — a common "my rules do nothing" cause.

sudo a2enmod rewrite
sudo systemctl restart apache2

Every rewrite file starts by turning the engine on:

RewriteEngine On

Clean URLs

The most common use — turn profile.php?username=amit into /amit:

RewriteEngine On

# Don't rewrite requests that map to a real file or directory —
# otherwise your CSS, images and JS get rewritten too and break.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^([a-zA-Z0-9_-]+)/?$ profile.php?username=$1 [L,QSA]

The two RewriteCond lines are essential and the old tutorials usually omit them. Without !-f and !-d, Apache tries to rewrite every request — including /style.css — through profile.php, and your assets stop loading.

The single trailing /? handles both /amit and /amit/, so you do not need the two separate rules the original used.

Front-controller pattern

Most modern PHP apps route everything through one entry point. This is the rule Laravel, Symfony and most frameworks ship:

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

Real files are served directly; everything else goes to index.php, which reads $_SERVER['REQUEST_URI'] and decides what to do.

Understanding the flags

The bracketed flags change how a rule behaves, and getting them wrong is most of the pain:

FlagMeaning
[L]Last — stop processing further rules if this matches
[R=301]Redirect the browser (permanent). R=302 is temporary
[QSA]Query String Append — keep any existing ?params
[NC]No Case — match case-insensitively
[F]Forbidden — return 403
[R=301,L]The common pairing for a permanent redirect

301 versus 302 matters more than it looks. A 301 is permanent and heavily cached by browsers — set one up wrongly and visitors keep being redirected even after you fix it, because their browser remembers. Use 302 while testing, and switch to 301 only once you are certain.

Canonical redirects

Pick one canonical form and redirect the rest, or search engines see duplicate content and your ranking is split across variants.

Force HTTPS

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

Pick www or non-www — one, not both

# Non-www → www
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L]

# Or the reverse: www → non-www
RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Combine HTTPS and www into one redirect where you can — chaining two separate 301s makes every visitor take two round trips before the page loads.

Old domain to new

RewriteCond %{HTTP_HOST} ^(www\.)?olddomain\.com$ [NC]
RewriteRule ^(.*)$ https://www.newdomain.com/$1 [R=301,L]

Preserving $1 means olddomain.com/about lands on newdomain.com/about, not just the homepage — which keeps the SEO value of every individual old URL.

Custom error pages

ErrorDocument 404 /errors/404.html
ErrorDocument 403 /errors/403.html
ErrorDocument 500 /errors/500.html

Use a root-relative path (/errors/404.html), not a full http:// URL. Pointing ErrorDocument at an absolute URL makes Apache serve the error with a 302 redirect and a 200 status — so search engines think your missing pages exist, which is actively harmful. A local path returns the correct 404 status.

The directive is ErrorDocument, capital E and D — the lowercase errorDocument in the old version is not recognised.

Directory protection

# Stop Apache listing directory contents when there's no index file
Options -Indexes

# Block access to sensitive files
<FilesMatch "\.(env|log|ini|sql|bak|config)$">
    Require all denied
</FilesMatch>

# Protect .htaccess itself
<Files ".htaccess">
    Require all denied
</Files>

Options -Indexes matters for security, not just tidiness — without it, a directory lacking an index file shows its entire contents to anyone, which routinely leaks backups and config files.

Note Require all denied is the Apache 2.4 syntax. The older Order deny,allow / Deny from all was removed in 2.4 — if you copy a pre-2.4 snippet it throws a 500.

Prevent image hotlinking

Stop other sites embedding your images and consuming your bandwidth:

RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com [NC]
RewriteRule \.(jpe?g|png|gif|webp|svg)$ - [F]

The first condition allows an empty referer, so direct visits and privacy-conscious browsers still see images.

Caching and compression

Real performance wins that belong in .htaccess:

# Compress text responses
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
</IfModule>

# Cache static assets aggressively
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType text/css   "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType text/html  "access plus 0 seconds"
</IfModule>

Cache assets long, HTML not at all. Use versioned filenames (app.4f2c.css) so a long cache never serves stale code — a cache-busting query string or hash in the name lets you set a one-year expiry safely.

Security headers

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>

Add HSTS only once HTTPS is fully working — it tells browsers to refuse HTTP for a year, which is painful to reverse if something is misconfigured.

Debugging: why rules "don't work"

The order of causes to check, most common first:

  1. AllowOverride is not All for your directory — the whole file is ignored.
  2. mod_rewrite is not enabled.
  3. Missing !-f / !-d conditions — assets get rewritten and break.
  4. A cached 301 from earlier testing — clear the browser cache or test in a private window.
  5. Rule ordering — an earlier rule with [L] stops the ones below it from running.
  6. 500 error — usually a typo, or pre-2.4 syntax like Order deny,allow. Check the Apache error log; it names the offending line.

Test redirects with curl -I https://example.com/old-page — it shows the status code and Location header without your browser's caching getting in the way.

Summary

  • .htaccess is Apache-only, and slower than main-config rules where you have the choice.
  • Always guard rewrites with !-f and !-d so real files are left alone.
  • Know your flags — especially 301 (permanent, cached) versus 302 (temporary).
  • Canonicalise HTTPS and www in as few redirects as possible.
  • Point ErrorDocument at a local path, not an absolute URL.
  • Use Apache 2.4 syntax (Require all denied).
  • Add caching, compression and security headers — real, easy wins.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers