Adding structured data to blog posts using Astro


One way to improve SEO for a website is to provide structured data that search engines can use to better understand the contents of a page. Structured data is also what powers rich results like the featured videos, FAQ, recipes etc. that you often see at the top of the search results.

There are multiple formats available to provide structured data. One of them is JSON-LD which we’ll be using this time.

JSON-LD is a Linked Data format written using JSON. In a HTML-document the JSON is placed inside <script type="application/ld+json">-elements (yes, you can have multiple) inside <head> or <body>. The format uses types defined in schemas typically found at https://schema.org to describe different types of objects.

For this demo I read Google’s page about structured data for articles which suggests the BlogPosting-type for blog posts. It supports many properties, but we’ll focus on basic data like headline, description, keywords, dates and author for now.

In our Astro-project, we start by creating a new component that outputs the necessary <script>-element. By default Astro escapes text inside elements, but we can use the set:html directive to bypass this and make sure the element contains our original JSON.

BlogPostingJSONLD.astro
---
import type { CollectionEntry } from 'astro:content';
type Props = CollectionEntry<'blog'>['data'];
const { title, description, publishDate, updateDate, tags } = Astro.props;
const schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"description": description,
"keywords": tags,
"author": {
"@type": "Person",
"name": "Frode Flaten",
"url": "https://frodeflaten.com"
},
/* the schema expects Date or DateTime using ISO 8601 format. For Date that is yyyy-MM-dd */
"datePublished": publishDate.toISOString().substring(0, 10),
/* updateDate is optional frontmatter, so we conditionally add dateModified if it exists */
...(updateDate && { "dateModified": updateDate.toISOString().substring(0, 10) }),
};
---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />

The next step is to add the component in the layout used for blog posts to make it automatic for all posts. The component only require properties we already have in this layout, so we can use the spread-operator to pass all values in Astro.props to the component.

BlogLayout.astro
---
import BaseLayout from "@layouts/BaseLayout.astro";
import type { CollectionEntry } from 'astro:content';
import BlogPostingJSONLD from "@components/blog/BlogPostingJSONLD.astro";
type Props = CollectionEntry<'blog'>['data'];
const { title, description, publishDate, updateDate, draft, heroImage, tags } = Astro.props;
---
<BaseLayout title={title} description={description}>
<BlogPostingJSONLD slot="head" {...Astro.props} />
<article>
...
</article>
</BaseLayout>

Did you notice the use of slot="head" above? That’s a special attribute that controls the placement of an element inside a layout. It allows us to place this component inside <head> while the rest goes inside <body> like we usually want. For this to work our layout has to define a slot with a matching name-attribute.

BaseLayout.astro
---
import BaseHead from "@components/BaseHead.astro";
const { title, description } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<!-- Elements with slot="head" attribute will be placed here -->
<slot name="head" />
</head>
<body>
<!-- Any elements without a slot-attribute or slot="default" will be placed here -->
<slot />
</body>
</html>

To make sure everything works we can build our site and inspect the the generated HTML-file for any post using this layout. You should see something like this:

<head>
...
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"Rockets, blogs and PowerShell","description":"What to expect from this unexpected blog.","keywords":["Astro","Learning","PowerShell"],"author":{"@type":"Person","name":"Frode Flaten","url":"https://frodeflaten.com"},"datePublished":"2023-01-01"}</script>
...
</head>

To take it one step further, Google offers a Rich Results Test tool that can scan a URL or provided HTML-code to verify that everything works as expected.

With a few simple steps we were able to add structured data to the blog posts. Hopefully this will improve search results and maybe one day feature one of these articles in the rich results.