Back to All Posts

Adding Structured Data in Astro's Starlight Documentation Framework

Astro's Starlight documentation theme is great, but currently lacks support for building structured data (JSON-LD). Fortunately, it's easy enough to roll yourself by overriding a component.

I remember when the Astro team first announced Starlight, their documentation framework. The timing was perfect. I had been meaning to overhaul the docs for JamComments and TypeIt, but didn’t want to do so on the shoddy setups they were using at the time.

Since then, all of my side project documentation is built with Starlight, and I'm not moving away anytime soon. I‘d still call the project relatively new, but they’re iterating quickly, so I'm sure things are only going to get even better as time goes on.

At the time of writing, however, there’s one thing they don’t generate for you out of the box: structured data. But they do allow you to override individual UI components. That makes it relatively easy to roll some JSON-LD yourself.

Overriding the Head Component

The only component we're interested in overriding is <Head>, which very shockingly renders the <head> of your HTML pages. In your Starlight configuration, add a Head property to components and point it to the new component we're about to build:

// astro.config.mjs

import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";

export default defineConfig({
  // ... other configuration stuff.
  integrations: [
    starlight({
      components: {
        Head: "/src/components/docs/Head.astro",
      },
    }),
  ],
});

Next up, let's do some light scaffolding for the our new <Head />:

---
// our custom Head.astro

import Default from "@astrojs/starlight/components/Head.astro";

const { title, description } = Astro.props.entry.data;
---

<Default {...Astro.props} />

If you refresh your documentation locally, you'll notice that nothing's changed. That makes sense. All we're doing is rendering the same Default component it would've rendered anyway.

Quick note: most of the time, you'll want to include a <slot /> as a child of the <Default /> component to ensure any children passed in are rendered in the correct spot:

---
import Default from "@astrojs/starlight/WhateverComponent.astro";
---

<Default>
  <slot/>
</Default>

But the <Head /> component is a little different. Instead of rendering a block of JSX, it's building an array of meta tags that's mapped to JSX. No children involved. So, at the time of writing, we're safe to leave the <slot /> out.

Building Structured Data

Next up, we can build our JSON-LD. We'll use the schema-dts for type safety and easier schema construction. You'll need to choose the schema type most appropriate for your content, but I'll be using TechArticle here, which will be used as a generic for the WithContext type:

---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Head.astro";
import type { TechArticle, WithContext } from "schema-dts";
---

<Default {...Astro.props} />

It's now a matter of building some structured data, using the data provided by Astro.props.entry.data for page-specific information:

---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Head.astro";
import type { TechArticle, WithContext } from "schema-dts";

const { title, description } = Astro.props.entry.data;

const techArticleSchema: WithContext<TechArticle> = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    headline: title,
    description: description,
    url: Astro.url.href,
    author: {
      "@type": "Organization",
      name: "JamComments",
      url: "https://jamcomments.com",
      logo: {
        "@type": "ImageObject",
        url: "https://jamcomments.com/img/open-graph.jpg"
      }
  },
  image: {
    "@type": "ImageObject",
    url: "https://jamcomments.com/img/open-graph.jpg"
  }
};
---

<Default {...Astro.props} />

The last step, of course, is to stringify it into your HTML:

---
  // ...other stuff.
---

<Default {...Astro.props} />

<script 
    type="application/ld+json" 
    set:html={JSON.stringify(techArticleSchema)}>
</script>

If you refresh a documentation page now, you'll see it rendered in your HTML as expected:

JSON-LD structured data rendered into the HTML

Of course, you're free to tweak anything you like, including the position of the markup relative to everything else in the <head />. In the spirit of not holding up anything critical to a user's experience, I prefer to stick it at the end. But do what you want. No one will die either way.

Don't Forget to Validate It

After deploying it, you're not quite done. Verify that you actually just shipped some valid structured data.

There are two go-to resources for this: the schema.org validator and Google's rich results test. Use both. You'd be surprised what one catches when the other says everything's fine.

Check in w/ the Starlight Project

Like I said: the Astro team's been moving pretty quickly on Starlight ever since it's been around. So, it's very possible there will be a dedicated API for doing this sort of thing soon enough. So, keep tabs on their documentation as you're tinkering.


Alex MacArthur is a software engineer working for Dave Ramsey in Nashville-ish, TN.
Soli Deo gloria.

Get irregular emails about new posts or projects.

No spam. Unsubscribe whenever.
Leave a Free Comment

0 comments