Photo: Semantic JSON code displayed on a screen.
Photo by Ferenc Almasi on Unsplash

I spent today learning about JSON for Linking Data and Structured Data for the Semantic Web.

I started from this good intro article and then made my way to Google's Advanced SEO pages.

I decided to implement this functionality for my site's articles, although there are other types I can implement later on.

Creating JSON+LD schema for my site was not as easy as I expected. I couldn't find any GatsbyJS or React plugins that did what I wanted out of the box. The closest one was react-schemaorg which seems to wrap the <script> tag generation - not something I'd use a plugin for.

In the end, I wrote code to generate the JSON+LD schema based on the necessary props for this type.

As I was writing it, I was confused by the examples provided by Google, specifically which @type I should use between Article, NewsArticle, and BlogPosting.

As far as I can tell, there aren't any significant differences from an SEO standpoint; I decided to go with the generic Article type.

I ended up with the following helper code (src/components/article-schema.js):

function ArticleSchema({
  title,
  description,
  date,
  lastUpdated,
  tags,
  image,
  canonicalURL,
}) {
  // load metadata defined in gatsby-config.js
  const { siteMetadata } = useSiteMetadata();
  // load the site's logo as a file/childImageSharp by its relative path
  const { siteLogo } = useSiteLogo();

  const authorProfiles = [
    SITE_URL,
    `https://twitter.com/${siteMetadata.social.Twitter}`,
    `https://linkedin.com/in/${siteMetadata.social.LinkedIn}`
    `https://github.com/${siteMetadata.social.GitHub}`
  ];

  const img = image ? getImage(image.image) : null;
  const imgSrc = image ? getSrc(image.image) : null;

  const jsonData = {
    '@context': `https://schema.org/`,
    '@type': `Article`,
    
    // helper that generates `'@type': 'Person'` schema
    author: AuthorModel({
      name: siteMetadata.author.name,
      sameAs: authorProfiles,
    }),
    url: canonicalURL,
    headline: title,
    description: description,
    keywords: tags.join(','),
    datePublished: date,
    dateModified: lastUpdated || date,
    
    // helper that generates `'@type': 'ImageObject'` schema
    image: ImageModel({
      url: siteMetadata.siteUrl + imgSrc,
      width: img?.width,
      height: img?.height,
      description: image.imageAlt,
    }),

    // helper that generates `'@type': 'Organization'` schema
    publisher: PublisherModel({
      name: siteMetadata.title,

      // helper that generates `'@type': 'ImageObject'` schema
      logo: ImageModel({
        url: siteLogo.src,
        width: siteLogo.image.width,
        height: siteLogo.image.height,
      }),
    }),
    mainEntityOfPage: {
      '@type': `WebPage`,
      '@id': siteMetadata.siteUrl,
    },
  };

  return (
    <Helmet>
      <script type="application/ld+json">
        {JSON.stringify(jsonData, undefined, 4)}
      </script>
    </Helmet>
  );
}
ArticleSchema.defaultProps = {
  tags: [],
};

ArticleSchema.propTypes = {
  title: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired,
  date: PropTypes.string,
  lastUpdated: PropTypes.string,
  tags: PropTypes.arrayOf(PropTypes.string),
  image: PropTypes.object,
  canonicalURL: PropTypes.string,
};

export default ArticleSchema;

Since you can call react-helmet multiple times, I opted to call <ArticleSchema ... /> directly from blog-post.js, the component that renders my blog posts and articles.

Once everything was set up, I tested the results in Google's rich results tester.

Here's the result:

Rich Results Tester

I then realized that including a <article> tag in HTML is also interpreted as Semantic Markup, competing with my JSON+LD definitions, duuuh!

I promptly removed the <article>, <header>, and <section> tags.

Since Google does not define itemProp in its schema, specifying it is superfluous, but for now, I annotated the article's body as:

<div dangerouslySetInnerHTML={{__html: post.html}} itemProp="articleBody" />

I now have all my posts correctly configured to show up in Google's search gallery, which in time will hopefully result in more organic traffic to my articles!

If you enjoyed this article and want to read more like it, please subscribe to my newsletter; I send one out every few weeks!

If you enjoyed this post, please share it with your friends!