The Power of Laravel’s Route ‘defaults’ for making root level SEO pages

So search engine optimisation is something a lot of backend developers dread dealing with. It’s critical to the growth of a website but SEO in itself is a whole other task that complex and generally quite uninteresting. Often the content for a backend is dynamic but still needs to be at the forefront of the content that a site delivers.

As a backend engineer you’ll often be asked to produce URL patterns that just don’t work with the rest of the site without breaking your current routing structure. Often you’ll create what’s known as a slug for your content, a simple hyphen separated string which is unique in the system. A typical slug would be just generated from the title like “My Simple Article” becomes as a slug my-simple-article. This way there’s a unique string in the system for each post.
If you’ve already been implementing routes like this in your system you’ll likely have urls that look like /post/{slug} but you know now that’s not going to be good enough. Your company’s marketing team or SEO wizards want it to be /{slug} and that’s pretty tricky. You can’t create the pattern /{post-slug} because it’s going to confuse the system. What is you have an About Us page or a Contact Us page which equally important urls like /about-us and /contact-us respectively. The problem here being that the routing system might pick up the /about-us link and believe it’s meant to be a slug for a Post model. At this point Laravel will simply not find the model and throw a HTTP 404 error instead. Not good.

This is where the ‘defaults’ method on routes comes into use to save the day.

Using default parameters

So for those of you who don’t know (and you likely won’t because it’s not in the official documents) you can create fixed routes in Laravel that specifies the parameters from the controller method. It’s a relatively hard thing to explain in words but here’s an example of it in use:

$post = Post::find(1);
Route::get(‘path-to-my-post’, ‘PostController@showPost’)
->defaults(‘post’, $post);

So all we’ve done is made a static route called /path-to-my-post that when browsed will use the Post model object as the parameter to the PostController showPost method. The controller method looks like this:

public function showPost(Post $post) {
return view(‘post’)->with(‘post’, $post);
}

This then forms the basis of the fix to our problem but it’s still a bit hack-ish in how it’s implemented, namely if we implement this in the Routes file it’s going to be ugly and a bit prone to breaking the file. My feelings has always been that your routes files should avoid using forks or loops to avoid the application breaking in such a critical place where little debug information will be provided. To rectify this we first need to create a service provider which will handle producing the routes for each model.

php artisan make:provider SeoServiceProvider

Then in the service provider’s boot method we can implement a mechanism for creating all the Post routes.

/** @var Router $router */
$router = app()->make(‘router’);
$posts = Post::all();
$posts->each(function (Post $post) use ($router) {
$router->get($post->slug, ‘App\Http\Controllers\PostController@showPost’)
->defaults(‘post’, $post);
});

We can then test this out by using the route:list command in artisan.

php artisan route:list

We’ll then see a complete list of the routes generated, including those created by our SeoServiceProvider.

URL generation

One of the partial downsides to this is that you can’t use the typical measures for generating via the route helper.

Instead we should modify the creation of our routes to make fixed names.

public function boot()
{
/** @var Router $router */
$router = app()->make(‘router’);
    $posts = Post::all();
    $posts->each(function (Post $post) use ($router) {
$router->get($post->slug, ‘App\Http\Controllers\PostController@showPost’)
->defaults(‘post’, $post)
->name($post->slug)
->middleware(‘web’);
    });
}

This means we will simply use the route helper like:

route($post->slug);

which will allow us to be sure the urls generated stay consistent to the slug in the database.

This overall is a simple but effective way to handle generating dynamic routes for content. Obviously this is particularly risky if you need to create root level for different models though. At that point we would need to create a polymorphic table for the slugs to attach to other models.

Page model

To make this work we create a Page model which will now hold our URL slug and the polymorphic fields for attaching to other models.

php artisan make:model Page --migration

Schema::create(‘pages’, function (Blueprint $table) {
$table->increments(‘id’);
$table->unsignedInteger(‘browseable_id’);
$table->string(‘browseable_type’);
$table->string(‘slug’)->unique();
$table->timestamps();
});

I’ve called this relationship between pages and models Browesable.

Then all we need do is make the right methods in each model.

public function browseable()
{
return $this->morphTo();
}

and in any of our subsequent models that will make use of the page model (such as the Post model) we will put the following method.

public function page()
{
return $this->morphOne(Page::class, ‘browseable’);
}

This does leave us with a problem again though. We’ll have to produce an interface that all of our browseable models can implement so we can map which parameter and which method our model will use.

In this case we simply create an interface called Browseable.

interface Browseable
{
/**
* @return string
*/
public function getAction();
    /**
* @return string
*/
public function getParameterName();
}

Once we we implement this into a model like Post we need to establish the route and parameter name.

public function getAction()
{
return ‘App\Http\Controllers\PostController@showPost’;
}
public function getParameterName()
{
return ‘post’;
}

Ok, now that’s done we can modify our previous SeoServiceProvider’s boot function to look like the following.

public function boot()
{
/** @var Router $router */
$router = app()->make(‘router’);
    $pages = Page::with(‘browseable’)->get();
    $pages->each(function (Page $page) use ($router) {
$browseable = $page->browseable;
        $router->get($page->slug, $browseable->getAction())
->defaults($browseable->getParameterName(), $browseable)
->name($page->slug)
->middleware(‘web’);
});
}

Wrapping up

So there we have it, a simple mechanism for generating root level URLs for our site’s content without having clashes between different kinds of content or any previously made root level pages. Obviously anything like this could become really process intensive and should ideally have some caching implemented around the database queries that become refreshed upon creating a new article. If you just like to dive into the code yourself or play around with it, feel free to with my repo.

I’m hoping to continue with these types of everyday challenges for Laravel. Please like, share and comment on this article if you found it helpful as it really helps to encourage me to take the time to make more content. Thank you for reading.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.