facebook youtube pinterest twitter reddit whatsapp instagram

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?

Related Post(s)

  • Laravel - Basic Routing & Controllers

    If you haven't read my guide on Creating a Tiny PHP MVC Framework From Scratch then you should do that immediately, the concept of the guide applies to the way things work in Laravel behind the scen

  • Laravel Blade Templating Engine

    In our last guide, we discussed the basics of Laravel routing and controllers, in this guide, you'll learn how to customize your Laravel views with the Blade templating engine. So far, here is my vie

  • Laravel - Edit and Delete Data (+ User Authentication)

    In this guide, you'll learn how to edit and delete post data in Laravel, before proceeding you should read the previous guides as this guide would be the continuation, here are the previous guides:

  • Guide To Laravel - Model and Database Migrations

    I don't know if you have read my guide on Creating a Tiny PHP MVC Framework From Scratch where we create a tiny MVC framework in the hope of understanding the concepts of how major frameworks imple

  • Building Dependency Injection and The Container from Scratch in PHP

    In this guide, you'll learn about dependency injection and a way to build a simple DIC (Dependency Injection Container) in PHP using PSR-11 from scratch. First, What is an Ordinary Dependency? This

  • Creating a Tiny PHP MVC Framework From Scratch

    In this guide, we would go over creating a tiny PHP MVC Framework, this would sharpen your knowledge on how major frameworks (e.g Codeigniter or Laravel) works in general. I believe if you can unders