简体   繁体   中英

Laravel 8 routes to controllers. SEO friendly URL structure

I am trying to figure out how to achieve a specific URL structure in a Laravel 8 project and the necessary route to achieve this. What I want is:

// Example urls to listings in the business directory.
// These urls should be routed to the directory controller.
www.domain-name.com/example-business-name-d1.html
www.domain-name.com/example-business-name-d15.html
www.domain-name.com/example-business-name-d100.html
www.domain-name.com/example-business-name-d123.html
www.domain-name.com/example-business-name-d432.html

// Example urls to articles/posts in the blog.
// These urls should be routed to the posts controller.
www.domain-name.com/example-post-name-p5.html
www.domain-name.com/example-post-name-p11.html
www.domain-name.com/example-post-name-p120.html
www.domain-name.com/example-post-name-p290.html
www.domain-name.com/example-post-name-p747.html

// We want to avoid the more traditional:
www.domain-name.com/directory/example-business-name-1.html
www.domain-name.com/blog/example-post-name-5.html

This is because we don't want the strings “directory” or “blog” contained in the url for every listing or blog post. Search engine results work better without it.

So far I am using a catch-all route {any} at the bottom of the web.php routes file to “catch all” routes that get that far. I then manipulate the string provided by the path to get the ID and single character token from the end of the urls. I then have these 2 variables but can figure out how to pass these onto the right controllers!

Or am I being really dumb and there is a much better way of achieving this?

Route::get('{any}', function($any = null){

    // Break up the url into seperate parts.
    $pieces = explode("-", $any);
    $pieces = array_reverse($pieces);
    $piece =  $pieces[0];

    // Remove the .html
    $piece = substr($piece, 0, -5);

    // Get the two parts of the identifier.
    $id = substr($piece, 1);
    $token = substr($piece, 0, 1);

    // Call correct controller based on the token.
    switch ($token) {
        case "d":
            // HERE I WANT TO PASS THE ID ON TO THE SHOW ACTION OF THE DIRECTORY CONTROLLER 
        break;
        case "p":
            // HERE I WANT TO PASS THE ID ON TO THE SHOW ACTION OF THE POSTS CONTROLLER 
        break;
        default:
            return abort(404);
        break;
    }

});

I would split the path into 2 variables ( $slug and $id ) and directly pass it to the controller.

Route::get('{slug}-d{id}.html', 'DirectoryController@show')
    ->where(['slug' => '([a-z\-]+)', 'id' => '(\d+)']);

Route::get('{slug}-p{id}.html', 'PostController@show')
    ->where(['slug' => '([a-z\-]+)', 'id' => '(\d+)']);

And in your controllers

class DirectoryController
{
    public function show(string $slug, int $id) {}
}

class PostController
{
    public function show(string $slug, int $id) {}
}

I can see two ways of achieving this result:

Create an intermediate controller

Route::get('{path}', 'CheckPathController@redirect')

Then in your CheckPathController you do all the checks and your call the proper controller action:

public function redirect(Request $request, $path) {
  // Your checks on $path, extract $id and content type

  if($isPost) {
     $controller = resolve(PostController::class);
     return $controller->show($request, $id);
  }

  if($isBusiness) {
     $controller = resolve(BusinessController::class);
     return $controller->show($request, $id);
  }

  // No matches, error 404
  abort(404);
}

Complex regex

see: https://laravel.com/docs/8.x/routing#parameters-regular-expression-constraints

I'm not a regexp master, this should be a basic was to match any {word}-{word}-...-p{id}.html pattern but it will break in case of unexpected chars

Route::get('{path}', 'PostController::show')
   ->where(['path' => '([\w]*-)*p[0-9]+\.html$']);

Route::get('{path}', 'BusinessController::show')
   ->where(['path' => '([\w]*-)*d[0-9]+\.html$']);

Note that in this case, you controller will receive the pull $path string, so you will need to extract the id there.

You can match the slug using regex

Route::get('/{any}', 'YourController@methodName')->where(['any' => '.*(-d(.*?)\.).*']);

Repeated with p

Then when you pickup your $site in your controller method you can use regex to grab the site.

public function methodName($site)
{
    preg_match('/.*(-(d(.*?))\.).*/', $site, $parts); //or something similar, $parts[2] will have what you want
}

OR

This will give your controller method d{number} or p{number}

Route::get('/{site}', function($site) {
    $code = preg_match('/.*(-(d(.*?)|p(.*?))\.).*/', $site, $parts) ? $parts[2] : null;

    $controllerName = 'ControllerA';
    if(isset($code) && !is_null($code) && Str::contains($code, 'p')) {
        $controllerName = 'ControllerB';
    }

    $controller = app()->make('App\Http\Controllers\Application\\' . $controllerName);

    return $controller->callAction('methodName', $params = ['code' => $code]);
})->where(['site' => '.*(-(d|p)(.*?)\.).*']);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM