Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

robineero's avatar

Route using /{slug} with /{id} as a fallback if slug does not exist

Hello! It is my first topic here :) I would like to get help refactoring this GET routing in the most Laravel way. Maybe someone can help me out here with a reference or some guidance on how it should be achieved. I believe everyone building a web app with even a minimum SEO in mind has tackled this problem.

Thanks to everyone in advance for thinking along.

Business model:

class Business extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'slug',
        'is_visible'
    ];
}

BusinessController show() method at the moment. Just returns Business from GET /{slug} to Blade view:

public function show(String $slug)
    {
        $business = Business::where('slug', $slug)
                            ->orWhere('id', $slug)
                            ->first();

        /* No business found */
        if (!$business) {
            abort(404);
        }

        /* Redirect if request is done by id but slug for the Business exists */
        if ($slug === strval($business->id) && $business->slug !== null) {
            return redirect()->action(
                [BusinessesController::class, 'show'],
                ['slug' => $business->slug],
                301);
        }

        $data = [
            'business' => $business,
        ];

        return view('businesses.show', $data);
    }

Routes:

/* I needed to redirect pages that were already indexed by Google. I don't like it because the parameter is actually id, not a slug. So I should be able to name it {id} for better readability but I can't figure out how to achieve it. In Java and C# there is method overloading for this (same method but different parameters). */
Route::permanentRedirect('/businesses/{slug}', '/{slug}');

/* Same issue here with naming the parameter. */
Route::get('/{slug}', [BusinessesController::class, 'show'])
    ->name('businesses.show');

This is the behaviour that I would like to achieve

If the slug for the entity does not exist then the fallback is id (PK):

/{id}

If slug for the entity exists then redirect the id to slug or use slug directly:

/{id} -> /{slug}
/{slug}
0 likes
3 replies
robineero's avatar

The direction I am thinking towards is something like this.

/* Got rid of {slug} parameter where ids are used */
Route::permanentRedirect('/businesses/{id}', '/{id}');

/* Still not happy with this because it is either slug or id. Not always slug as the name refers. Even though slug and id in the database can be equal (int 1 as id and sting "1" as slug). */
Route::get('/{slug}', [BusinessesController::class, 'show'])
->name('businesses.show');

Using parameters with nullable types and default values allows using the same same show() method for multiple routes but the method itself does not get cleaner :)

public function show(?String $slug = null, ?int $id = null)
{
    /* GET /{id} redirect to /{slug} */
    if ($id !== null) {

        $business = Business::find($id);

        if (!$business) {
            abort(404);
        }

        $slug = $business->slug ?? $id;

        return redirect()->action(
            [BusinessesController::class, 'show'],
            ['slug' => $slug],
            301);
    }
    
    /* GET /{slug} */
    $business = Business::where('slug', $slug)
                        ->orWhere('id', $slug)
                        ->first();

    if (!$business) {
        abort(404);
    }

    /* Another redirect if provided slug equals id but slug for the Business exists */
    if ($business->slug !== null && $slug === strval($business->id)) {
        return redirect()->action(
            [BusinessesController::class, 'show'],
            ['slug' => $business->slug],
            301);
    }

    $data = [
        'business' => $business,
        'metaTitle' => $business->name
    ];

    return view('businesses.show', $data);
}
Snapey's avatar

see my site here

https://speakernet.co.uk/talk/648/a-butterfly-garden-and-a-changing-environment-option-to-present-via-zoom

The id in the URL is used always, and the slug is added on for seo purposes and is never used in the finding of the right entry.

The route looks like /talk/{id}/{slug} but I don't use the slug in the controller at all.

The user can change the title of their talk at any time and it does not result in a broken link

An improvement would be to accept the slug into the controller, compare it to the item's current slug and if they differ, issue a 301 (permanent redirect) to the same item with the current slug.

robineero's avatar

@Snapey This is one possible option and an interesting one to be honest. Thank you for sharing. I would still like to stick to the domain.com/{slug} option for business profiles on my page. I'll continue playing around and let's see if anyone here comes up with similar issue in the future.

Please or to participate in this conversation.