.htaccess is a configuration file the web server reads per directory. It is how most redirects, access rules and small server behaviours get set on shared hosting, where you cannot edit the main configuration.
It is also the file most likely to take a site down with a typo, which shapes everything below.
Before you touch it
Take a copy. A mistake here produces a 500 error across the whole site, and having the original beside it turns a crisis into a thirty-second fix.
The file starts with a dot, so File Manager hides it until you turn on hidden files. Directory Indexes and Hidden Files in cPanel has the detail.
And keep application-managed blocks separate. WordPress owns the block between its markers and will rewrite it; anything you put inside is lost the next time permalinks are saved. There is more on that block in WordPress Permalinks and .htaccess.
Redirect a single page
Redirect 301 /old-page.html /new-page.html
Use 301 for a permanent move, so ranking transfers and browsers cache it. 302 means temporary and transfers nothing, which is a common accident on a move that was actually permanent.
For a pattern instead of one page:
RedirectMatch 301 ^/blog/(.*)$ /articles/$1
Force HTTPS and one hostname
Two rules people often write separately, causing an extra redirect hop:
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
That sends everything to HTTPS on the non-www form in a single redirect. Reverse it if you prefer www, which you pick does not matter, being consistent does.
Get the certificate in place first. A redirect to HTTPS without a valid certificate sends every visitor to a browser warning. There is more on the order in How to Force HTTPS and Redirect HTTP to HTTPS.
Protect files that should not be public
Configuration files, environment files and database exports are readable by anyone who guesses the name:
<FilesMatch "^\.env|\.sql$|wp-config\.php$"> Require all denied </FilesMatch>
Better still, keep those files above public_html where the web server never looks. A deny rule is a second line, not the first. Understanding File Permissions and Ownership goes into placement.
Stop PHP running in uploads
One of the highest-value rules on this page. A .php file in the uploads directory is malicious, and this stops it executing:
<Files *.php> Require all denied </Files>
Put that in a .htaccess inside the uploads directory itself, not the site root. It costs nothing and it closes the most common route from a file upload to a compromised site.
Block XML-RPC if you do not use it
<Files xmlrpc.php> Require all denied </Files>
Blocking it at the server is better than a plugin, because a plugin has to load WordPress to refuse the request, which is most of the cost you were avoiding. Check nothing you use needs it first. Securing the WordPress REST API and XML-RPC deals with which interface is which.
Custom error pages
ErrorDocument 404 /404.html ErrorDocument 500 /500.html
Use a path, not a full URL: a full URL makes the server redirect, which returns 302 instead of the error code and tells search engines the page exists.
Keep the 500 page as plain HTML with no dependency on the application, since the application is what failed. How to Create a Custom Error Page explains writing them.
Security headers
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"
The always keyword matters: without it, the headers are omitted from error responses, which are exactly the responses an attacker provokes. How to Set Up Website Security Headers deals with the two that need more care.
Compression and caching
<IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/css application/javascript </IfModule> <IfModule mod_expires.c> ExpiresActive On ExpiresByType image/webp "access plus 1 year" ExpiresByType text/css "access plus 1 month" </IfModule>
Wrap anything optional in IfModule. Without it, a rule referencing a module the server does not load produces a 500 across the site, and this is the most common cause of a copied block breaking everything. There is more on the values in Compression and Cache Headers in cPanel.
Order matters, and so does [END]
Rules run top to bottom, and a broad rule above a specific one catches requests the specific one was meant to handle.
One detail specific to .htaccess: [L] stops processing the current pass, and the whole file is then reprocessed from the top after a rewrite. Where you genuinely need processing to stop, [END] is the flag that does it: a rule using [L] that seems to be ignored by a later catch-all is usually this.
Test it immediately
Load the site the moment you save. A syntax error takes down every page, and finding out an hour later means an hour of downtime you could have avoided.
Then check a deep path as well as the homepage. A redirect rule that works at the root and mangles paths below it is common, and the symptom is everything landing on the front page. Understanding HTTP Status Codes goes over reading what you get back.
A rule that genuinely applies to every site on a server belongs above the account rather than in each one. See Apache Configuration and the Include Editor in WHM.
Find every file that is being applied
Rules apply from every directory above the one being requested, so the file you are editing may not be the one causing the behaviour.
find ~/public_html -name '.htaccess' 2>/dev/null grep -c . ~/public_html/.htaccess namei -l ~/public_html/subfolder/index.php 2>/dev/null | head
A rule in the account root applies to every site under it, including addon domains whose files live inside. That is the usual explanation for a rule affecting a site nobody edited.
Directories added by applications frequently contain their own file, written automatically and rewritten whenever the application updates. Anything you add there is temporary, which is worth knowing before spending an afternoon on it.
The performance cost is real on a deep tree
Every request causes the server to look for these files in each directory of the path, whether or not any exist.
On a shallow site that is negligible. On a deeply nested application with many small requests it is measurable, and the cost is paid on every file including images and stylesheets.
find ~/public_html -type d | awk -F/ '{print NF}' | sort -rn | head -1
find ~/public_html -name '.htaccess' | wc -l
Where server configuration is available, moving the rules there removes the lookup entirely and the rules are read once at startup rather than on every request.
That is not usually an option on shared hosting, which is why keeping the file short and the tree shallow is the version of this you can control.
A syntax error takes the whole site down
Unlike most configuration, a mistake here produces an immediate server error on every page rather than a warning.
cp ~/public_html/.htaccess ~/htaccess-$(date +%F).bak
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/
tail -5 ~/logs/example.com.error.log
Copy the file before editing, every time. Then load the site immediately after saving rather than after finishing a set of changes, so a fault is attached to one edit rather than to ten.
The error log names the line and the directive, which is considerably faster than rereading the file. An invalid command in a file the server cannot parse produces an internal error with no detail on the page itself. Understanding cPanel error logs covers finding it.