Adding Schema.org Markup to your SvelteKit Site

Rodney Lab - Aug 6 '21 - - Dev Community

πŸ˜• What use is Schema.org Markup for SEO?

In this post we look at adding Schema.org markup to your SvelteKit site. This is the third post in the series on SEO in SvelteKit. Firstly we had an introduction to SEO and saw how to add Twitter compatible metadata to you Svelte site. Following that we looked at the OpenGraph protocol for SEO metadata developed at Facebook and adopted on many apps for creating lovely share cards for your pages. Be sure to skim through those posts for more detailed explanations of anything which is not immediately clear in this post. Anyway let's get back to Schema.org. As usual before we get going, we'll look at why this topic matters. With that out of the way we see how you can add the SEO markup to your site and then how to check Google is happy with it.

How Schema.org is different to OpenGraph Meta?

The meta we looked at in the previous posts is mostly about telling apps how to present your website when it is shared. That is what image, title and description to use. Although the title and meta description give search engines an idea of what the page is about, it is not easy for search engines to parse a page's content to infer what it is all about in detail. Is the page about a live performance which you can still buy tickets for? Or, for example, is it a step-by-step recipe for baking banana bread? Is there a video on the page? What are the author's Twitter and other social media pages? It is easy to let search engines know all of that information and more using Schema.org. As an example here is how a page with embedded How-To Schema.org metadata is displayed in the Google search results page:

Adding Schema.org Markup to your SvelteKit Site:  Example of a featured snippet from Google search results. Shows a Rodney Lab page in results given prominence over other search results in a featured snippet

The presentation of your page in search results pages will vary depending on the type of Schema.org markup you include. Notice in the example above how the result is displayed in a larger test and with a picture. This makes it stand out from other results, increasing the chances of attracting the user to your site.

In the next section we will look at some Schema.org meta you might want to include in your site. Following that we see how you can go about adding Schema.org markup to your SvelteKit site. Finally we explore a few methods for checking the markup is valid. Are you ready to get going?

πŸ§‘πŸ½β€πŸŽ“ Schema.org Types

There are literally 100s of Schema.org types (currently 792 to be more precise). Below is a list you might find useful for your site. If you are working on a niche or specialist site, it is worth taking ten minutes to browse through additional types on Schema.org which are relevant for your field. Also, for more ideas, be sure to crack open your competitors' sites in Developer Tools to see what Schema.org they include. The first group, bleow, contains items which will all probably be relevevant for the site you are working on. The second contains other types which are still common, but might not be appropriate for your site.

Schema.org Types for Most Sites

We look at code examples below focussing on including these in your site. But first, here are the types:

  • Entity: key information about your organisation. Unlike the other tags, this only needs to be included on a single page. You can then reference the meta from other pages when needed on other pages. We will see how to do this in the code below. Typically you will include this tag on your home page. This works for many sites, where your home page mostly contains information about you and your entity. If you have a lot of other information on your home page (blog posts, contact information, maps, customer testimonials, reviews etc.) Google might have a hard time working out which information is about you and your organisation. In this case place the meta on your about page. Try to keep the about page focussed if you do this.

  • WebSite: key information about your site, included on every page.

  • WebPage: this goes on every page and includes similar meta to what we included in the Twitter and OpenGraph tags.

  • SearchAction: lets search engines know how users can do an internal search on your site. Skip this if you do not have internal search. Also don't forget to adapt the meta to match your site's search parameter format.

More Schema.org Types for Most Sites

  • ImageObject: use this to add your picture or company logo to the markup. It can also be used on images in general on the site (also used within some other types we look at here).

  • BreadcrumbList: structured object letting the search engine know how the current page fits into the site's structure. If you include this, Google adds the breadcrumbs to the search results. It helps bots understand the structure of your site too. Including breadcrumbs on your pages themselves (within the HTML body, not just the meta) also provides internal links, again providing hints to bots on how content is related.

  • Article: metadata on articles, includes the author, post categories and language as well as initial publish and modification dates. You can add comments and likes in here if you want to go to town.

  • Person: Has many uses. Include this on personal sites in the WebSite object to associate the site with the owner. Also include in posts to identify the author. Include links to social media profiles or other websites associated with the person.

  • Organization: information about the organisation which the site represents.

Some More Particular Schema.org Types

For examples of how to implement these, follow the link and scroll to the bottom of the page that opens up. There are typically examples in several languages. Choose the JSON-LD one for an idea of the schema and use an example code below as a template for your SvelteKit implementation.

πŸ“ Some Notes on Adding Schema.org Markup to your SvelteKit Site

Before we look at the code, there are a few points worth mentioning. First of all, Google have some eligibility criteria. In the main these relate to the Schema.org data provided being representative of the page it appears on and not being misleading. Google guidelines detail other eligibility criteria including the content not being obscene, inappropriate or hateful.

Beyond the guidelines, Google might seem fussy about the fields included. You may need a couple of attempts to get a new type right. We will look at tools for testing shortly. These rely on publishing the data on a public site. You might need a little patience to get things right. Fortunately SvelteKit sites build very quickly so debugging isn't too onerous.

πŸ–₯ Adding Schema.org Markup to your SvelteKit Site: Code

There is a little data which feeds into the SchemaOrg component. The mechanism is similar to the one we used for the Twitter and OpenGraph components though. As there is a bit to get through here, we won't go into details on how to plumb the data in. That should stop the post getting too long! You can see the full code on the Rodney Lab GitHub repo which is a complete and tested version. The demo site is up at sveltekit-seo.rodneylab.com/. We will focus on the SchemaOrg component which is in the file src/lib/components/SEO/SchemaOrg.svelte.

SchemaOrg component

Let's start at the end! There are a few data format options for including Schema.org on your site. I would say the easiest is using JSON-LD in a script tag. You have to be a little careful with how you include the tag in your Svelte file firstly, for it to be parsed as intended and secondly so prettier doesn't mangle it! I found this works, strange as it looks:

  const schemaOrgArray = [
    schemaOrgEntity,
    schemaOrgWebsite,
    schemaOrgImageObject,
    schemaOrgWebPage,
    schemaOrgBreadcrumbList,
    schemaOrgPublisher,
  ];
  let jsonLdString = JSON.stringify(schemaOrgObject);
  let jsonLdScript = `
        <script type="application/ld+json">
            ${jsonLdString}
        ${'<'}/script>
    `;

<svelte:head>
  {@html jsonLdScript}
</svelte:head>
Enter fullscreen mode Exit fullscreen mode

We will build up the elements of schemaOrgArray one by one. If you are using this as a guide for work on other frameworks, the most important feature is to include the script tag in each page's HTML head section. That's basically all the code above does:

<script type="application/ld+json">
  `${jsonLdString}`
</script>
Enter fullscreen mode Exit fullscreen mode

Entity

Okay let's look at the schemaOrgEntity first. This is the first element in the array in lines 185–192. Essentially the array combines several Schema.org type objects into a single element which we can include in the script tag just mentioned.

  const schemaOrgEntity =
    entityMeta !== null
      ? {
          '@type': ['Person', 'Organization'],
          '@id': `${siteUrl}/#/schema/person/\${entityHash}`,
          name: author,
          image: {
            '@type': 'ImageObject',
            '@id': `${siteUrl}/#personlogo`,
            inLanguage: siteLanguage,
            url: entityMeta.url,
            width: entityMeta.faviconWidth,
            height: entityMeta.faviconHeight,
            caption: author,
          },
          logo: {
            '@id': `${siteUrl}/#personlogo`,
          },
          sameAs: [
            `https://twitter.com/${twitterUsername}`,
            `https://github.com/${githubPage}`,
            `https://www.tiktok.com/${tiktokUsername}`,
            `https://t.me/${telegramUsername}`,
            `https://uk.linkedin.com/in/${linkedinProfile}`,
            facebookPage,
          ],
        }
      : null;
Enter fullscreen mode Exit fullscreen mode

We saw earlier that we only need to include this element on a single page. We include it on the home page in this example. This is done by adding the entityMeta object in the props passed to the SEO component on the home page. The @id field in line 31 allows us to reference this object in other objects. We will see that field used in other objects. The social media profiles are included so Google can add those profiles to your Knowledge graph in search results. The knowledge graph appears towards the right on the Google desktop search result page. Here is an example:

Adding Schema.org Markup to your SvelteKit Site:  example of a knowledge graph shows GitHub metadata with social media icons towards the bottom

Web Site

Next up is the schemaOrgWebsite object. This includes the SearchAction type.

  const schemaOrgWebsite = {
    '@type': 'WebSite',
    '@id': `${siteUrl}/#website`,
    url: siteUrl,
    name: siteTitle,
    description: siteTitleAlt,
    publisher: {
      '@id': `${siteUrl}/#/schema/person/${entityHash}`,
    },
    potentialAction: [
      {
        '@type': 'SearchAction',
        target: `${siteUrl}/?s={search_term_string}`,
        'query-input': 'required name=search_term_string',
      },
    ],
    inLanguage: siteLanguage,
  };
Enter fullscreen mode Exit fullscreen mode

No need to include this if internal search is not implemented in your site. The search parameter in this code (line 68) works if, to search for β€œcheese” you would enter the url https://example.com/?s=cheese. Tweak as needed for your own use case.

Image Object

Next up we have the ImageObject. This is the featured image for the page we are adding the meta to. The data included is not too different to the data we used for the Twitter and OpenGraph meta:

  const schemaOrgImageObject = {
    '@type': 'ImageObject',
    '@id': `${url}#primaryimage`,
    inLanguage: siteLanguage,
    url: featuredImage.url,
    contentUrl: featuredImage.url,
    width: featuredImage.width,
    height: featuredImage.height,
    caption: featuredImage.caption,
  };
Enter fullscreen mode Exit fullscreen mode

I don't think there is anything that needs clarification here, but let me know if I am wrong.

Breadcrumb List

Moving swiftly on, we have BreadcrumbList up next. Breadcrumbs just provide a hierarchy. The code included within the SchemaOrg component relies on us defining a breadcrumb object for each page or template. Here is an example of the code for defining breadcrumbs on a page, used in the blog post template:

  const breadcrumbs = [
    {
      name: 'Home',
      slug: '',
    },
    {
      name: title,
      slug,
    },
  ];
Enter fullscreen mode Exit fullscreen mode

This works fine for small blog sites, but for larger sites (with many non-blog post pages) it might not scale well. I heard Elder.js has a smart way of handling Breadcrumbs, but I haven't yet had a chance to investigate. Anyway here is the actual code breadcrumb code in the SchemaOrg component which ingests data supplied in the format above:

  const schemaOrgBreadcrumbList = {
    '@type': 'BreadcrumbList',
    '@id': `${url}#breadcrumb`,
    itemListElement: breadcrumbs.map((element, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      item: {
        '@type': 'WebPage',
        '@id': `${siteUrl}/${element.slug}`,
        url: `${siteUrl}/${element.slug}`,
        name: element.name,
      },
    })),
  };
Enter fullscreen mode Exit fullscreen mode

Web Page, Article and Publisher

We just have three more objects to investigate, so let's prepare for the sprint finish! The final three objects are not substantially different to the preceding ones so we will tackle them in a single leap:

 const schemaOrgWebPage = {
    '@type': 'WebPage',
    '@id': `${url}#webpage`,
    url,
    name: title,
    isPartOf: {
      '@id': `${siteUrl}/#website`,
    },
    primaryImageOfPage: {
      '@id': `${url}#primaryimage`,
    },
    datePublished,
    dateModified: lastUpdated,
    author: {
      '@id': `${siteUrl}/#/schema/person/\${entityHash}`,
    },
    description: metadescription,
    breadcrumb: {
      '@id': `${url}#breadcrumb`,
    },
    inLanguage: siteLanguage,
    potentialAction: [
      {
        '@type': 'ReadAction',
        target: [url],
      },
    ],
  };

  let schemaOrgArticle = null;
  if (article) {
    schemaOrgArticle = {
      '@type': 'Article',
      '@id': `${url}#article`,
      isPartOf: {
        '@id': `${url}#webpage`,
      },
      author: {
        '@id': `${siteUrl}/#/schema/person/\${entityHash}`,
      },
      headline: title,
      datePublished,
      dateModified: lastUpdated,
      mainEntityOfPage: {
        '@id': `${url}#webpage`,
      },
      publisher: {
        '@id': `${siteUrl}/#/schema/person/${entityHash}`,
      },
      image: {
        '@id': `${url}#primaryimage`,
      },
      articleSection: ['blog'],
      inLanguage: siteLanguage,
    };
  }

  const schemaOrgPublisher = {
    '@type': ['Person', 'Organization'],
    '@id': `${siteUrl}/#/schema/person/${entityHash}`,
    name: entity,
    image: {
      '@type': 'ImageObject',
      '@id': `${siteUrl}/#personlogo`,
      inLanguage: siteLanguage,
      url: `${siteUrl}/assets/rodneylab-logo.png`,
      contentUrl: `${siteUrl}/assets/rodneylab-logo.png`,
      width: 512,
      height: 512,
      caption: entity,
    },
    logo: {
      '@id': `${siteUrl}/#personlogo`,
    },
    sameAs: [
      `https://twitter.com/${twitterUsername}`,
      `https://github.com/${githubPage}`,
      `https://www.tiktok.com/${tiktokUsername}`,
      `https://t.me/${telegramUsername}`,
      `https://uk.linkedin.com/in/${linkedinProfile}`,
      facebookPage,
    ],
  };
Enter fullscreen mode Exit fullscreen mode

As always if there is anything in here which needs further explanation, don't hesitate to drop a comment below.

For reference here is the complete set of output JSON for a blog post:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": ["Person", "Organization"],
      "@id": "https://www.example.com/#/schema/person/6df93040824c7e06639bca4856a86a16",
      "name": "Rodney Johnson",
      "image": {
        "@type": "ImageObject",
        "@id": "https://www.example.com/#personlogo",
        "inLanguage": "en-GB",
        "url": "https://www.example.com/",
        "width": 512,
        "height": 512,
        "caption": "Rodney Johnson"
      },
      "logo": { "@id": "https://www.example.com/#personlogo" },
      "sameAs": [
        "https://twitter.com/askRodney",
        "https://github.com/rodneylab",
        "https://www.tiktok.com/@askRodney",
        "https://t.me/askRodney",
        "https://uk.linkedin.com/in/ask-rodney",
        "https://www.facebook.com/rodneyLab"
      ]
    },
    {
      "@type": "WebSite",
      "@id": "https://www.example.com/#website",
      "url": "https://www.example.com",
      "name": "SvelteKit SEO Demo Site",
      "description": "SvelteKit SEO",
      "publisher": {
        "@id": "https://www.example.com/#/schema/person/6df93040824c7e06639bca4856a86a16"
      },
      "potentialAction": [
        {
          "@type": "SearchAction",
          "target": "https://www.example.com/?s={query}",
          "query": "required"
        }
      ],
      "inLanguage": "en-GB"
    },
    {
      "@type": "ImageObject",
      "@id": "https://www.example.com/#primaryimage",
      "inLanguage": "en-GB",
      "url": "https://rodneylab-climate-starter.imgix.net/home-open-graph.jpg?ixlib=js-3.2.1&w=1200&h=627&s=81c4407df7d9782806b78d698dbcbc75",
      "contentUrl": "https://rodneylab-climate-starter.imgix.net/home-open-graph.jpg?ixlib=js-3.2.1&w=1200&h=627&s=81c4407df7d9782806b78d698dbcbc75",
      "width": 672,
      "height": 448,
      "caption": "Home page"
    },
    {
      "@type": "WebPage",
      "@id": "https://www.example.com/#webpage",
      "url": "https://www.example.com/",
      "name": "SvelteKit SEO Demo Site | Home",
      "isPartOf": { "@id": "https://www.example.com/#website" },
      "primaryImageOfPage": { "@id": "https://www.example.com/#primaryimage" },
      "datePublished": "2021-07-07T14:19:33.000+0100",
      "dateModified": "2021-07-07T14:19:33.000+0100",
      "author": {
        "@id": "https://www.example.com/#/schema/person/6df93040824c7e06639bca4856a86a16"
      },
      "description": "SvelteKit MDsvex Blog Starter - starter code by Rodney Lab to help you get going on your next blog site",
      "breadcrumb": { "@id": "https://www.example.com/#breadcrumb" },
      "inLanguage": "en-GB",
      "potentialAction": [
        { "@type": "ReadAction", "target": ["https://www.example.com/"] }
      ]
    },
    {
      "@type": "BreadcrumbList",
      "@id": "https://www.example.com/#breadcrumb",
      "itemListElement": [
        {
          "@type": "ListItem",
          "position": 1,
          "item": {
            "@type": "WebPage",
            "@id": "https://www.example.com/",
            "url": "https://www.example.com/",
            "name": "Home"
          }
        }
      ]
    },
    {
      "@type": ["Person", "Organization"],
      "@id": "https://www.example.com/#/schema/person/6df93040824c7e06639bca4856a86a16",
      "name": "Rodney Lab",
      "image": {
        "@type": "ImageObject",
        "@id": "https://www.example.com/#personlogo",
        "inLanguage": "en-GB",
        "url": "https://www.example.com/assets/rodneylab-logo.png",
        "contentUrl": "https://www.example.com/assets/rodneylab-logo.png",
        "width": 512,
        "height": 512,
        "caption": "Rodney Lab"
      },
      "logo": { "@id": "https://www.example.com/#personlogo" },
      "sameAs": [
        "https://twitter.com/askRodney",
        "https://github.com/rodneylab",
        "https://www.tiktok.com/@askRodney",
        "https://t.me/askRodney",
        "https://uk.linkedin.com/in/ask-rodney",
        "https://www.facebook.com/rodneyLab"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

Don't forget to include the new SchemaOrg component in the SEO component (as mentioned earlier we won't go into details on this, but let me know if anything is unclear):

<svelte:head>
  <title>{pageTitle}</title>
  <meta name="description" content={metadescription} />
  <meta
    name="robots"
    content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
  />
  <html lang={siteLanguage} />
</svelte:head>
<Twitter {...twitterProps} />
<OpenGraph {...openGraphProps} />
<SchemaOrg {...schemaOrgProps} />
Enter fullscreen mode Exit fullscreen mode

You can see the full code on the Rodney Lab GitHub repo which is a complete and tested version. If it's OK with you let's move on to testing.

πŸ’― Adding Schema.org Markup to your SvelteKit Site: Testing

As usual I can't go without us first running through how to test our work. There are currently three steps I use. The first is a sanity check using the browser on the dev server. You can check, the markup contains all the expected fields in the browser developer tools using Inspector on Firefox or Elements on Chrome. I will run through the process using Firefox, though it is similar on Chrome. First search for the Schema.org script tag in the Inspector search tool. When you find the element it will be tricky to make out as the code is minified. I like to copy the JSON to Visual Code and use Prettier to format it before taking a look.

Here we are just looking for anything which looks out of place or missing. It makes sense to run this sanity check before pushing the code to our server and building the site.

Google Structured Data Testing Tools

For the next steps we need to run tests using a publicly accessible URL. This means you will need to publish the site to your testing server. There are two Google tools for testing Structured Data. The first is marked for retirement, but it still works and I find it more helpful for debugging. You go to search.google.com/structured-data/testing-tool and paste in you test site's URL. It will give warnings or errors if it is not happy with something. Be sure to fix the errors, using the Schema.org site for help. It is worth repairing warnings where you can to improve your ranking.

Google's replacement tool works in a similar way.

Google Search Console

That's it in terms of testing. However the schema do change from time to time and it is also possible you inadvertently break some functionality without knowing. For that reason regularly check your structured data in the Google Search Console. To do this go to search.google.com/search-console/about and log in. From the left side menu look at each of the items under Enhancements in turn. If Google found errors in the structured data when crawling the site, they will show up here in red. Also, typically Google will send you an email when the bot does encounter an error while crawling.

Adding Schema.org Markup to your SvelteKit Site:  Example of a featured snippet from Google search results. Shows a Rodney Lab page in results given prominence over other search results in a featured snippet

πŸ™ŒπŸ½ Adding Schema.org Markup to your SvelteKit Site: Wrapup

That's it for this post. We have seen:

  • why Schema.org is so useful,
  • what Schema.org types you might want to include on your site,
  • adding Schema.org markup to your SvelteKit site and testing it.

As always suggestions for improvements, together with requests for explanations and feedback are more than welcome. Also let me know what other features you would like implemented on the starter.

πŸ™πŸ½ Adding Schema.org Markup to your SvelteKit Site: Feedback

Have you found the post useful? Would you like to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a couple of dollars, rupees, euros or pounds, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .