schemapilot.
Blog
Implementation

Structured Data for Astro: How to Add JSON-LD

·7 min read

Astro is built for speed. Static-first output, zero JavaScript by default, and content collections that turn Markdown into fast, SEO-friendly pages. But Astro does not include structured data out of the box. If you want rich results in Google -- FAQ dropdowns, article metadata, breadcrumb trails, star ratings -- you need to add JSON-LD schema markup to your Astro site yourself.

This guide covers three approaches to adding Astro structured data: automated generation with Schema Pilot, native implementation using Astro components, and community packages for type safety. Pick the approach that fits your project.

Option 1: Automated schema markup with Schema Pilot

If you do not want to write or maintain JSON-LD code, Schema Pilot handles Astro structured data automatically. It works with both static (SSG) and server-rendered (SSR) Astro sites without any npm packages or per-page configuration.

Here is how it works:

  1. Sign up and add your Astro site URL
  2. Schema Pilot's AI scans your pages, detects the content type, and generates the correct JSON-LD for each page
  3. Add a single embed script to your base layout:
---
// src/layouts/BaseLayout.astro
const { title, description } = Astro.props;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <script src="https://www.schemapilot.app/embed/YOUR_SITE_ID.js" defer></script>
  </head>
  <body>
    <slot />
  </body>
</html>

That is it. No schema code to write, no JSON-LD objects to maintain across your content collection pages. When your content changes, Schema Pilot re-scans and updates the Astro structured data automatically.

The free plan covers 1 site and 30 page scans. For larger Astro sites, paid plans add unlimited scans and weekly auto-rescanning.

Stop writing schema markup by hand

Schema Pilot scans your pages, generates valid JSON-LD, and serves it automatically. No code changes required.

Option 2: Native JSON-LD in Astro components

Astro's component model makes it straightforward to build reusable schema markup. Since Astro renders everything on the server by default, your JSON-LD is always present in the initial HTML -- exactly what search engines want.

Creating a SchemaMarkup component

Start with a generic component that accepts any schema object and renders it as a JSON-LD script tag:

---
// src/components/SchemaMarkup.astro
interface Props {
  schema: Record<string, unknown> | Record<string, unknown>[];
}

const { schema } = Astro.props;
---

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

Astro's set:html directive injects raw HTML content into an element. This is the Astro equivalent of React's dangerouslySetInnerHTML, but with a cleaner syntax.

Adding schema to layout components

Site-wide schema types like Organization and WebSite belong in your base layout so they appear on every page:

---
// src/layouts/BaseLayout.astro
import SchemaMarkup from "../components/SchemaMarkup.astro";

const { title, description } = Astro.props;

const orgSchema = {
  "@context": "https://schema.org",
  "@type": "Organization",
  name: "Your Company",
  url: "https://www.yoursite.com",
  logo: "https://www.yoursite.com/logo.png",
  sameAs: [
    "https://twitter.com/yourcompany",
    "https://github.com/yourcompany",
  ],
};

const websiteSchema = {
  "@context": "https://schema.org",
  "@type": "WebSite",
  name: "Your Company",
  url: "https://www.yoursite.com",
};
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <SchemaMarkup schema={orgSchema} />
    <SchemaMarkup schema={websiteSchema} />
  </head>
  <body>
    <slot />
  </body>
</html>

Every page using this layout now has Organization and WebSite structured data baked into the static HTML.

Free Organization Schema Generator

Schools, NGOs, corporations, and similar entities. Generate valid JSON-LD in seconds.

Generating schema from content collection frontmatter

This is where Astro's content collections and structured data work together particularly well. Define your blog collection schema in src/content.config.ts, then use the frontmatter fields to build Article JSON-LD automatically.

First, define a content collection with the fields you need for Astro structured data:

// src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    author: z.string().default("Your Name"),
    image: z.string().optional(),
  }),
});

export const collections = { blog };

Then in your blog post layout, pull these values into an Article schema:

---
// src/layouts/BlogPostLayout.astro
import SchemaMarkup from "../components/SchemaMarkup.astro";
import BaseLayout from "./BaseLayout.astro";

interface Props {
  title: string;
  description: string;
  date: Date;
  updatedDate?: Date;
  author: string;
  image?: string;
  slug: string;
}

const { title, description, date, updatedDate, author, image, slug } = Astro.props;
const siteUrl = "https://www.yoursite.com";

const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: title,
  description: description,
  datePublished: date.toISOString(),
  ...(updatedDate && { dateModified: updatedDate.toISOString() }),
  author: {
    "@type": "Person",
    name: author,
  },
  ...(image && { image: `${siteUrl}${image}` }),
  url: `${siteUrl}/blog/${slug}`,
  publisher: {
    "@type": "Organization",
    name: "Your Company",
    logo: {
      "@type": "ImageObject",
      url: `${siteUrl}/logo.png`,
    },
  },
};
---

<BaseLayout title={title} description={description}>
  <SchemaMarkup schema={articleSchema} />
  <article>
    <h1>{title}</h1>
    <time datetime={date.toISOString()}>{date.toLocaleDateString()}</time>
    <slot />
  </article>
</BaseLayout>

Now wire it up in your blog post page. Every post in the collection gets Article structured data generated from its frontmatter, with no manual JSON-LD per post:

---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
import BlogPostLayout from "../../layouts/BlogPostLayout.astro";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<BlogPostLayout
  title={post.data.title}
  description={post.data.description}
  date={post.data.date}
  updatedDate={post.data.updatedDate}
  author={post.data.author}
  image={post.data.image}
  slug={post.id}
>
  <Content />
</BlogPostLayout>

Astro content collections validate your frontmatter at build time with Zod. If a required field like title is missing, the build fails. This means your Astro structured data inputs are guaranteed to be valid before the schema markup is ever generated.

Option 3: astro-seo and community packages

astro-seo

The astro-seo package handles meta tags and Open Graph data but does not generate JSON-LD directly. It is useful alongside your schema markup, not as a replacement for it:

npm install astro-seo
---
import { SEO } from "astro-seo";
import SchemaMarkup from "../components/SchemaMarkup.astro";

const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: "Your Article Title",
  datePublished: "2026-03-21",
};
---

<head>
  <SEO
    title="Your Article Title"
    description="Article description for meta tags"
    openGraph={{
      basic: {
        title: "Your Article Title",
        type: "article",
        image: "/og-image.png",
      },
    }}
  />
  <SchemaMarkup schema={articleSchema} />
</head>

astro-seo handles meta tags and Open Graph. Your SchemaMarkup component handles the Astro structured data. They complement each other.

schema-dts for type safety

For TypeScript validation of your schema objects, install schema-dts:

npm install schema-dts

Then type your schema objects to catch errors at build time:

---
// src/components/SchemaMarkup.astro
import type { Thing, WithContext } from "schema-dts";

interface Props {
  schema: WithContext<Thing>;
}

const { schema } = Astro.props;
---

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

Now TypeScript flags invalid properties before your Astro site even builds:

import type { Article, WithContext } from "schema-dts";

// TypeScript catches the typo in datePublished
const schema: WithContext<Article> = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: "My Post",
  datePublished: "2026-03-21",
  // @ts-expect-error - 'writer' is not valid, use 'author'
  writer: "John Doe",
};

For any Astro structured data implementation beyond a handful of pages, schema-dts is worth adding.

Free Article Schema Generator

News articles and blog content. Generate valid JSON-LD in seconds.

Dynamic schema from content collections

The content collection approach shown earlier scales well. Here is a pattern for generating BreadcrumbList schema dynamically based on the page path, which works across your entire Astro site:

---
// src/components/BreadcrumbSchema.astro
interface Props {
  path: string;
  labels?: Record<string, string>;
}

const { path, labels = {} } = Astro.props;
const siteUrl = "https://www.yoursite.com";

const segments = path.split("/").filter(Boolean);
const items = [
  { "@type": "ListItem", position: 1, name: "Home", item: siteUrl },
  ...segments.map((segment, i) => ({
    "@type": "ListItem",
    position: i + 2,
    name: labels[segment] || segment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
    item: `${siteUrl}/${segments.slice(0, i + 1).join("/")}`,
  })),
];

const breadcrumbSchema = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: items,
};
---

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

Use it in any layout or page:

<BreadcrumbSchema path={Astro.url.pathname} labels={{ blog: "Blog", guides: "Guides" }} />

This generates valid BreadcrumbList Astro structured data for every page without maintaining breadcrumb markup manually.

Content collections are not limited to blog posts. You can define collections for documentation pages, product listings, team member profiles, or any content type -- and generate the matching schema for each. Define the Zod schema once, and structured data flows automatically to every page in the collection.

Essential schema types for Astro sites

The right schema types depend on what your Astro site does:

Site typePrimary schemaSupporting schema
Blog / content siteArticle / BlogPostingWebSite, Organization, BreadcrumbList
DocumentationWebPage, HowToWebSite, BreadcrumbList, Organization
Marketing / landingOrganization, FAQPageWebSite, BreadcrumbList
E-commerce (Astro + Shopify)Product with OfferOrganization, BreadcrumbList
Portfolio / personalPerson, WebSiteBreadcrumbList

Every Astro site should have Organization and WebSite schema in the base layout. Add page-specific types from the table above based on your content.

BreadcrumbList is worth adding to nearly every Astro site. It replaces the raw URL in search results with a clean navigation path like "Home > Blog > Schema Markup for Astro," and as shown above, it is straightforward to generate from the URL path.

Testing and validation

Before deploying your Astro structured data, validate it:

Google Rich Results Test (search.google.com/test/rich-results) -- paste a URL or code snippet. This tells you which rich result types your structured data qualifies for and highlights any errors.

Schema Markup Validator (validator.schema.org) -- validates against the full Schema.org spec. Catches structural issues that Google's tool might not flag.

Build output inspection -- since Astro generates static HTML by default, you can check the output directly:

npm run build
grep -l "application/ld+json" dist/**/*.html

This confirms which pages in your build output contain structured data. For Astro sites, this is the most reliable way to verify your schema markup is present, since what lands in the dist/ folder is exactly what gets deployed.

Keep your Astro structured data in sync with visible page content. If your page shows one price but your JSON-LD contains a different price, Google may issue a manual action. Always derive schema values from the same data source that renders the page -- content collection frontmatter is ideal for this because it is the single source of truth.

Conclusion

Adding JSON-LD structured data to Astro is not complicated, but it does require a deliberate approach. For teams that want zero maintenance, Schema Pilot automates the entire process with a single embed script in your base layout. For developers who prefer manual control, Astro's component model and content collections make it straightforward to build typed, reusable schema markup that scales with your site.

Astro already gives you fast, static pages that search engines love to crawl. Structured data is the piece that tells those search engines exactly what your content means.

Stop writing schema markup by hand

Schema Pilot scans your pages, generates valid JSON-LD, and serves it automatically. No code changes required.

Related posts

Stop leaving rich results on the table

Every page without schema markup is a missed opportunity for clicks. Schema Pilot handles the entire process — scanning, generating, validating, and serving structured data — so you don't have to.