Structured Data for the Semantic Web with JSON-LD

How to add semantic data to your articles with JSON for Linking Data (JSON+LD) and have Google display them in their search gallery.

Mihai Bojin
Geek Culture

--

🔔 This article was originally posted on my site, MihaiBojin.com. 🔔

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

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, 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;

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.

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

--

--

Mihai Bojin
Geek Culture

Software Engineer at heart, Manager by day, Indie Hacker at night. Writing about DevOps, Software engineering, and Cloud computing. Opinions my own.