DEV Community

Mihai Bojin
Mihai Bojin

Posted on • Originally published at mihaibojin.com on

Structured Data for the Semantic Web with JSON-LD

๐Ÿ”” This article was originally posted on my site, MihaiBojin.com. ๐Ÿ””


I spent today learning about JSON for Linking Data and Strucยญtured 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 major differences from a 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;
Enter fullscreen mode Exit fullscreen mode

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 end 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" />
Enter fullscreen mode Exit fullscreen mode

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 liked this article and want to read more like it, please subscribe to my newsletter; I send one out every few weeks!

Top comments (0)