How to create SEO-friendly URLs in Laravel?

Bharat Jain
Jan 7 · 4 min read

To increase users traffic on our website, SEO plays a very important role. If your website have seo friendly URL then it can help to increase your site rank in search engines like google, yahoo, etc..

In this article, I am going to share the idea with you “How to create non-breaking SEO friendly URL in Laravel 5.x application?”

Factors that affect SEO

This is not a comprehensive tutorial on SEO. It only introduces you to what you need to know that your role in SEO as a developer. Following are some of the factors that search engines such as Google Search consider when evaluating web sites

  1. : No one loves websites that take forever to load. We all love fast websites. The goal should be to keep the load time under 2 seconds. If you can get it under a second that is even much better. This role falls to you as the developer.
  2. Responsive designs: Mobile devices have a significant market share of internet usage. Since user experience matters to search engines, you need to ensure that the web site displays properly in mobile devices, tablets and desktops
  3. Keywords: search engines look at keywords when querying billions of indexed websites. Your role as a developer is to ensure you provide title tags, meta description and HTML H2 heading that the content writers can use to place keywords.
  4. Website URLs: The URLs should be keyword rich and words should be separated by dashes and not underscores.

Problem: When admin create or update any entity, a url slug can be generated based on it’s title. The downside here is that when the title changes, the old url would break. If we wouldn’t regenerate the url on updates, edited titles would still have an old slug in the url, which isn’t an ideal situation either.

Solution: Add a unique identifier to the url that will never change, while keeping the slug intact. This creates links that are both readable and unbreakable.

Determining the Identifier

Assuming we’re using a relational database like MySQL, the simplest form of an identifier is something we already have: the model’s ID.

An incrementing ID can expose a lot though. It makes it easy for someone or something to crawl through an entire dataset, and it provides an indication of it’s size.

If we’d want to obfuscate our ID’s, we could either use a library to hash the existing ID like . For this article, I will use model’s ID.

Setup your Routes and the Controller

We want following URL structure:

https://yourwebsite.com/posts/<id>/<slug>

So in Laravel route file, we would have something like this:

Route::get('/posts/{id}/{slug}', 'PostController@detail')

In our controller, we need ID to retrieve the post item, the slug only exists to make the url human-readable or to make url SEO-friendlier.

use App\Models\Post;  class PostController {     public function detail($id)
{
return view('post.detail')
->withPost(Post::findOrFail($id));
}
}

Since Post maps to a single url in this context, let’s create a computed url attribute which returns an url to the post's detail page so we don't have to repeat this to generate url in our whole application.

class Post extends Model {     // ...     public function getUrlAttribute(): string
{
return action('PostController@detail', [$this->id, $this->slug]);
}
}

One more thing, since we don’t care about the slug, we might as well make it optional.

Route::get('/post/{id}/{slug?}', 'PostController@detail')

Avoiding Duplicate URLs for same content

Links to your old pages won’t break anymore, but having multiple urls pointing to the same piece of content isn’t a good idea either since that creates duplicate content. To prevent this, old links should respond with a redirect to the correct url.

Let’s modify our controller’s detail method. This time, we'll need to pull in the slug to find out if it represent's the latest revision of the post’s title.

Since we’re going to compare the request slug with the post slug, we’ll need to inject the route segment in our controller. The slug segment is optional so we’ll assign an empty string by default.

Validating the slug should be easy, all we need to do is a simple string comparison! If the post slug doesn’t match the request slug, we’ll redirect the visitor to the correct url.

use App\Models\Post;class PostController {    public function detail($id, $slug = '')
{
$post = Post::findOrFail($id);
if ($slug !== $post->slug) {
return redirect()->to($post->url);
}
return view('post.detail')
->withPost($post);
}
}

Alternative to Redirects: Canonical Links

If we don’t want an actual redirect — or if we don’t want that annoying conditional logic in our controller — we could use a canonical link tag instead.

Let’s add a link tag in our layout file if we've explicitly provided one.

<html>
<head>
@if(isset($canonical))
<link rel="canonical" href="{{ $canonical }}" />
@endif
</head>
<body>
@yield('content'))
</body>
</html>

Then we don’t have to handle redirects in our controller anymore, but we need to share the canonical link (which is the post’s url) with the view.

use App\Models\Post;class PostController {     public function detail($id, $slug = '')
{
$post = Post::findOrFail($id);
return view('post.detail')
->withPost($post)
->withCanonical($post->url);
}
}

Conclusion

We have achieved our two goals:

  1. We can change the post’s title without worrying about breaking old links.
  2. The urls will have a human readable slug.

NOTE: You can get a similar setup, like storing slugs in the database and do some changes accordingly, it’s all up to you to decide which will fit the best for your application.

Bharat Jain

Written by

I'm a Full Stack Developer at IndiaNIC. I enjoy building web applications people love to use. More about me on bigbharatjain.github.io