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:
- Sign up and add your Astro site URL
- Schema Pilot's AI scans your pages, detects the content type, and generates the correct JSON-LD for each page
- 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 type | Primary schema | Supporting schema |
|---|---|---|
| Blog / content site | Article / BlogPosting | WebSite, Organization, BreadcrumbList |
| Documentation | WebPage, HowTo | WebSite, BreadcrumbList, Organization |
| Marketing / landing | Organization, FAQPage | WebSite, BreadcrumbList |
| E-commerce (Astro + Shopify) | Product with Offer | Organization, BreadcrumbList |
| Portfolio / personal | Person, WebSite | BreadcrumbList |
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.