DoeJohn's avatar

SEO friendly URLs with category/subcategories/article slug?

Hi,

First of all, I have Article model and articles table in the database. Each article can be shown using Laravel's standard URI structure: www.example.com/articles/5 (where 5 is the article id). Each article has a slug field (slug column in the articles table) , so with Route Model Binding it is easy to change this and have a slug instead of id in the URI:

In RouteServiceProvider.php I just added:

    public function boot(Router $router)
    {
        parent::boot($router);

        \Route::bind('articles', function($slug) {
            return \App\Article::where('slug', $slug)->firstOrFail();
        });
    }

... and now I can open articles with: www.example.com/articles/this-is-some-slug .

On the other hand, each article belongs to one category. For example, let's say that there are the following categories:

  • Politics
  • Sport
    • Football
    • Tennis
      • ATP
      • WTA
  • Culture

I created these categories by using Baum (an implementation of the Nested Set pattern for Laravel 5's Eloquent ORM). So there is a Category model and categories table in the database:

            $table->increments('id');
            $table->string('name');

            $table->integer('parent_id')->nullable();
            $table->integer('lft')->nullable();
            $table->integer('rgt')->nullable();
            $table->integer('depth')->nullable();

            $table->timestamps();

Of course, in articles table there is a column category_id because of One-to-Many relationship (one Article belongs to one Category, one Category can have many Articles).

All articles belonging to some category can be displayed via the following URL: www.example.com/articles/category/1 (where 1 is the category id). If we add slug column to the categories table & set Route Model Binding :

        \Route::bind('category', function($slug) {
            return \App\Category::where('slug', $slug)->firstOrFail();
        });

then we use a slug instead of id: www.example.com/articles/category/politics (this will display all the articles belonging to the category politics).

But I would like to have URIs with the following structure:

  • www.example.com/sport/tennis/wta/article_slug (/category/subcategory/subcategory/article_slug)
  • www.example.com/politics/article_slug (/category/article_slug ) and so on... I hope you understand what I want.

The problem is that I have no idea how to do this with Laravel. Is it even possible? How would you solve this problem?

Thanks in advance and sorry for my bad English.

0 likes
4 replies
spekkionu's avatar
$article = \App\Article::where('slug', $article_slug)->whereHas('category'', function($query){
    $query->where('slug', $category_slug);
} use ($category_slug))->firstOrFail();
1 like
spekkionu's avatar

For deeply nested categories it might be better to store the full path in a field or cache to avoid having to use a recursive function every time you need to pull data. You will have to update this field or clear the cache whenever the path would change.

3 likes
DoeJohn's avatar

@spekkionu Thanks for the replies. What did you mean when you wrote that it might be better to store the full path in a field? If I understand well, in articles table I'll need to have one column named path in which I would store the full path. For example, if there is some article that belongs to a Tennis category which is a child of Sport category, then I would need to store sport/tennis/article_slug in path column, right? If yes, then how would you setup in routes.php & RouteServiceProvider.php ? Sorry for my bad English.

spekkionu's avatar

This is actually going to be a bit tricky as there isn't a way to tell if something is a category or an article just by the url.

You will need to setup a catchall route which must be at the end of your routes file as it will match all urls.

Route::any( '{path}', [
    'uses' => 'controller@action'
] )->where('path', '(.*)');

In your controller you will then need to try and pull an article by the path, then if one is not found try to pull a category by the path. If neither is found throw a NotFoundHttpException.

You can then call a different method based on if you found an article or a category that does any extra work you need on those pages and returns the appropriate view.

3 likes

Please or to participate in this conversation.