skip to Main Content

I’m building my first Laravel app. I have posts that belongs to different categories.

I’m trying to make SEO friendly urls /categories/category_name for categories without saving a ‘slug’ in database.

Here is what I came with so far:

My category model:

class Category extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function getSlugAttribute(): string
    {
        return str_slug($this->name);
    }

    public function getUrlAttribute(): string
    {
        return action('CategoriesController@index', [$this->id, $this->slug]);
    }
}

My category controller

class CategoriesController extends Controller
{
    public function index($id, $slug = '')
    {
        $posts = Category::findOrFail($id)->posts;

        return view('index', compact('posts'));
    }
}

My route:

Route::get('/categories/{slug}', 'CategoriesController@index')->name('category');

With {{ $post->category->url }} I get something like http://localhost/category/1?alpha but it works and display the appropriate posts. Route is returning an error.

How I get ride of the id? and make the route works?

Thank you in advance for your help!

2

Answers


  1. I don’t think this is possible. The reason behind is that you will need to search for the category using the slug but is not stored in the database, so you will need to convert the slug to its original value.

    For example, if you have a category named “Modern art”, the slug would look something like modern-art. If you pass that slug to your controller, you need to convert it back to “Modern art” to be able to retrieve the category by its name. But maybe the real name was “Modern Art” or “Modern art!” or something else.

    I recommend you storing the slug in the database.

    Login or Signup to reply.
  2. I’m assuming that you have a human readable category name (ex My Category) and you are converting that to a slug in your URLs (ex my-category).

    A naive approach to doing this would be to attempt to reverse the slug and lookup the category by name, but there are use-cases where this won’t work.

    class CategoriesController extends Controller
    {
        public function index($slug)
        {
            $name = ucwords(str_replace('-', ' ', $slug));
            $posts = Category::where('name', '=', $name)->first()->posts;
    
            return view('index', compact('posts'));
        }
    }
    

    The problem with that approach is there are cases where information is lost when converting the name to a slug, for example if the proper category name contains a hyphen, the hyphen becomes a space when converting the resulting slug.


    You can approach this a different way, without storing the slug, by modifying your route:

    Route::get('/categories/{category}/{slug}', 'CategoriesController@index')->name('category');
    

    In this case, you pass both the ID and the slug in the URL. You can either fetch the category from the ID or use route model binding to fetch it automatically. This technically means you can pass any string as the slug and it will still work. You can handle this by determining the actual slug and redirecting to the canonical URL if the end user provides an incorrect slug.

    class CategoriesController extends Controller
    {
        public function index(Category $category, $slug)
        {
            if(str_slug($category->name) != $slug) {
                // Redirect to the canonical URL
            }
    
            $posts = $category->posts;
    
            return view('index', compact('posts'));
        }
    }
    

    A benefit of keeping both the ID and the slug in the URL is you get redirection for free: If the category name changes, the new URL will work and the old URL will automatically redirect to the new one.

    Side note: If you look the URL for his question you’ll notice this is the approach StackOverflow uses.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search