JSON-LD schema with GatsbyJS for rich snippets
Asked Answered
D

2

6

I have a basic blog setup with Gatsby and at the time of posting this question there lacks good documentation for SEO components. There are examples of basic SEO components but what I am wanting is a little more in-depth. Maybe, if a solution is reached here it could be contributed to the Gatsby docs for others to benefit.

On top of the usual title and description meta tags and the facebook/twitter open graph meta (which I have done already), I want to add structured data for rich snippets which will vary depending on what the blog post type is. For example, I might have a regular post which would print Article schema, some posts might be How-to, in which case I'd like to print HowTo schema instead of Article. At some point I might write a post with would suit FAQ schema.

I don't know if this is the best approach but here's what I'm thinking:

1. In frontmatter set the schema type I want to true, leave the rest false.

I am also thinking of storing the schema data in the frontmatter but as this data is quite complex and will vary from post type to post type (Article, HowTo etc.) I'm not sure if this is yet a good idea?

---
title: Hello World
description: How to say hello
article: false
how-to: true
faq: false
---

2. Test for true/false in the SEO component and print the correct schema.

Below is my entire SEO component, which obviously doesn't work but you can hopefully see where my thinking is headed. I have dissected and borrowed from the gatsby advanced starter component and the gatsby starter prismic component but neither do quite what I need. Here's mine:

import React from "react"
import Helmet from "react-helmet"
import PropTypes from "prop-types"
import { useStaticQuery, graphql } from "gatsby"
import Facebook from "./Facebook"
import Twitter from "./Twitter"

const SEO = ({
  title,
  desc,
  banner,
  pathname,
  published,
  modified,
  article,
  webpage,
  node,
}) => {
  const { site } = useStaticQuery(query)

  const {
    buildTime,
    siteMetadata: {
      siteUrl,
      defaultTitle,
      defaultDescription,
      defaultBanner,
      headline,
      siteLanguage,
      ogLanguage,
      author,
      twitter,
      facebook,
    },
  } = site

  const seo = {
    title: title || defaultTitle,
    description: desc || defaultDescription,
    image: `${siteUrl}${banner || defaultBanner}`,
    url: `${siteUrl}${pathname || "/"}`,
    date_published: published,
    date_modified: modified,
  }

  // Default Website Schema
  const schemaOrgJSONLD = [
    {
      "@context": "http://schema.org",
      "@type": "WebSite",
      url: siteUrl,
      name: defaultTitle,
      alternateName: headline ? headline : "",
    },
  ]

  if (howto) {
    schemaOrgJSONLD.push({
      /* HowTo Schema here */
    })
  }
  
  if (faq) {
    schemaOrgJSONLD.push({
      /* FAQ Schema here */
    })
  }

  if (article) {
    schemaOrgJSONLD.push({
      /* Regular Article Schema */
      "@context": "http://schema.org",
      "@type": "Article",
      author: {
        "@type": "Person",
        name: author,
      },
      copyrightHolder: {
        "@type": "Person",
        name: author,
      },
      copyrightYear: "2019",
      creator: {
        "@type": "Person",
        name: author,
      },
      publisher: {
        "@type": "Organization",
        name: author,
        logo: {
          "@type": "ImageObject",
          url: `${siteUrl}${defaultBanner}`,
        },
      },
      datePublished: seo.date_published,
      dateModified: seo.date_modified,
      description: seo.description,
      headline: seo.title,
      inLanguage: siteLanguage,
      url: seo.url,
      name: seo.title,
      image: {
        "@type": "ImageObject",
        url: seo.image,
      },
      mainEntityOfPage: seo.url,
    })
  }

  return (
    <>
      <Helmet title={seo.title}>
        <html lang={siteLanguage} />
        <meta name="description" content={seo.description} />
        <meta name="image" content={seo.image} />
        {/* Schema.org tags */}
        <script type="application/ld+json">
          {JSON.stringify(schemaOrgJSONLD)}
        </script>
      </Helmet>
      <Facebook
        desc={seo.description}
        image={seo.image}
        title={seo.title}
        type={article ? "article" : "website"}
        url={seo.url}
        locale={ogLanguage}
        name={facebook}
      />
      <Twitter
        title={seo.title}
        image={seo.image}
        desc={seo.description}
        username={twitter}
      />
    </>
  )
}

export default SEO

SEO.propTypes = {
  title: PropTypes.string,
  desc: PropTypes.string,
  banner: PropTypes.string,
  pathname: PropTypes.string,
  published: PropTypes.string,
  modified: PropTypes.string,
  article: PropTypes.bool,
  webpage: PropTypes.bool,
  node: PropTypes.object,
}

SEO.defaultProps = {
  title: null,
  desc: null,
  banner: null,
  pathname: null,
  published: null,
  modified: null,
  article: false,
  webpage: false,
  node: null,
}

const query = graphql`
  query SEO {
    site {
      buildTime(formatString: "YYYY-MM-DD")
      siteMetadata {
        siteUrl
        defaultTitle: title
        defaultDescription: description
        defaultBanner: logo
        headline
        siteLanguage
        ogLanguage
        author
        logo
        twitter
        facebook
      }
    }
  }
`

The problems I can see are:

  1. How to test for what schema type to use and print it
  2. Include breadcrumbs schema for all types
  3. Print only a single schema JSON-LD script tag, avoiding any duplicate schema
  4. Is using frontmatter in markdown files suitable to store complex schema data
  5. Retrieving frontmatter data for schema
Dart answered 11/11, 2019 at 21:27 Comment(0)
D
8

I settled on this solution.

In frontmatter:

---
type: howto // Use either 'article' or 'howto'
---

Query for it with GraphQL like you would for your other data:

frontmatter {
 title
 published(formatString: "MMMM DD, YYYY")
 modified(formatString: "MMMM DD, YYYY")
 description
 type
}

Pass it to your SEO component:

<SEO
 title={post.frontmatter.title}
 desc={post.frontmatter.description}
 published={post.frontmatter.published}
 modified={post.frontmatter.modified}
 type={post.frontmatter.type}
/>

In your SEO component, you can use it like this (do the same for all your types). You can setup your Posts and SEO component for as my types as you need, FAQ, Course etc:

const schemaType = type

if (schemaType === "howto") {
 schemaHowTo = {
  // Your howto schema here
 }
}

if (schemaType === "article") {
 schemaArticle = {
  // Your article schema here
 }
}

Finally, in React Helmet we have:

<Helmet>
 {schemaType === "howto" && (
  <script type="application/ld+json">
   {JSON.stringify(schemaHowTo)}
  </script>
 )}
 {schemaType === "article" && (
  <script type="application/ld+json">
   {JSON.stringify(schemaArticle)}
  </script>
 )}
...
<Helmet>
Dart answered 22/11, 2019 at 5:53 Comment(1)
You could make your schemaTypes as properties of a schema object and then reference them dynamically based on the schemaType. IE JSON.stringify(schemas[schemaType]), or if you wanted to keep as is, you could change to JSON.stringify(eval('schema'+schemaType)), but be careful of eval if schemaType can be manipulated by a visitor.Eskill
G
2

Just found great article on the topic: https://www.iamtimsmith.com/blog/creating-a-better-seo-component-for-gatsby/ Helped me to create rich snippets dynamically for all pages in my app. Main idea: pass children in you seo.js:

return (
    <Helmet
      htmlAttributes={{lang: `en`}}
      titleTemplate={`%s | ${data.site.siteMetadata.title}`}
    >
      <title>{title}</title>
      
      {children}
    </Helmet>
  );

and then on any page/component:

return (
      <SEO title={title} description={description} image={image} slug={slug}>
        <script type='application/ld+json'>
          {`{
                        '@context': 'https://schema.org',
                        '@type': 'LiveBlogPosting',
                        '@id': 'https://example.com',
                        'headline': ${title},
                        'description': ${description}
                    }`}
        </script>
      </SEO>
  );
};
Gastrotomy answered 6/9, 2021 at 15:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.