Andrew Welch · Tips · #SEO #SEOmatic #craft-3

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

Advanced SEOmatic Tips

SEO­mat­ic is a plu­g­in that offers deep func­tion­al­i­ty. In this arti­cle, we dive in to look at some of the more advanced fea­tures that will make your life eas­i­er, and your SEO better.

Seomatic seo icon banner

SEO­mat­ic is a plu­g­in for Craft CMS 3 that offers a com­pre­hen­sive SEO solu­tion for your web­sites. It’s easy to get up and run­ning with it quick­ly, but it also has a depth of func­tion­al­i­ty when you’re ready to dive in.

In this arti­cle, I’m going to show you some more advanced things about SEO­mat­ic that you might not know, and give you my favorite tips and tricks for using it effectively.

So let’s get going!

Link Customized Setup

If you peek into the SEO­mat­ic plug­in’s fold­er struc­ture, you’ll see a direc­to­ry called seomatic-config that looks pret­ty unassuming:

vagrant@homestead ~/sites/craft3/vendor/nystudio107/craft-seomatic (develop) $ tree -L 2 src/seomatic-config/
src/seomatic-config/
├── categorymeta
│   ├── Bundle.php
│   ├── BundleSettings.php
│   ├── GlobalVars.php
│   ├── JsonLdContainer.php
│   ├── LinkContainer.php
│   ├── ScriptContainer.php
│   ├── SitemapVars.php
│   ├── TagContainer.php
│   └── TitleContainer.php
├── entrymeta
│   ├── Bundle.php
│   ├── BundleSettings.php
│   ├── GlobalVars.php
│   ├── JsonLdContainer.php
│   ├── LinkContainer.php
│   ├── ScriptContainer.php
│   ├── SitemapVars.php
│   ├── TagContainer.php
│   └── TitleContainer.php
├── fieldmeta
│   ├── Bundle.php
│   ├── BundleSettings.php
│   ├── GlobalVars.php
│   ├── JsonLdContainer.php
│   ├── LinkContainer.php
│   ├── ScriptContainer.php
│   ├── SitemapVars.php
│   ├── TagContainer.php
│   └── TitleContainer.php
├── globalmeta
│   ├── Bundle.php
│   ├── BundleSettings.php
│   ├── Creator.php
│   ├── FrontendTemplatesContainer.php
│   ├── GlobalVars.php
│   ├── Identity.php
│   ├── JsonLdContainer.php
│   ├── LinkContainer.php
│   ├── ScriptContainer.php
│   ├── SitemapVars.php
│   ├── SiteVars.php
│   ├── TagContainer.php
│   └── TitleContainer.php
└── productmeta
    ├── Bundle.php
    ├── BundleSettings.php
    ├── GlobalVars.php
    ├── JsonLdContainer.php
    ├── LinkContainer.php
    ├── ScriptContainer.php
    ├── SitemapVars.php
    ├── TagContainer.php
    └── TitleContainer.php

5 directories, 49 files

Big deal, right? Well, it kind of is… because these con­fig files con­tain the blue­print for the set­tings that SEO­mat­ic will use when it cre­ates var­i­ous types of meta information.

Nor­mal­ly this would­n’t be that inter­est­ing, except that SEO­mat­ic lets you over­ride any of these built-in con­fig files with your own!

For exam­ple, if you cre­ate many web­sites using SEO­mat­ic, there may be cer­tain things that you’d like to have set­up just the way you like it, such as the Cre­ator infor­ma­tion (that’s you!).

So we can put some­thing like this in our Craft config/ directory:

vagrant@homestead ~/sites/nystudio107 (develop) $ tree -L 3 config/seomatic-config/
config/seomatic-config/
└── globalmeta
    └── Creator.php

1 directory, 1 file

Note that the fold­er struc­ture and file name mir­rors what is in the seomatic-config direc­to­ry in SEO­mat­ic itself. What SEO­mat­ic does is it looks in the Craft config/ direc­to­ry first for all of its con­fig files, and it uses them first, rather than its own inter­nal version!

Here’s what mine looks like:

<?php
/**
 * SEOmatic plugin for Craft CMS 3.x
 *
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
 * and flexible
 *
 * @link      https://nystudio107.com
 * @copyright Copyright (c) 2017 nystudio107
 */

/**
 * @author    nystudio107
 * @package   Seomatic
 * @since     3.0.0
 */

return [
    '*' => [
        'siteType'                     => 'Organization',
        'siteSubType'                  => '',
        'siteSpecificType'             => '',
        'computedType'                 => 'Organization',
        'genericName'                  => 'nystudio107',
        'genericAlternateName'         => 'nys',
        'genericDescription'           => 'We do technology-based consulting, branding, design, and development. Making the web better one site at a time, with a focus on performance, usability & SEO',
        'genericUrl'                   => 'https://nystudio107.com/',
        'genericImage'                 => 'https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/site/nys_logo_seo.png',
        'genericImageWidth'            => '1042',
        'genericImageHeight'           => '1042',
        'genericImageIds'              => [],
        'genericTelephone'             => '',
        'genericEmail'                 => 'info@nystudio107.com',
        'genericStreetAddress'         => '',
        'genericAddressLocality'       => 'Webster',
        'genericAddressRegion'         => 'NY',
        'genericPostalCode'            => '14580',
        'genericAddressCountry'        => 'US',
        'genericGeoLatitude'           => '43.2365384',
        'genericGeoLongitude'          => '-77.49211620000001',
        'personGender'                 => '',
        'personBirthPlace'             => '',
        'organizationDuns'             => '',
        'organizationFounder'          => 'Andrew Welch, Polly Welch',
        'organizationFoundingDate'     => '2013-05-02',
        'organizationFoundingLocation' => 'Webster, NY',
        'organizationContactPoints'    => [],
        'corporationTickerSymbol'      => '',
        'localBusinessPriceRange'      => '',
        'localBusinessOpeningHours'    => [],
        'restaurantServesCuisine'      => '',
        'restaurantMenuUrl'            => '',
        'restaurantReservationsUrl'    => '',
    ],
];

Boom! For any new web­site I cre­ate, I just have this in my boil­er­plate, so that the Cre­ator infor­ma­tion is filled in auto­mat­i­cal­ly for me.

You can do this with absolute­ly any­thing in SEO­mat­ic, but I would­n’t go too crazy: if you over­ride any of the *Container.php files, you may not get any inter­nal updates I make to SEO­mat­ic that affect those files.

But if you’re will­ing to sync things your­self, go for it! I per­son­al­ly stick to the var­i­ous *Vars.php files and Creator.php

N.B.: this does­n’t work like Project Con­fig; this is just the tem­plate that is used to cre­ate new meta infor­ma­tion. It does­n’t retroac­tive­ly change any set­tings you’ve already made in the CP.

Link Rendering Metadata from another Route

It hap­pens some­times that you end up with a route that does­n’t have meta infor­ma­tion asso­ci­at­ed with it. Per­haps you’re using a cus­tom router, or per­haps you’re mak­ing a Google AMP ver­sion of a par­tic­u­lar page.

Would­n’t it be nice if there was a way you could tell SEO­mat­ic to pop­u­late the page with meta infor­ma­tion from anoth­er route? Well, you can!

As it turns out, SEO­mat­ic makes a num­ber of very use­ful Helper Func­tions avail­able in your tem­plates via seomatic.helper. One in par­tic­u­lar is just what we want: seomatic.helper.loadMetadataForUri()

You pass in a URI and an option­al SiteID, and it’ll load in the meta data for that URI. Here’s an exam­ple of how I use it for the AMP pages on dev​Mode​.fm, which have /amp append­ed to the end of them:

{% extends "_layouts/amp-layout.twig" %}

{% if entry is not defined %}
    {% set entrySlug = craft.app.request.getSegment(2) %}
    {% set entry = craft.entries({
        "section": "episodes",
        "slug": entrySlug,
    }).one() %}
{% endif %}

{% do seomatic.helper.loadMetadataForUri(entry.uri) %}
{% do seomatic.script.container().include(false) %}

This pop­u­lates the meta data for the Google AMP page (which does­n’t have an entry auto­mat­i­cal­ly inject­ed into it) by using seomatic.helper.loadMetadataForUri(entry.uri) to load the meta infor­ma­tion from the nor­mal page.

This tech­nique can be used in a vari­ety of sit­u­a­tions to make your life easier.

As an aside, the seomatic.script.container().include(false) state­ment tells SEO­mat­ic not to include any of the script tags on this page, since third par­ty JavaScript isn’t allowed for AMP.

Link Robust Twig API

What the pre­vi­ous exam­ple starts to reveal is that SEO­mat­ic has a robust Twig Tem­plat­ing API that you can lever­age when you real­ly need to cus­tomize things.

The way it works is that SEO­mat­ic takes its default set­tings, com­bines them with your Glob­al, Con­tent, and Entry SEO set­tings from the Craft CP, and injects the appro­pri­ate meta tags into con­tain­ers.

SEO­mat­ic uses the fol­low­ing containers:

  • jsonLd — avail­able via seomatic.jsonLd in your tem­plates, this con­tains all of the JSON-LD struc­tured data for the page
  • link — avail­able via seomatic.link in your tem­plates, this con­tains all of your <link> tags
  • tag — avail­able via seomatic.tag in your tem­plates, this con­tains all of your <meta> tags
  • title — avail­able via seomatic.title in your tem­plates, this con­tains your <title> tag

These con­tain­ers all use the same con­sis­tent api as described in the SEO­mat­ic Tags & Con­tain­ers sec­tion of the doc­u­men­ta­tion, so you can do things like:

{% do seomatic.tag.get("description")
  .content("Some Description")
  .include(false)
%}

{% do seomatic.script.get("googleAnalytics").include(false) %}

{% do seomatic.title.get("title").content("My page title") %}

All of the con­tain­ers and tags are actu­al­ly PHP objects under the hood that self-val­i­date, and allow you to manip­u­late them.

There’s even a self-val­i­dat­ing object for each of the 500+ schema​.org JSON-LD types! The JSON-LD Meta Object Func­tions sec­tion of the doc­u­men­ta­tion gives you a num­ber of exam­ples and use-cas­es, such as:

{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}

{% set offersJsonLd = seomatic.jsonLd.create({
    'type': 'Offer',
    'name': 'Some prop',
    'url': 'Some url',
}, false) %}

{% do mainEntity.offers(offerJsonLd) %}

What this does is cre­ate a new Offer JSON-LD object, and adds it to the exist­ing mainEntityOfPage JSON-LD objec­t’s offers property.

The false para­me­ter passed in to seomatic.jsonLd.create() tells SEO­mat­ic not to add the JSON-LD object to the jsonLd con­tain­er, because we’re adding it to a JSON-LD object that’s already in the jsonLd con­tain­er. Any objects that are in the jsonLd con­tain­er are auto­mat­i­cal­ly ren­dered on the page.

If you want to learn more about JSON-LD, check out the JSON-LD, Struc­tured Data and Erot­i­ca article.

Since all of the tags and con­tain­ers are self-val­i­dat­ing mod­els, if you have devMode on, your Yii2 Debug Tool­bar will show all sorts of infor­ma­tion. For more infor­ma­tion on the Yii2 Debug Tool­bar, check out the Pro­fil­ing your Web­site with Craft CMS 3’s Debug Tool­bar article.

Self validating json ld seomatic

I’m not going to dupli­cate what’s in the doc­u­men­ta­tion on the Twig API, but more just make sure you real­ize it’s avail­able to you!

You can use SEO­mat­ic with­out ever using the Twig API, but it is there if you need to do some­thing cus­tom. And it allows you com­plete con­trol to change what­ev­er you like.

A real­ly com­mon thing to do is just to change the SEO­mat­ic Meta Vari­ables that are used to pop­u­late the var­i­ous tags. e.g.:

 {% do seomatic.meta.seoTitle(category.title) %}

Link Caching and Cache Busting

SEO­mat­ic does a lot. It ren­ders a ton of meta data, and since it pars­es all of the SEO fields in the CP as Twig tem­plates, this can be expensive.

Because of this, SEO­mat­ic has a two-lev­el cache to keep things performant:

  • It caches the meta con­tain­ers and their tag con­tents before they are rendered
  • It then also caches the ren­dered result for all of the meta con­tain­ers and their tag contents

It works this way so that it can cache the con­tain­ers that are made avail­able to your Twig tem­plates, but then also allow you to change things how­ev­er you please via Twig. Thus it needs a sec­ond cache as well.

The cache used by SEO­mat­ic is what­ev­er you’ve set Craft to use as a Caching method, so if you clear Craft’s data caches, you’ll also be clear­ing SEO­mat­ic’s caches.

Clear­ing the data caches some­thing we rec­om­mend you do on deploy­ment, as dis­cussed in the Explor­ing the Craft CMS 3 Con­sole Com­mand Line Inter­face (CLI) article.

The cache SEO­mat­ic uses is on a per-URI basis, and nor­mal­ly you don’t need to wor­ry about it at all… but if you are chang­ing tags or oth­er­wise mod­i­fy­ing SEO­mat­ic meta data in a way that can be dif­fer­ent per request for the same URI, you may need to tell the con­tain­er to bust its cache.

A real-world exam­ple would be a cook­ie con­sent sys­tem where you want the user to accept cer­tain types of cook­ies before you include the Google Tag Man­ag­er script. This varies from user to user for the same URI, so you’ll need to bust the cache:

{% if not consentedToCookies %}
    {% do seomatic.script.get('googleTagManager').include(false) %}
    {% do seomatic.script.container().clearCache(true) %}
{% endif %}

The vast major­i­ty of the time, this will not be need­ed, and SEO­mat­ic will take care of the cache bust­ing itself. You only need to use the .clearCache() method if you’re chang­ing meta data via Twig and it can change from request to request for the same URI.

One oth­er quick caching tip: if you’re using Craft in a load-bal­anced envi­ron­ment, make sure that your cache is coher­ent across the var­i­ous servers!

Link A Peek Under the Hood

I men­tioned ear­li­er that SEO­mat­ic puts every­thing into con­tain­ers, and that all of your tags are actu­al­ly PHP objects. But let’s look at what this actu­al­ly means.

So let’s dump the tag con­tain­er and see what’s inside:

{{ dump(seomatic.tag.container()) }}

You’ll see an out­put that will look some­thing like this:

/home/vagrant/sites/craft3/vendor/twig/twig/src/Extension/DebugExtension.php:61:
object(nystudio107\seomatic\models\MetaTagContainer)[3727]
  public 'data' => 
    array (size=5)
      'generator' => 
        object(nystudio107\seomatic\models\MetaTag)[3711]
          public 'charset' => string '' (length=0)
          public 'content' => string 'SEOmatic' (length=8)
          public 'httpEquiv' => string '' (length=0)
          public 'name' => string 'generator' (length=9)
          public 'property' => null
          private '_errors' (yii\base\Model) => null
          private '_validators' (yii\base\Model) => null
          private '_scenario' (yii\base\Model) => string 'default' (length=7)
          private '_events' (yii\base\Component) => 
            array (size=0)
              ...
          private '_eventWildcards' (yii\base\Component) => 
            array (size=0)
              ...
          private '_behaviors' (yii\base\Component) => 
            array (size=0)
              ...
          public 'include' => boolean true
          public 'key' => string 'generator' (length=9)
          public 'environment' => null
          public 'dependencies' => 
            array (size=1)
              ...
      'keywords' => 
        object(nystudio107\seomatic\models\MetaTag)[3478]
          public 'charset' => string '' (length=0)
          public 'content' => string '{seomatic.meta.seoKeywords}' (length=27)
          public 'httpEquiv' => string '' (length=0)
          public 'name' => string 'keywords' (length=8)
          public 'property' => null
          private '_errors' (yii\base\Model) => null
          private '_validators' (yii\base\Model) => null
          private '_scenario' (yii\base\Model) => string 'default' (length=7)
          private '_events' (yii\base\Component) => 
            array (size=0)
              ...
          private '_eventWildcards' (yii\base\Component) => 
            array (size=0)
              ...
          private '_behaviors' (yii\base\Component) => 
            array (size=0)
              ...
          public 'include' => boolean true
          public 'key' => string 'keywords' (length=8)
          public 'environment' => null
          public 'dependencies' => null
      'description' => 
        object(nystudio107\seomatic\models\metatag\DescriptionTag)[3477]
          public 'charset' => string '' (length=0)
          public 'content' => string '{seomatic.meta.seoDescription}' (length=30)
          public 'httpEquiv' => string '' (length=0)
          public 'name' => string 'description' (length=11)
          public 'property' => null
          private '_errors' (yii\base\Model) => null
          private '_validators' (yii\base\Model) => null
          private '_scenario' (yii\base\Model) => string 'default' (length=7)
          private '_events' (yii\base\Component) => 
            array (size=0)
              ...
          private '_eventWildcards' (yii\base\Component) => 
            array (size=0)
              ...
          private '_behaviors' (yii\base\Component) => 
            array (size=0)
              ...
          public 'include' => boolean true
          public 'key' => string 'description' (length=11)
          public 'environment' => null
          public 'dependencies' => null
      'referrer' => 
        object(nystudio107\seomatic\models\metatag\ReferrerTag)[3475]
          public 'charset' => string '' (length=0)
          public 'content' => string 'no-referrer-when-downgrade' (length=26)
          public 'httpEquiv' => string '' (length=0)
          public 'name' => string 'referrer' (length=8)
          public 'property' => null
          private '_errors' (yii\base\Model) => null
          private '_validators' (yii\base\Model) => null
          private '_scenario' (yii\base\Model) => string 'default' (length=7)
          private '_events' (yii\base\Component) => 
            array (size=0)
              ...
          private '_eventWildcards' (yii\base\Component) => 
            array (size=0)
              ...
          private '_behaviors' (yii\base\Component) => 
            array (size=0)
              ...
          public 'include' => boolean true
          public 'key' => string 'referrer' (length=8)
          public 'environment' => null
          public 'dependencies' => null
      'robots' => 
        object(nystudio107\seomatic\models\metatag\RobotsTag)[3474]
          public 'charset' => string '' (length=0)
          public 'content' => string 'none' (length=4)
          public 'httpEquiv' => string '' (length=0)
          public 'name' => string 'robots' (length=6)
          public 'property' => null
          private '_errors' (yii\base\Model) => null
          private '_validators' (yii\base\Model) => null
          private '_scenario' (yii\base\Model) => string 'default' (length=7)
          private '_events' (yii\base\Component) => 
            array (size=0)
              ...
          private '_eventWildcards' (yii\base\Component) => 
            array (size=0)
              ...
          private '_behaviors' (yii\base\Component) => 
            array (size=0)
              ...
          public 'include' => boolean true
          public 'key' => string 'robots' (length=6)
          public 'environment' => 
            array (size=3)
              ...
          public 'dependencies' => null
  private '_errors' (yii\base\Model) => null
  private '_validators' (yii\base\Model) => null
  private '_scenario' (yii\base\Model) => string 'default' (length=7)
  private '_events' (yii\base\Component) => 
    array (size=0)
      empty
  private '_eventWildcards' (yii\base\Component) => 
    array (size=0)
      empty
  private '_behaviors' (yii\base\Component) => 
    array (size=0)
      empty
  public 'name' => string 'General' (length=7)
  public 'description' => string 'General Meta Tags' (length=17)
  public 'class' => string 'nystudio107\seomatic\models\MetaTagContainer' (length=44)
  public 'handle' => string 'general' (length=7)
  public 'include' => boolean true
  public 'dependencies' => 
    array (size=0)
      empty
  public 'clearCache' => boolean false

We can see our con­tain­er, with each <meta> tag rep­re­sent­ed as an object that has prop­er­ties that we can mod­i­fy as we see fit. We can add tags, remove tags… the sky’s the limit!

It works the same way for all of the oth­er con­tain­ers as well.

As men­tioned ear­li­er, all of the 500+ schema​.org types are avail­able as objects in SEO­mat­ic for your struc­tured data pleasure. 

There’s a spe­cial mainEn­ti­ty­Of­Page JSON-LD object that SEO­mat­ic injects on each page that describes the thing the page is about.

{% set mainEntity = seomatic.jsonLd.get('mainEntityOfPage') %}
{{ dump(mainEntity) }}

Let’s have a look at one where it’s set to IndividualProduct:

object(nystudio107\seomatic\models\jsonld\IndividualProduct)[3647]
  public 'serialNumber' => null
  public 'additionalProperty' => null
  public 'aggregateRating' => null
  public 'audience' => null
  public 'award' => null
  public 'brand' => null
  public 'category' => null
  public 'color' => null
  public 'depth' => null
  public 'gtin12' => null
  public 'gtin13' => null
  public 'gtin14' => null
  public 'gtin8' => null
  public 'height' => null
  public 'isAccessoryOrSparePartFor' => null
  public 'isConsumableFor' => null
  public 'isRelatedTo' => null
  public 'isSimilarTo' => null
  public 'itemCondition' => null
  public 'logo' => null
  public 'manufacturer' => null
  public 'material' => null
  public 'model' => null
  public 'mpn' => null
  public 'offers' => null
  public 'productID' => null
  public 'productionDate' => null
  public 'purchaseDate' => null
  public 'releaseDate' => null
  public 'review' => null
  public 'sku' => null
  public 'weight' => null
  public 'width' => null
  public 'additionalType' => null
  public 'alternateName' => null
  public 'description' => string '{seomatic.meta.seoDescription}' (length=30)
  public 'disambiguatingDescription' => null
  public 'identifier' => null
  public 'image' => 
    array (size=2)
      'type' => string 'ImageObject' (length=11)
      'url' => string '{seomatic.meta.seoImage}' (length=24)
  public 'mainEntityOfPage' => string '{seomatic.meta.canonicalUrl}' (length=28)
  public 'name' => string '{seomatic.meta.seoTitle}' (length=24)
  public 'potentialAction' => 
    array (size=3)
      'type' => string 'SearchAction' (length=12)
      'target' => string '{seomatic.site.siteLinksSearchTarget}' (length=37)
      'query-input' => string '{seomatic.helper.siteLinksQueryInput()}' (length=39)
  public 'sameAs' => null
  public 'url' => string '{seomatic.meta.canonicalUrl}' (length=28)
  public 'context' => string 'http://schema.org' (length=17)
  public 'type' => string '{seomatic.meta.mainEntityOfPage}' (length=32)
  public 'id' => null
  private '_errors' (yii\base\Model) => null
  private '_validators' (yii\base\Model) => null
  private '_scenario' (yii\base\Model) => string 'default' (length=7)
  private '_events' (yii\base\Component) => 
    array (size=0)
      empty
  private '_eventWildcards' (yii\base\Component) => 
    array (size=0)
      empty
  private '_behaviors' (yii\base\Component) => 
    array (size=0)
      empty
  public 'include' => boolean true
  public 'key' => string 'mainEntityOfPage' (length=16)
  public 'environment' => null
  public 'dependencies' => null

As you can see, this is a PHP object as well, with all of the prop­er­ties that the Indi­vid­u­al­Prod­uct schema​.org type has! You can manip­u­late these prop­er­ties to your heart’s content.

My rec­om­men­da­tion when it comes to struc­tured data is that you focus on the types that Google con­sumes, as per the Struc­tured Data Search Gallery. Give a lis­ten to the dev​Mode​.fm pod­cast SEO for Web Devel­op­ers: The Silence of the Chick­ens for more on this.

Link Wrapping up

SEO­mat­ic sup­ports a whole host of things we did­n’t cov­er here, such as mul­ti-site & mul­ti-lin­gual sup­port, Google AMP sup­port, fine-grained per­mis­sions for each CP func­tion, head­less Craft” SPA sup­port, pag­i­na­tion and oodles more. Check out the SEO­mat­ic Doc­u­men­ta­tion if you want more on those!

But hope­ful­ly these tips pre­sent­ed here help you under­stand how to use SEO­mat­ic a bit more effectively!