Using polymorphic relationships of Laravel for SEO content

This tutorial applies to any applications that are built with Laravel framework and that need some sort of SEO and SEO management tool. You can also apply it as an extension to my previous tutorial on building a shop (where we built a review system).

SEO (Search Engine Optimization) is an important aspect of any website these days. While the tactics for SEO could be varying and there are multiple ways of optimizing a site for search engines, one way of telling the search engines about the content of your pages is to use page title, meta description and meta keywords tags. These three elements play an important in how the search engines view your pages and how the pages are displayed in the search results.

This past week I had an assignment at my job to build an SEO management panel for a large website. The SEO management panel will allow the admins of the site to set the title, description and keyword tags for site’s dynamic and static pages (there are well over 1000 pages).  Using Laravel’s polymorphic relationships I finished the assignment within two days. The tutorial below will explain some of the takeaways that I learned while implementing this task and the fundamentals that I used.

Before we start with the code, let’s first set some tone. Imagine that you have an e-Commerce website that has certain categories of products. Each category has its own page at a URL that uses a slug of category’s name (for example category “Long Sleeve Shirts” would be slug-ified to “long-sleeve-shirts”). Each product also has its own page using its name’s slug as well.

From a simplified shop model above you could see that there are at least 2 types of data in the system that will be visible to the users – categories and products. Of course you could have many many more – tags, brands, colors, manager’s picks, etc. Each of those data types has some sort page that displays the information about that data – category page would show all products under a certain category, product page would show the product and so on.

What if you wanted to implemented SEO for any or all of those data types? 

Solution 1 is to add SEO information as extra DB columns to each type of data. Maybe if you have only 1 or 2 types it is not a problem but when your application has more – your DB would grow considerably having columns like “title”, “description” and “keywords” for each data type.

Solution 2 is to use Laravel’s excellent Polymorphic relationships.

Don’t let the name scare you. Polymorphic relationships are a bit special but they are simpler than you might think. If you haven’t used them, they can greatly simplify the DB structure of an application in certain cases. Polymorphic relationships are extremely helpful when you have a common trait to two or more data types. You will understand the concept better as you read through this post. This is the type of a relationship I will be using for managing SEO content of an application.

Using Polymorphic relationships to store SEO data for Laravel application:

Polymorphic relationship is simply a method of convenience for accessing data in a single DB table that could relate to content from different DB tables. Instead of adding the same extra columns to all DB tables that need them (for example SEO data columns) you could have just one table that would have ID and TYPE of data from other tables stored. See the picture below that shows an actual DB structure if you were to use a polymorphic relationship to store SEO data for multiple types of data (multiple tables):

The lines with arrows point to the names and ids of the tables and that is actually what you will see in the DB in order for the relationship to work.

The SQL to create the SEO table is provided below:

CREATE TABLE `seo` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `description` varchar(255) NOT NULL,
  `keywords` varchar(255) NOT NULL,
  `seoble_id` int(11) unsigned NOT NULL,
  `seoble_type` varchar(255) NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

How can you use it?

First, you would need to create a model for your SEO data. Then you could use that model to all of the data types that you want to be able to relate to. The model for the SEO table is below (store it in app/models/Seo.php):

<?php

class Seo extends Eloquent {

    protected $table = 'seo';

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

}

That’s it! In any other model that you want to use the SEO for, you would just specify $this->morphMany(‘Seo’,’seoble’) as the relationship returned for the seo (as an example I provide the model for Products stored in app/models/Product.php) :

class Product extends Eloquent
{
	...

	public function seo()
	{
	    return $this->morphMany('Seo', 'seoble');
	}

	...
}

Now, if you had the SEO data present in the SEO table, specifying an ID of some product in the DB and the “seoble_type” as “Product”, you could get the SEO data for the specific product and display it like this:

// retrieve the product from DB by ID
$product = Product::find($id);

// Get the SEO data for the product as an object
$seo = $product->seo->first();

// In the Blade view for the product display the SEO data:
<title>{{ isset($seo->title) ? $seo->title : 'Viewing Product'}}</title>
<meta name="keywords" content="{{ isset($seo->keywords) ? $seo->keywords : ''}}">
<meta name="description" content="{{ isset($seo->description) ? $seo->description : ''}}">

Moreover, if you had a model for the categories that would “morphMany(‘Seo’,’seoble’)” and a row for one of the categories available in the DB (in “categories” table), and if you had the ID of that category, then having a row in the “seo” table specifying that ID and the “seoble_type” as “Category”(the name of the model), you could retrieve the SEO data for that category exactly like you would for the product:

// retrieve the category from DB by ID
$category = Category::find($id);

// Get the SEO data for the category as an object
$seo = $category->seo->first();

// In the Blade view for the category display the SEO data:
<title>{{ isset($seo->title) ? $seo->title : 'Viewing Category'}}</title>
<meta name="keywords" content="{{ isset($seo->keywords) ? $seo->keywords : ''}}">
<meta name="description" content="{{ isset($seo->description) ? $seo->description : ''}}">

This is super convenient, isn’t it?

At this point, you could already input data through the DB admin (like PHP myadmin) for all products, categories and more and you could easily show them to the search engines in a way I just described.  Of course there is even a better way of inputting this data.

Over the weekend I will publish another post that will have a link to the source for an admin interface that helps you manage SEO data in the polymorphic relationship table, so stay tuned!

UPDATE: I have released the admin panel interface to manage SEO data, read the details at https://maxoffsky.com/code-blog/releasing-admin-panel-for-seo-polymorphic-relationships-using-laravel/

Enjoy using polymorphic relationships!

Liked it? Take a second to support Maks Surguy on Patreon!
Become a patron at Patreon!

You may also like

35 comments

  • Francisco December 14, 2013  

    Nice! I’m starting with Laravel and these tutorials are incredibly useful and well explained.

    Thank you!

  • Davi December 14, 2013  

    Great article! However as it’s about reducing duplicate columns in the database, and then your example, all the tables have a “name” and a “slug” field. Surely these could be combined into the SEO table as well, as a slug is great for SEO too.

    I usually use a “links” table to contain information regarding slugs and what they relate too, but haven’t done so with the SEO information yet, but I will do now! Thanks.

  • Franz December 14, 2013  

    Hey Maks,

    nice tutorial. You can, by the way, use this shortcut in your Blade view to avoid having to use isset():

    {{ $seo->title or ‘Viewing Category’ }}

  • Jaime Alberto López December 14, 2013  

    Great solution to a common web page requirement. Thanks a lot!

  • Maks Surguy December 14, 2013  

    Thanks Franz! Yes, in Laravel 4.1 this is now a bit easier 🙂 but I know quite a bit of people will still be using 4.0 for some time before they upgrade. I will update tutorial with this tip
    Thanks for the feedback!

  • Maks Surguy December 14, 2013  

    The reason why I wouldn’t put the slug and name columns into SEO table is because of the number of SQL queries that are executed throughout the site. Imagine having products and categories displayed on the home page of your site. If the slugs (that are used to link to category and product pages) and the names (of products and categories) are stored in the SEO table, your Blade templates wouldn’t be as clean and # of SQL queries would be a bit bigger (since you need to retrieve not only the product info from respective tables, but also consult with SEO table). Hence in my opinion it is better to still have some of the things kept in separate tables.

    Does this make good sense?

  • David December 14, 2013  

    It does make sense 🙂 However the title is still stored in there so surely you’d need to access it anyway to get the title of the category etc?

  • Maks Surguy December 14, 2013  

    I would just use the “name” column for that, nothing from SEO column…

  • Maks Surguy December 14, 2013  

    Thanks for the warm words! Make sure you check out http://laravel-tricks.com for more Laravel tips!

  • aditya menon December 15, 2013  

    Thanks for jogging my memory on how Polymorphic relations work, Maxim. Great post!

  • abdelwahed December 23, 2013  

    awsome man, you just made my day, really i love you 😀

  • Nikol Smith (@nikoloviva) May 12, 2014  

    Recently I’ve downloaded Doptor CMS. It is an Integrated and well-designed Content Management System (CMS) that provides the end user with the tools to build and maintain a sustainable web presence. I’m really satisfied.

  • Jesus July 7, 2014  

    Hello Maksim,

    nice and clear article. I’m working on an app with similar relationships and this helped to cleared it out. Thanks!

    I have a question tho. Another app i’m working on, works with the usual User -> Item -> Comment entities and also implements similar polymorphic relationships, but in this case Many to Many, because I wanted some more flexibility in this part (1 User owns 1+ Items. 1 User and 1 Comment owns/have 1+ Comments. 1 Item and 1 Comment belongs to 1 User). The problem is that, deciding to follow this approach is kind of making it more problematic than I wanted. In example, eager loading the relationships, loading nested relationships seems not to work with this approach (at least im missing something).

    Said that, what would be your recommendation for modeling such relationships and make them flexible enough to, for example, being able to add comments not only to Items, but to Users too (user’s profile). Extending the functionality to Flagging to flag comments, items and users, etc. etc.

    Thanks, any comments is appreciated.
    -J.

  • Bogdan September 10, 2014  

    This is nice. How would you make this work in a collection of Products? so with an Product::all() instead of by id. Is there a way to do this without looping through them?

  • Md. Sahadat Hossain September 21, 2014  

    I think this would be more clear if you show example with some demo data. Personally I would be more clear if you show example with demo data.

  • Brian December 15, 2017  

    I had the same thought as David. It seems like you could more easily validate and enforce uniqueness of the slugs if they were in a single field in a common table. Then routes could hit a common controller with a findBySlug() method. I am considering using this design for a pages table with polymorphic relationships to represent different types of pages as some page types have data points that others will not have.

    I would appreciate any feedback detailing why storing the urls in the common table is less performant. I realize this post is several years old, but the concepts are still the same!

  • maxsurguy December 17, 2017  

    @brian_seymour:disqus thanks for resurrecting the conversation!

    Perhaps I misunderstood what both of you were proposing and maybe a diagram of some sort would be of help? Could you describe what you’re proposing in a table structure diagram?

    Thanks!

  • Brian December 26, 2017  

    Hey Maks, if we stick your schema, my inclination is to remove the slug field from the Categories, Posts and Products tables and instead include it in the SEO table. And also change the relationship from morphMany to MorphOne. I am going to play around with architecture a bit this a bit for an upcoming project. I typically install Laravel Debugbar to help keep an eye on the number of queries and execution time.

    Thank you for your articles and feedback!

Leave a comment