Best Way To Implement a Non-Breaking Friendly URL In PHP or Laravel or Any Language

I was working on the link structure of my new Laravel app, and out of the blue I said: “What would happen if a user changes the slug of a post?”

First Attempt – 301 Redirection

The first solution I thought of is to keep the old URLs and do a 301 redirect to the new URL, while this is not a bad idea per se, I don’t like the idea of keeping the old URL, there should be a super simple way to achieve my goal so, I ruled that out.

Second Attempt – Add a Unique Identifier

Note: I’ll be the last person to follow what others are doing blindly, this is just a quest for finding the right solution to my problem.

The second solution is adding a never-changing identifier to the URL.

I first saw this on youtube, here is an example:

https://www.youtube.com/watch?v=bC6ouP7qRoQ

You’ll never see youtube generating a slug from the video title which is a bit crazy of them to do since they recommend using a human-readable URL.

The sites that I have recently seen that handle this very well are StackOverFlow, Medium, Reddit.

E.g on StckOverfFlow, the following:

https://stackoverflow.com/questions/535020/tracking-the-script-execution-time-in-php

is the same as:

https://stackoverflow.com/questions/535020/tracking

and as:

https://stackoverflow.com/questions/535020

They all redirects to:

https://stackoverflow.com/questions/535020/tracking-the-script-execution-time-in-php

The slug doesn’t have to be unique, what matters is the ID (which is unique), the slug is only for SEO purposes.

The First Solution Try

My first attempt at solving this is to use the Post ID which already has an AUTO_INCREMENT attribute meaning when you insert a new record to the table, and the auto_increment field is NULL or DEFAULT, the value will automatically be incremented.

So, using this would give us a URL like so:

https://example.com/category/1/my-first-post

and the second post would have /category/2/my-second-post. This is a simple and straightforward way, and you might just head off and implement it, but I went further…

The Second Solution Try

The issue with the first solution is that I not only hate the fact that it is sequential, but it also makes it damn easy to scrape my data, it isn’t too much problem as the data is public but it doesn’t quite make sense when you are using it for your order table or your user’s table, you might expose a lot to your competitor, scrapers, bots, etc.

Although, the data can still be scraped, however, no one would know your data size and the velocity(rate of publishing a  post or getting orders or new users signing up), and besides, I might end up doing it for a table other than the post table, so, I just need something that works right now.

That aside, the second way I attempted to solve this is to generate a short random integer number(less than 8 digits), I first tried it with:

PHP mt_rand() – It doesn’t take quite long to get a collision, the fact that I wanted a smaller integer makes the collision higher.

Here is the code I am using to detect that:

<?php        
$randomData = array(); $collison = 0; $duplicatedValue = array();
for($i= 0; $i <= 100000; ++$i) {
    $randgen = mt_rand(00000001, 99999999);
    if (in_array($randgen, $randomData, true)) { // if our randgen data collides with randomData, we...
        ++$collison; // increment the collision
        $duplicatedValue[] = $randgen;
        $randomData[] = $randgen;
    }
    $randomData[] = $randgen;
}

print_r($duplicatedValue);
echo "<br>";
return "The number of collision is $collison\n";

The code is pretty much self-explanatory, ran a hundred thousand loop (you can shuffle this if you like), and was generating random data on each run. Was also checking if any collision occurs with the in_array() (might not be the best function to use in this case). If any collision occurs, we increment the collision variable and also store the collided number into the $duplicatedvalue.

Here are my results:

Note: Open the image in a new tab to see the results properly.

TEST 1

1. mt_rand collision test 1

TEST 2

2. mt_rand collision test 2

TEST 3

3. mt_rand collision test 3

TEST 4

4. mt_rand collision test 4

TEST 5

5. mt_rand collision test 5

The chance that I would get a collision with the above-sampled data is in the range of 40 – 60, which is a no-no. Again, I don’t want too many digits, so, uniqid() or hrtime() or microtime() doesn’t cut it for me.

As for microtime, it would generate the same number if done simultaneously, see:

This code:

echo microtime(true). "<br>" . PHP_EOL.
            microtime(true). "<br>" . PHP_EOL.
            microtime(true). "<br>" . PHP_EOL.
            microtime(true);

Gave me:

1616291719.7417
1616291719.7417
1616291719.7417
1616291719.7417

Which is also a no-no…and that brings us to…

The Solution

The solution is the combination of the first try and a little touch of the second try…and that would not only guarantee 100% uniqueness but would also obfuscate your ID. So, as you might have guessed, we append each post id to a random number.

for example:

<?php      
 $randomData = [];
for($i= 0; $i <= 100000; ++$i) {
    $randgen = $i.random_int(000000, 999999);
    $randomData[] = $randgen;
}

I am using the $i counter to simulate an auto-increment column, so, no matter the condition, we can not only guarantee a unique number but it is also random in some way, For example, we can have:

6. Solution to random number

As you can see in the image above, the random number after the post ids are within the range of  000000 – 999999, so, even if you know the actual post id, you’ll still need to guess digit from 000000 – 999999 to get the actual slug id.

Laravel Implementation

Once you’ve saved your post in the PostsController e.g  $post->save();

You can add an event listener to your service provider that automatically adds the slug_id to the associated data as soon as a post is created.

Create a service provider like so:

php artisan make:provider GeneratePostSlugIDProvider

Then under the boot() method, you can add the following:

        Post::created(function($model){
            // We get the last inserted ID, we then append a random integer. This would make for our post...
            //  slug identifier
            $slugGen = $model->post_id.random_int(000000, 999999);
            $model->slug_id = $slugGen;
            $model->save(); // re-save the post

        });

or, you can just do everything in the PostsController.

If you are not using laravel, find a way to return the lastinsertedID (make sure a column in your table has the AUTO_INCREMENT attribute), then append the rand number.

Spice It Up

You can spice things up a bit with just a simple 301 redirection, here is what I mean:

Assuming, your post URL is:

http://example.com/root-category/10781626/a-very-new-post

Since the slug_id is unique. If the user visits the URL without adding the slug title, e.g:

http://example.com/root-category/10781626

Write a logic that checks for the above condition, e.g, you query the slug_id, and if it founds a result in your post table, return a 301 redirection to the real URL.

If the user visits with an incomplete slug-title, e.g:

http://example.com/root-category/10781626/a-very

You also query the slug_id, in short, always ignore the slug-title since it is only for SEO purposes. Once you get the queried result, return a 301 to the original URL.

If everything else fails, you can then query the slug title, and return a 301 to the original address.

Here is an illustration:

Non Breaking URL Illustration

Let me know if you have any questions?

Posted in PHP

Comment policy: Respectful and beneficial comments are welcome with full open hands. However, all comments are manually moderated and those that doesn't relate with what the passage is saying or offensive comments would be deleted. Thanks for understanding!

Leave a Reply

Your email address will not be published. Required fields are marked *