turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

SEO-003

Clean URLs in .htaccess, and the setting that saves you when they fail

The rewrite rules, the file protection that belongs beside them, and a switch that keeps the site working on hosting without mod_rewrite.

Clean URLs are worth having: they are readable, they survive being pasted into a forum, and they keep the query string free for things that are actually parameters.

The risk is that every internal link on the site assumes they work. On hosting where mod_rewrite is missing or AllowOverride is off, every link points at a 404 and the site looks completely broken to anyone who installs it.

So pair the rules with a setting. The URL helpers ask whether clean URLs are on and emit whichever form works, and the operator flips one switch instead of filing a support ticket.

APACHE
Options -Indexes
DirectoryIndex index.php

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Generated so they carry this install&#039;s own domain
    RewriteRule ^robots\.txt$ robots.php [L]
    RewriteRule ^sitemap\.xml$ sitemap.php [L]

    RewriteRule ^c/([A-Za-z0-9\-]+)/?$   browse.php?cat=$1  [L,QSA]
    RewriteRule ^tag/([A-Za-z0-9\-\.]+)/?$ browse.php?tag=$1 [L,QSA]
    RewriteRule ^s/([A-Za-z0-9\-]+)/?$   snippet.php?s=$1   [L,QSA]
    RewriteRule ^p/([A-Za-z0-9\-]+)/?$   page.php?p=$1      [L,QSA]
</IfModule>

ErrorDocument 404 /404.php

# Never serve the config, the schema, or the seed data
<FilesMatch "^(config\.php|config\.sample\.php)$">
    Require all denied
</FilesMatch>
<IfModule mod_authz_core.c>
    <FilesMatch "\.(sql|json|md)$">
        Require all denied
    </FilesMatch>
</IfModule>

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

Using it

QSA preserves anything already in the query string, so pagination still works on a rewritten URL.

Make the URL helpers read the setting:

return clean_urls() ? url('s/' . $slug) : url('snippet.php?s=' . rawurlencode($slug));

Then 301 the query form to the clean one when rewrites are on, so both do not get indexed as separate pages.

What bites people

A trailing-slash-optional pattern means two URLs serve the same page. The canonical tag decides which one counts; without it you have duplicate content by default.

Denying .md also hides your README from the web, which is usually what you want on a live site and surprising the first time you notice.

Shared hosts sometimes allow mod_rewrite but not AllowOverride for FilesMatch. Test the config protection specifically — do not assume it applied because the rewrites did.

Also in SEO and Frontend