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
TEST 2
TEST 3
TEST 4
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:
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:
Let me know if you have any questions?