Using .htaccess Files for Pretty URLs
Continuing our review of .htaccess files, today we'll examine how to use mod_rewrite to create pretty URLs.
Benefits of Formatted URLs
While some claim pretty URLs help in search engine rankings, the debate here is fierce. We can all agree that pretty URLs make things easier for our users and add a level of professionalism and polish to any web application.
I could go over all the theoretical reasons for this, but I like real-world examples better. Like it or hate it, we all must admit that Twitter is a wildly popular web application, and part of the reason for that is most certainly how it formats URLs. I can tell anyone in the know that my Twitter username is noahhendrix, and they know my profile can easily be found at twitter.com/noahhendrix. This seemingly simple concept has vast effects on the popularity of your application.



Overview and Setup
If you plan to follow along, you need to install a PHP development environment on your computer, and you can go for either WAMP or XAMPP. Both of these packages come bundled with the Apache server, which provides the modules we are going to use.
Create the following folder and files in your web server root directory.
1 |
demo/ |
2 |
├── .htaccess |
3 |
├── home.php |
4 |
├── article.php |
5 |
├── profile.php |
The folder demo contains four files: home.php, article.php, profile.php, and .htaccess.
In the absence of an index.php file, we'll use .htaccess to set home.php as our index page. That way, it'll show at the URL localhost/demo/.
In the HTML markup for home.php, we'll provide a link to our article page.
1 |
<!DOCTYPE html>
|
2 |
<html lang="en"> |
3 |
<head>
|
4 |
<-head->
|
5 |
</head>
|
6 |
<body>
|
7 |
<h1 style="text-align: center"> Semantic SEO Friendly with htaccess</h1> |
8 |
<a href="article?year=2022&month=3&slug=using-htaccess-to-prettify-url">View Post</a> |
9 |
</body>
|
10 |
</html>
|
We added a query string to the URL containing three variables that we want to pass to the article: year, month, and slug. Later, we'll use the .htaccess to configure our server to use a cleaner, well-structured URL whilst ensuring that the page still gets the query strings.
Inside article.php, we get these variables from the request URL and display them on the page:
1 |
<!DOCTYPE html>
|
2 |
<html lang="en"> |
3 |
<head>
|
4 |
|
5 |
</head>
|
6 |
<body>
|
7 |
<h1 style="text-align: center"> Article Details </h1> |
8 |
<p>Post Year: <?php echo $_GET['year']?></p> |
9 |
<p>Post Month: <?php echo $_GET['month']?></p> |
10 |
<p>Post Slug: <?php echo $_GET['slug']?></p> |
11 |
</body>
|
12 |
</html>
|
The .htaccess File
.htaccess is a server configuration file used to control websites. In using an Apache server, this file provides a way to reconfigure your server without having to manually edit the server configuration files.
Since we'll be working with the request URLs, we'll use the mod_rewrite Apache module to manipulate incoming URLs on the server-side.
We'll make all rewrites and reconfigurations inside the <IfModule>
directive.
1 |
<IfModule mod_rewrite.c> |
2 |
# rules goes here |
3 |
</IfModule>
|
The instructions inside this directive will run only if mod_rewrite is loaded by Apache. To make any modifications, we must first enable the RewriteEngine
on our Apache server.
1 |
# mod_rewrite
|
2 |
# directive
|
3 |
<IfModule mod_rewrite.c> |
4 |
RewriteEngine On |
5 |
<IfModule mod_negotiation.c> |
6 |
Options -MultiViews |
7 |
</IfModule> |
8 |
</IfModule> |
In the nested directive just below, we set the environment up to follow symbolic links using the Options
directive.
With the base configuration all set, let's take a look at some of the ways we can improve our URLs using Apache and PHP.
Making URLs Cleaner With .htaccess
Setting Alternative Index Files
Recall that our site doesn't have a default page, and this is because the index.php file is absent in the directory. In cases where you want to use an alternative PHP script file as your site's index, use DirectoryIndex
.
Below, we are manually making home.php our index page by pointing DirectoryIndex
to that file.
1 |
# mod_rewrite
|
2 |
# directive
|
3 |
<IfModule mod_rewrite.c> |
4 |
# other code
|
5 |
|
6 |
DirectoryIndex home.php |
7 |
</IfModule> |
Formatting URLs Using Apache
Unclean URLs expose the filenames of underlying server scripts and variables used in rendering the page. For example, a person can easily make out that the profile.php file is responsible for loading the page based on the URL example.com/profile.php?id=1.
By using clean URLs, you can help secure the site by hiding the structure of your application's back end. For example, based on the URL example.com/user/1, it's impossible to tell what server-side scripting file is responsible for rendering the page.
Let's match an alternative URL that we want the user to see:
1 |
# Rewrite for projects.php
|
2 |
RewriteRule ^user profile.php [NC,L] |
The string of text you have after the caret symbol is the alternative directory listing that you want in place of the actual URL. Now when we navigate to /user on our site, it'll actually take us to profile.php.
Passing Variables
Some of our URLs may contain dynamic variables. Take the following, for example: https://localhost/demo/article?year=2022&month=3&slug=using-htaccess-to-prettify-url
Going back to article.php, recall that we got the variables from the query string in the URL because the page requires these variables to render:
1 |
<p>Post Year: <?php echo $_GET('year')?></p> |
2 |
<p>Post Month: <?php echo $_GET('month')?></p> |
3 |
<p>Post Slug: <?php echo $_GET('slug')?></p> |
To display the article page, we must add the query string to the URL: article?year=2022&month=3&slug=using-htaccess-to-prettify-url
This URL is neither SEO- nor user-friendly. A significantly better URL would be: article/2022/3/using-htaccess-to-prettify-url
However, changing the URL to this will give an Undefined Index error because the query string is no longer being sent to the page via the URL, and this is where .htaccess comes into play.
To solve this problem, we need to write a regular expression (regex) to match our intended semantic URL, and then pass query variables from each capture group to article.php:
1 |
RewriteRule ^article/([0-9]+)/([0-9]+)/([0-9A-Za-z\-_]+) article.php?year=$1&month=$2&slug=$3 [NC, L] |
We'll only match a URL that contains the path articles/ followed by a double-digit number for the article year and month, followed by a string for the post slug which could contain text, numbers, hyphens, and an underscore.
When a request's URL matches the provided regex, we tell the server to redirect to article.php. Most importantly, we pass the query strings along to the page.
For the query string, the three capture groups, denoted by $1
, $2
, and $3
respectively, are all transplanted into the variables year
, month
, and slug
.
We end the rule with two flags. The first flag, NC
(Non-case sensitive), tells Apache not to consider the case when matching the URL with our regex. The second flag, L
, tells Apache to stop code execution thereafter.
Finally, replace +SymLinksIfOwnerMatch
with -MultiViews
.
1 |
<IfModule mod_negotiation.c> |
2 |
# Options +SymLinksIfOwnerMatch |
3 |
Options -MultiViews |
4 |
|
5 |
# other code |
6 |
</IfModule>
|
Navigate to localhost/demo/article/2022/3/using-htaccess-to-prettify-url on your browser and you'll find that our page is still able to access the variables without using URL query strings.
Keep in mind that the order is important. So, for example, if you reorder the variables in the query string like in the following instance:
1 |
article.php?slug=$1&month=$2&year=$3 |
You also need to reorganize the regular expression groups to match them.
Using PHP
This next method is great for those who don't want to distribute too much logic to Apache and feel more comfortable in PHP (or similar scripting languages). The concept here is to capture any URL the server receives and push it to a PHP controller page. This comes with the added benefit of control, but greater complexity at the same time. Your .htaccess file might look something like this:
1 |
<IfModule mod_rewrite.c> |
2 |
RewriteEngine On |
3 |
|
4 |
<IfModule mod_negotiation.c> |
5 |
Options -MultiViews |
6 |
</IfModule>
|
7 |
|
8 |
DirectoryIndex home.php |
9 |
|
10 |
RewriteCond %{REQUEST_FILENAME} !-d |
11 |
RewriteCond %{REQUEST_FILENAME}\.php -f |
12 |
|
13 |
RewriteRule ^.*$ ./home.php |
14 |
</IfModule>
|
Instead of creating a capture group, we just tell Apache to grab every URL and redirect it to home.php. What this means is we can do all of our URL handling in PHP without relying too much on stringent URL paths in .htaccess. Here is what we might do at the top of our home.php file to parse out the URL:
1 |
<?php
|
2 |
#replace demo/ in the request URL with an empty string
|
3 |
$request = str_replace("/demo/", "", $_SERVER['REQUEST_URI']); |
4 |
|
5 |
#split the path by '/'
|
6 |
$params = split("/", $request); |
The first line is not necessary unless your application doesn't live at the root directory, like my demos. I am removing the non-sense part of the URL that I don't want PHP to worry about. $_SERVER['REQUEST_URI']
is a global server variable that PHP provides and stores the request URL, it generally looks like this:
1 |
demo/article/query1/query2/... |
As you can see, it is basically everything after the domain name. Next, we split up the remaining part of the virtual path and split it by the /
character. This allows us to grab individual variables.
One thing you might do is take the first element of the $params
array and include a file by that same name. Then, within the file, you can use the second, third, and subsequent elements in the array (i.e. the queries) to execute some code (such as fetching from the database). This might look something like this:
1 |
<?php
|
2 |
#keeps users from requesting any file they want
|
3 |
$safe_pages = array("users", "profile", "article"); |
4 |
|
5 |
if(in_array($params[0], $safe_pages)) { |
6 |
include($params[0].".php"); |
7 |
} else { |
8 |
include("404.php"); |
9 |
}
|
10 |
?>
|
Now that we have the soapbox out of the way, let's move on. Next, we check if the requested file is in the $safe_pages
array, and if it is, we include—otherwise, we will include a 404 not found page. On the included page, you will see that you have access to the $params
array, and you can grab whatever data from it that is necessary for your application.
This is great for those who want a little more control and flexibility. It obviously requires quite a bit of extra code, so it's probably better for new projects that won't require a lot of code to be updated to fit the new URL formats.
A Simple URL Shortener
This last part of the tutorial is going to let us put the code we went over above into practice, and it's more or less a "real-life" example. We are going to create a service called shrtr (I made up this name, so any other products with this name are not associated with the code I am posting below). I know this is not an original concept, and it's only meant for a demonstration of mod_rewrite. First, let's take a look at the database:



As you can see, this is very straightforward. We have only four columns:
-
id
: unique identifier used to reference specific rows -
short
: unique string of characters appended to the end of our URL to determine where to redirect -
url
: the URL that the short URL redirects to -
created_at
: a simple timestamp so we know when this URL was created
The Basics
Next, let's go over the six files we need to create for this application:



- .htaccess: redirects all short URLs to serve.php
- create.php: validates URL, creates shortcode, saves to the database
- css/style.css: holds some basic styling information
- db_config.php: store variables for database connections
- index.php: the face of our application with a form for entering URLs
- serve.php: looks up a short URL and redirects to an actual URL
That is all we need for our basic example. I will not cover index.php or css/style.css in very great detail because they are static files with no PHP.
1 |
<!DOCTYPE html>
|
2 |
<html lang="en"> |
3 |
<head>
|
4 |
<meta charset="UTF-8"> |
5 |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
7 |
<title>Make URL shorter</title> |
8 |
</head>
|
9 |
<body>
|
10 |
<div id="pagewrap"> |
11 |
<h1>shrt<span class="r">r</span>.me</h1> |
12 |
|
13 |
<div class="body"> |
14 |
<form action="./create.php" method="post"> |
15 |
|
16 |
<span class="instructions">Type your URL here</span> |
17 |
<input name="url" type="text" /> |
18 |
<input type="submit" value="shrtr" /> |
19 |
|
20 |
</form>
|
21 |
</div>
|
22 |
|
23 |
</div>
|
24 |
</body>
|
25 |
</html>
|
The only interesting to note here is that we submit the form with a field called URL to create.php.
1 |
# css/style.css |
2 |
----
|
3 |
/* reset */
|
4 |
* { |
5 |
font-family: Helvetica, sans-serif; |
6 |
margin: 0; |
7 |
padding: 0; |
8 |
}
|
9 |
|
10 |
/* site */
|
11 |
html, body { background-color: #008AB8; } |
12 |
a { color: darkblue; text-decoration: none;} |
13 |
|
14 |
#pagewrap { |
15 |
margin: 0 auto; |
16 |
width: 405px; |
17 |
}
|
18 |
|
19 |
h1 { |
20 |
color: white; |
21 |
margin: 0; |
22 |
text-align: center; |
23 |
font-size: 100px; |
24 |
}
|
25 |
h1 .r { color: darkblue; } |
26 |
|
27 |
.body { |
28 |
border-radius: 10px; |
29 |
border-radius: 10px; |
30 |
background-color: white; |
31 |
text-align: center; |
32 |
padding: 50px; |
33 |
height: 80px; |
34 |
position: relative; |
35 |
}
|
36 |
|
37 |
.body .instructions { |
38 |
display: block; |
39 |
margin-bottom: 10px; |
40 |
}
|
41 |
.body .back { |
42 |
right: 15px; |
43 |
top: 10px; |
44 |
position: absolute; |
45 |
}
|
46 |
|
47 |
.body input[type=text] { |
48 |
display: block; |
49 |
font-size: 20px; |
50 |
margin-bottom: 5px; |
51 |
text-align: center; |
52 |
padding: 5px; |
53 |
height: 20px; |
54 |
width: 300px; |
55 |
}
|
That is all very generic, but it makes our application a little more presentable.



The last basic file we need to look at is our db_config.php. I created this to abstract some of the database connection information.
1 |
# db_config.php |
2 |
---- |
3 |
<?php
|
4 |
|
5 |
$database = "DATABASE_NAME"; |
6 |
$username = "USERNAME"; |
7 |
$password = "PASSWORD"; |
8 |
$host = "localhost"; |
9 |
|
10 |
?>
|
You need to replace the values with what works in your database, and host is probably localhost, but you need to double-check with your hosting provider to make sure. Here is the SQL dump of the table, url_redirects
, which holds all the information we showed above:
1 |
--
|
2 |
-- Table structure for table `url_redirects` |
3 |
--
|
4 |
|
5 |
CREATE TABLE IF NOT EXISTS `url_redirects` ( |
6 |
`id` int(11) NOT NULL auto_increment, |
7 |
`short` varchar(10) NOT NULL, |
8 |
`url` varchar(255) NOT NULL, |
9 |
`created_at` timestamp NOT NULL default CURRENT_TIMESTAMP, |
10 |
PRIMARY KEY (`id`), |
11 |
KEY `short` (`short`) |
12 |
) ENGINE=MyISAM DEFAULT CHARSET=utf8; |
Creating the Short URL
Next, let's look at the code necessary to create our short URL.
1 |
# create.php |
2 |
---- |
3 |
<?php
|
4 |
require("./db_config.php"); |
5 |
|
6 |
$url = $_REQUEST['url']; |
7 |
|
8 |
if(!preg_match("/^[a-zA-Z]+[:\/\/]+[A-Za-z0-9\-_]+\\.+[A-Za-z0-9\.\/%&=\?\-_]+$/i", $url)) { |
9 |
$html = "Error: invalid URL"; |
10 |
} else { |
11 |
|
12 |
$db = mysql_connect($host, $username, $password); |
13 |
|
14 |
$short = substr(md5(time().$url), 0, 5); |
15 |
|
16 |
if(mysql_query("INSERT INTO `".$database."`.`url_redirects` (`short`, `url`) VALUES ('".$short."', '".$url."');", $db)) { |
17 |
$html = "Your short URL is<br />shrtr.me/".$short; |
18 |
} else { |
19 |
$html = "Error: cannot find database"; |
20 |
}
|
21 |
|
22 |
mysql_close($db); |
23 |
}
|
24 |
?>
|
25 |
|
26 |
<!DOCTYPE html>
|
27 |
<html lang="en"> |
28 |
<head>
|
29 |
<meta charset="UTF-8"> |
30 |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
31 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
32 |
<title>Make URL shorter</title> |
33 |
</head>
|
34 |
<body>
|
35 |
<div id="pagewrap"> |
36 |
<h1>shrt<span class="r">r</span>.me</h1> |
37 |
|
38 |
<div class="body"> |
39 |
<?= $html ?> |
40 |
<br /><br /> |
41 |
<span class="back"><a href="./">X</a></span> |
42 |
</div>
|
43 |
</body>
|
44 |
</html>
|
Now we are getting a bit more complex! First, we need to include the database connection variables we created earlier, and then we store the URL parameter sent to us by the create
form in a variable called $url
. Next, we do some regular expressions magic to check if they actually sent a URL, and if not, we store an error.
If the user entered a valid URL, we create a connection to the database using the connection variables we include at the top of the page. Next, we generate a random five-character string to save to the database, using the substr
function. The string we split up is the MD5 hash of the current time()
and $url
concatenated together. Then we insert that value into the url_redirects table along with the actual URL, and store a string to present to the user. If it fails to insert the data, we store an error. If you move down into the HTML part of the page, all we do is print out the value of $html
, be it error or success. This obviously isn't the most elegant solution, but it works!



Serving the Short URL
So we have the URL in the database. Now, let's work on serve.php so we can actually translate the short code into a redirect.
1 |
<?php
|
2 |
require("./db_config.php"); |
3 |
|
4 |
$short = $_REQUEST['short']; |
5 |
|
6 |
$db = mysql_connect($host, $username, $password); |
7 |
$query = mysql_query("SELECT * FROM `".$database."`.`url_redirects` WHERE `short`='".mysql_escape_string($short)."' LIMIT 1", $db); |
8 |
$row = mysql_fetch_row($query); |
9 |
|
10 |
if(!empty($row)) { |
11 |
Header("HTTP/1.1 301 Moved Permanently"); |
12 |
header("Location: ".$row[2].""); |
13 |
} else { |
14 |
$html = "Error: cannot find short URL"; |
15 |
}
|
16 |
|
17 |
mysql_close($db); |
18 |
?>
|
19 |
|
20 |
<!DOCTYPE html>
|
21 |
<html lang="en"> |
22 |
<head>
|
23 |
<meta charset="UTF-8"> |
24 |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
25 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
26 |
<title>Make URL shorter</title> |
27 |
</head>
|
28 |
<body>
|
29 |
<div id="pagewrap"> |
30 |
<h1>shrt<span class="r">r</span>.me</h1> |
31 |
|
32 |
<div class="body"> |
33 |
<?= $html ?> |
34 |
<br /><br /> |
35 |
<span class="back"><a href="./">X</a></span> |
36 |
</div>
|
37 |
|
38 |
</div>
|
39 |
</body>
|
40 |
</html>
|
This one is very similar to create.php: we include the database information and store the short code sent to us in a variable called $short
. Next, we query the database for the URL of that short code. If we get a result, we redirect to the URL; if not, we print out an error like before.
As far as PHP goes, that is all we need to do. However, at the moment, to share a short URL, users must enter this: http://shrtr.me/server.php?short=SHORT_CODE. Not very pretty, is it? Let's see if we can't incorporate some mod_rewrite
code to make this nicer.
Prettify With .htaccess
Of the two methods I wrote about at the beginning of the tutorial, we will use the Apache one because this application is already created without considering any URL parsing. The code will look something like this:
1 |
<IfModule mod_rewrite.c> |
2 |
RewriteEngine On |
3 |
|
4 |
<IfModule mod_negotiation.c> |
5 |
Options +FollowSymLinks |
6 |
</IfModule> |
7 |
|
8 |
RewriteCond %{SCRIPT_FILENAME} !-d |
9 |
RewriteCond %{SCRIPT_FILENAME} !-f |
10 |
|
11 |
RewriteRule ^(\w+)$ ./serve.php?short=$1 |
12 |
</IfModule> |
Skipping to the RewriteRule
, we are directing any traffic that doesn't already have a real file or directory to serve.php and putting the extension in the GET variable short. Not too bad—now go try it out for yourself!
Conclusion
Today, we learned a few different ways to utilize mod_rewrite
in our application to make our URLs pretty. Thanks for reading!
This post has been updated with contributions from Kingsley Ubah. Kingsley is passionate about creating content that educates and inspires readers. Hobbies include reading, football, and cycling.