Astro integration for automatic structured data (JSON-LD) generation. Supports Articles, FAQs, Products, Recipes, Videos, Breadcrumbs, Local Businesses, Events, Organizations, and more — with TypeScript-typed components and matching Zod schemas for validating your content.
Landing Page | GitHub Repository | npm Package
npm install @casoon/astro-structured-dataAdd the integration to your astro.config.mjs:
import { defineConfig } from 'astro/config';
import structuredData from '@casoon/astro-structured-data';
export default defineConfig({
site: 'https://example.com', // used automatically by the integration
integrations: [
structuredData({
generateMeta: true,
siteName: 'My Awesome Website',
locale: 'de_DE',
twitterSite: '@mywebsite',
}),
],
});If you don't set site in your Astro config, pass siteUrl explicitly:
structuredData({
siteUrl: 'https://example.com',
generateMeta: true,
})The integration registers a middleware that completes every HTML page once it has fully rendered: it inserts the @graph block (graph mode) and the meta tags (generateMeta) and runs the page-level checks. Because it needs the complete page, HTML responses are buffered instead of streamed. Nothing needs to be set up for this.
| Option | Type | Default | Description |
|---|---|---|---|
siteUrl |
string |
— | Absolute http(s) base URL — falls back to Astro's site config; one of the two is required |
useGraph |
boolean |
false |
Combine all schemas of a page into one @graph block (requires <SchemaGraph /> in the layout) |
generateMeta |
boolean |
false |
Generate standard HTML head meta tags (og:, twitter:, canonical, etc.) from schemas |
siteName |
string |
— | Global site name used for og:site_name |
locale |
string |
— | Global locale used for og:locale (e.g. de_DE) |
twitterSite |
string |
— | Twitter site handle used for twitter:site (e.g. @my_site) |
twitterCreator |
string |
— | Fallback Twitter creator handle used for twitter:creator (e.g. @author) |
warnOnMissingRecommended |
boolean |
true |
Log warnings during build when recommended schema.org fields are absent |
defaultLocalBusiness |
LocalBusinessSchema props |
— | Site-wide local business defaults merged into LocalBusinessSchema |
defaultArticlePublisher |
{ name: string; logo?: { url: string } } |
— | Default publisher for ArticleSchema, WebPageSchema, OrganizationSchema and the hiring organization of JobPostingSchema |
defaultBrand |
Brand | string |
— | Default brand for ProductSchema |
defaultShippingDetails |
OfferShippingDetails |
— | Default shipping details for ProductSchema |
defaultReturnPolicy |
MerchantReturnPolicy |
— | Default return policy for ProductSchema |
Options are validated when the integration is created: unknown keys (typos) and invalid values fail the build.
Invalid structured data is never rendered. Every component validates its props against its Zod schema at render time; any violation throws and fails the build (or the request, in SSR) with a message naming the component, the page and the offending fields:
[astro-structured-data] <EventSchema> on /events/meetup/ received invalid props:
✖ Must be an ISO 4217 currency code like "EUR"
→ at priceCurrency
- Unknown props are rejected, so typos cannot silently drop data. Attributes that belong to Astro itself —
slot(e.g.<ArticleSchema slot="head" />) anddata-astro-*— are not props of the component and are ignored. - Formats: dates are ISO 8601 (
2026-06-01,2026-06-01T18:00:00+02:00) orDateobjects; durations are ISO 8601 (PT1H30M); currencies are ISO 4217 (EUR); URLs are absolutehttp(s)URLs or root-relative paths (/img.jpg); text fields must not be empty. - Cross-field rules, e.g.
priceandpriceCurrencyonly together,ratingValueandreviewCountonly together,imageWidth/imageHeight/… only with an image, an end date not before its start date, at most one ofoffers/priceRange/price. - No invented values: components never fill in placeholder data. Where schema.org needs a value (e.g. an organization name), it comes from a prop or a configured default — otherwise the build fails.
- Page-level checks: all schemas on a page must agree on their sitemap hints, and in graph mode a page with schemas must render
<SchemaGraph />exactly once.
The exported Zod schemas ignore unknown top-level keys, so they can be used directly in Content Collections whose frontmatter has additional fields; the components themselves are strict. The generic <Schema> component is the exception: it only requires an @type and outputs your object as given.
| Package version | Astro | Node.js |
|---|---|---|
| 2.0.x | 5.x · 6.x · 7.x | ≥ 18 |
| 1.5.x | 5.x · 6.x · 7.x | ≥ 18 |
| 1.4.x | 5.x · 6.x | ≥ 18 |
Astro 7 introduces a Rust-based compiler and upgrades to Vite 8. Both changes are purely additive — no integration API was altered — so this package is fully compatible without any changes on your end.
2.0 validates all data strictly and fails the build instead of rendering invalid structured data. Most upgrades come down to fixing the data the build reports:
- Run a build. Every invalid prop is reported with component, page and field (see Validation). Unknown props and unknown integration options are errors now, too.
- Provide values that used to be invented:
LocalBusinessSchemaandOrganizationSchemaneed aname,JobPostingSchemaahiringOrganizationName(each can also come from the configured defaults).baseSalaryneeds aunit.employmentType,operatingSystemandapplicationCategoryare no longer defaulted — set them explicitly where you relied on'FULL_TIME','Web'or'DeveloperApplication'. - Graph mode: keep exactly one
<SchemaGraph />in the base layout. Its position no longer affects which schemas are included. - Online events: use
attendanceMode="Online"withonlineUrlinstead of a placeholder address. - Options:
defaultArticlePublisheris{ name, logo?: { url } }anddefaultLocalBusinesstakes theLocalBusinessSchemaprops. - Meta tags now come from the middleware and are inserted into
<head>; tags your layout already defines are kept. If you worked around meta tags ending up in<body>, you can drop that workaround.
The integration registers an Astro Dev Toolbar panel (visible only in astro dev) that shows every <script type="application/ld+json"> block found on the current page.
For each schema it shows:
- Rich Result Preview — a Google-style mockup rendered directly in the toolbar:
- Article / BlogPosting / NewsArticle — thumbnail, author, date snippet
- FAQPage — interactive accordion (click to expand answers)
- Product — star rating, price, in-stock badge
- BreadcrumbList — breadcrumb trail in Google style
- Event — calendar date box, location, time
- JobPosting — job card with location, employment type, salary badges
- LocalBusiness — phone, address, opening hours
- SoftwareApplication — star rating, OS, category
- Validation warnings — required and recommended field checks per type
- 📋 Copy JSON-LD — copies the full JSON-LD to the clipboard
- 🔍 Test on Schema.org — opens
validator.schema.orgin a new tab - Show JSON-LD Raw — toggle the raw JSON for inspection
When generateMeta: true is enabled, the integration derives <meta> and <link> elements from the page's primary schema and inserts them into <head>. This runs as middleware after the page has fully rendered, so it sees every schema on the page regardless of where the components are placed (layout head, page body, …) — including components that await data before rendering their schema.
- Primary schema: the most page-specific schema wins — Article/BlogPosting/NewsArticle, Product, Recipe, VideoObject, Event, JobPosting, SoftwareApplication, ProfilePage, FAQPage, WebPage types — before site-wide LocalBusiness and WebSite schemas.
- Your tags win: tags the page already defines (same
name/property, canonical, orhreflang) are kept and not duplicated. - Canonical /
og:url: built fromsiteUrland the page path, or taken from aWebPageSchemaurl/ an article'smainEntityOfPage. Theurlof an event, business or product is never used as the page's canonical. - A page with a primary schema but no
</head>fails the build.
Generated tags:
- Canonical:
<link rel="canonical" href="..."> - Description:
<meta name="description" content="..."> - Robots:
<meta name="robots" content="...">(derived fromitem.robotsoritem.noindex/item.nofollow) - Author:
<meta name="author" content="...">(derived from schemaitem.author) - Reading Time:
<meta name="reading-time" content="...">(non-standard; derived fromitem.readingTimeor parsed from ISO durationitem.timeRequired) - Alternates (hreflang):
<link rel="alternate" hreflang="..." href="...">(extracted fromitem.alternatesor schema translations) - OpenGraph:
og:title,og:description,og:image,og:image:width/height/type/alt,og:url,og:type,og:site_name,og:locale,article:published_time,article:modified_time,article:author,article:section,article:tag(relative images are automatically resolved to absolute URLs using your config'ssiteUrl) - Twitter Cards:
twitter:card,twitter:title,twitter:description,twitter:image,twitter:image:alt,twitter:site,twitter:creator
Every schema component (and the generic <Schema>) accepts sitemap crawl properties (e.g. <ArticleSchema ... changefreq="weekly" priority={0.8} />).
These properties are encoded as data-attributes on the JSON-LD <script> tag (the @graph script in graph mode) and are not part of the JSON-LD itself. A page has one sitemap entry, so all schemas on a page must agree on these values — conflicting or invalid values (changefreq outside the sitemap protocol values, priority outside 0–1) fail the build. Post-build sitemap generators like @casoon/astro-site-files can read these tags directly from the HTML to dynamically build/patch the sitemap entries, meaning you don't need to duplicate sitemap logic in your configs. This feature is completely decoupled and will fall back gracefully to the sitemap defaults if @casoon/astro-site-files is not installed or configured.
Import components from @casoon/astro-structured-data/components:
---
import { ArticleSchema, FAQSchema } from '@casoon/astro-structured-data/components';
---Docs: schema.org/Article · Google: Article
<ArticleSchema
title="My Article"
description="Article description"
datePublished="2024-01-01"
authorName="Jane Doe"
imageUrl="https://example.com/image.jpg"
/>| Prop | Type | Required | Description |
|---|---|---|---|
title |
string |
Yes | Article headline |
description |
string |
Yes | Article description |
datePublished |
string | Date |
Yes | Publication date |
dateModified |
string | Date |
No | Last modified date |
authorName |
string | string[] |
Yes | Author name(s) |
authorType |
'Person' | 'Organization' |
No | Default: 'Person' |
authorUrl |
string |
No | Author profile URL (single author only) |
authorId |
string |
No | Author @id for linked data (single author only) |
imageUrl |
string |
No | Article image URL |
imageWidth |
number |
No | Image width in pixels |
imageHeight |
number |
No | Image height in pixels |
imageFormat |
string |
No | Image MIME type, e.g. 'image/jpeg' |
imageCaption |
string |
No | Image caption |
publisherName |
string |
No | Publisher name (falls back to defaultArticlePublisher) |
publisherLogo |
string |
No | Publisher logo URL (falls back to defaultArticlePublisher) |
schemaType |
'Article' | 'BlogPosting' | 'NewsArticle' |
No | Default: 'BlogPosting' |
inLanguage |
string |
No | Content language, e.g. 'de' |
articleSection |
string |
No | Section or category name |
keywords |
string | string[] |
No | Keywords |
wordCount |
number |
No | Word count |
readingTimeMinutes |
number |
No | Reading time in minutes (encoded as timeRequired) |
isAccessibleForFree |
boolean |
No | Default: true |
isPartOfHeadline |
string |
No | Parent series headline (for isPartOf) |
isPartOfUrl |
string |
No | Parent series URL |
seriesPosition |
number |
No | Position within the series |
hasPart |
{ headline: string; url: string; position?: number }[] |
No | Child articles in a series |
Docs: schema.org/FAQPage · Google: FAQ
<FAQSchema
questions={[
{ question: 'What is this?', answer: 'An Astro integration.' },
{ question: 'How does it work?', answer: 'It injects JSON-LD.' },
]}
/>| Prop | Type | Required | Description |
|---|---|---|---|
questions |
{ question: string; answer: string }[] |
Yes | List of Q&A pairs |
Docs: schema.org/Product · Google: Product
<ProductSchema
name="Super Gadget"
description="The best gadget ever"
imageUrl="https://example.com/gadget.jpg"
price={29.99}
priceCurrency="EUR"
availability="InStock"
sku="SG-001"
ratingValue={4.5}
reviewCount={128}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Product name |
description |
string |
Yes | Product description |
imageUrl |
string | string[] |
Yes | Product image URL(s) |
price |
string | number |
No | Price (simple offer) |
priceCurrency |
string |
No | ISO 4217 currency code, e.g. 'EUR' |
availability |
'InStock' | 'OutOfStock' | 'PreOrder' | 'OnlineOnly' |
No | Offer availability (requires price) |
offers |
Offer | Offer[] |
No | Full offer object(s) for advanced use cases |
priceRange |
PriceRange |
No | Price range for variable pricing |
brand |
string | Brand |
No | Brand name or object (falls back to defaultBrand) |
sku |
string |
No | Stock keeping unit |
gtin |
string |
No | GTIN barcode |
ratingValue |
number |
No | Aggregate rating (0–5), only together with reviewCount |
reviewCount |
number |
No | Number of reviews, only together with ratingValue |
reviews |
ReviewItem[] |
No | Individual review objects |
shippingDetails |
object |
No | Shipping details (falls back to defaultShippingDetails) |
returnPolicy |
object |
No | Return policy (falls back to defaultReturnPolicy) |
Docs: schema.org/LocalBusiness · Google: Local Business
<LocalBusinessSchema
name="My Shop"
telephone="+49 30 1234567"
address={{
streetAddress: 'Hauptstraße 42',
addressLocality: 'Berlin',
postalCode: '10119',
addressCountry: 'DE',
}}
openingHours={['Mo-Fr 09:00-18:00', 'Sa 10:00-16:00']}
/>All props fall back to defaultLocalBusiness from the integration config. A name is required — as a prop or in the defaults.
| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes* | Business name (*unless defaultLocalBusiness.name is set) |
url |
string |
No | Business website URL |
description |
string |
No | Short business description |
imageUrl |
string |
No | Business image URL |
telephone |
string |
No | Phone number |
email |
string |
No | Email address |
priceRange |
string |
No | Price range indicator, e.g. '$$' |
address |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
No | Postal address |
geo |
{ latitude: number; longitude: number } |
No | Geographic coordinates |
openingHours |
string[] |
No | Opening hours, e.g. ['Mo-Fr 09:00-18:00'] |
sameAs |
string[] |
No | Social profile / same-entity URLs (e.g. Google Business, Facebook) |
Docs: schema.org/BreadcrumbList · Google: Breadcrumb
<BreadcrumbSchema
items={[
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: 'My Post', url: '/blog/my-post' },
]}
/>| Prop | Type | Required | Description |
|---|---|---|---|
items |
{ name: string; url: string }[] |
Yes | Ordered breadcrumb items |
Docs: schema.org/BreadcrumbList · Google: Breadcrumb
Generates breadcrumbs automatically from the current URL path. Segments are converted from kebab-case to title case by default.
<AutoBreadcrumbSchema />
<!-- With custom labels -->
<AutoBreadcrumbSchema
homeLabel="Start"
labels={{ blog: 'Articles', 'my-post': 'My Post' }}
/>| Prop | Type | Required | Description |
|---|---|---|---|
homeLabel |
string |
No | Label for the root segment. Default: 'Home' |
labels |
Record<string, string> |
No | Override labels for specific URL path segments |
ignoreSegments |
string[] |
No | URL segments to skip — useful for language prefixes like ['de', 'en'] |
prependBreadcrumbs |
{ name: string; url: string }[] |
No | Breadcrumbs inserted after Home, before auto-generated segments |
appendBreadcrumbs |
{ name: string; url: string }[] |
No | Breadcrumbs appended after all auto-generated segments |
Docs: schema.org/Event · Google: Event
<EventSchema
name="Tech Meetup Berlin"
startDate="2024-06-15T18:00:00"
locationName="Hub Berlin"
locationAddress={{
streetAddress: 'Alexanderplatz 1',
addressLocality: 'Berlin',
postalCode: '10178',
addressCountry: 'DE',
}}
attendanceMode="Offline"
status="Scheduled"
/>
<!-- Online event: no venue, the stream URL becomes a VirtualLocation -->
<EventSchema
name="Astro Live Webinar"
startDate="2024-06-20T17:00:00+02:00"
attendanceMode="Online"
onlineUrl="https://example.com/live"
/>The location follows the attendanceMode, as Google requires: 'Offline' needs locationName + locationAddress (output as Place), 'Online' needs onlineUrl and allows no venue (output as VirtualLocation), 'Mixed' needs both (output as [Place, VirtualLocation]). Any other combination fails the build.
| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Event name |
startDate |
string | Date |
Yes | Start date/time |
endDate |
string | Date |
No | End date/time |
description |
string |
No | Event description |
imageUrl |
string | string[] |
No | Event image URL(s) |
locationName |
string |
Offline/Mixed | Venue name |
locationAddress |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
Offline/Mixed | Venue address |
onlineUrl |
string |
Online/Mixed | Absolute http(s) URL of the stream or virtual event (output as VirtualLocation) |
attendanceMode |
'Offline' | 'Online' | 'Mixed' |
No | Default: 'Offline' |
status |
'Scheduled' | 'Cancelled' | 'Postponed' | 'Rescheduled' |
No | Default: 'Scheduled' |
url |
string |
No | Event page URL |
price |
number | string |
No | Ticket price |
priceCurrency |
string |
No | ISO 4217 currency code |
availability |
'InStock' | 'OutOfStock' | 'PreOrder' | 'OnlineOnly' |
No | Ticket availability |
organizer |
{ name: string; url?: string } |
No | Organizing entity (recommended by Google) |
performer |
{ name: string; url?: string } |
No | Performer or speaker at the event |
Docs: schema.org/Organization · Google: Organization
<OrganizationSchema
name="ACME Corp"
logoUrl="https://example.com/logo.png"
sameAs={['https://twitter.com/acme', 'https://linkedin.com/company/acme']}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes* | Organization name (*unless defaultArticlePublisher.name is set) |
url |
string |
No | Organization URL (falls back to siteUrl) |
logoUrl |
string |
No | Logo URL (falls back to defaultArticlePublisher.logo; omitted if neither is set) |
sameAs |
string[] |
No | Social profile / same-entity URLs |
telephone |
string |
No | Phone number |
email |
string |
No | Email address |
address |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
No | Postal address |
Docs: schema.org/WebSite · Google: Sitelinks Searchbox
<WebSiteSchema name="My Site" />
<!-- With Sitelinks Searchbox -->
<WebSiteSchema name="My Site" searchQueryInput="q" />| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Site name |
url |
string |
No | Site URL (falls back to siteUrl) |
searchQueryInput |
string |
No | URL query param name to enable Sitelinks Searchbox, e.g. 'q' |
Docs: schema.org/WebPage · Google: WebPage
Generic page schema — use for landing pages, legal pages, or any page that doesn't fit a more specific type.
<WebPageSchema
title="About Us"
description="Learn more about our company."
inLanguage="en"
dateModified="2024-06-01"
/>| Prop | Type | Required | Description |
|---|---|---|---|
title |
string |
Yes | Page title |
description |
string |
No | Page description |
url |
string |
No | Canonical URL (falls back to current page URL) |
inLanguage |
string |
No | Content language, e.g. 'de' |
datePublished |
string | Date |
No | Publication date |
dateModified |
string | Date |
No | Last modified date |
isAccessibleForFree |
boolean |
No | Default: true |
image |
string |
No | Page image URL |
imageWidth |
number |
No | Image width in pixels |
imageHeight |
number |
No | Image height in pixels |
imageFormat |
string |
No | Image MIME type |
imageCaption |
string |
No | Image caption |
author |
{ name: string; url?: string; id?: string; type?: 'Person' | 'Organization' } |
No | Page author |
publisher |
{ name: string; url?: string; logo?: string } |
No | Publisher (falls back to defaultArticlePublisher) |
robots |
string |
No | Robots directive, e.g. 'noindex' (meta tag only, not part of the JSON-LD) |
alternates |
{ href: string; hreflang: string }[] |
No | Alternate language versions (hreflang links only, not part of the JSON-LD) |
Docs: schema.org/ProfilePage · Google: Profile Page
<ProfilePageSchema
name="Jane Doe"
description="Software engineer and writer"
imageUrl="https://example.com/jane.jpg"
sameAs={['https://github.com/janedoe']}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Person name |
description |
string |
No | Short bio |
imageUrl |
string |
No | Profile image URL |
sameAs |
string[] |
No | Social profile URLs |
publishingPrinciples |
string |
No | URL to editorial / publishing guidelines |
Docs: schema.org/CollectionPage
For product listing / archive pages.
<CollectionPageSchema
name="All Products"
description="Browse our full product catalogue"
products={[
{ name: 'Widget A', url: '/products/widget-a', imageUrl: '/images/widget-a.jpg', price: 9.99, priceCurrency: 'EUR' },
{ name: 'Widget B', url: '/products/widget-b' },
]}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Page / collection name |
description |
string |
Yes | Collection description |
products |
{ name: string; url: string; imageUrl?: string; price?: number | string; priceCurrency?: string }[] |
Yes | List of products |
Docs: schema.org/JobPosting · Google: Job Posting
<JobPostingSchema
title="Senior Developer"
description="<p>We're looking for a senior developer...</p>"
hiringOrganizationName="ACME GmbH"
datePosted="2024-06-01"
jobLocation={{
streetAddress: 'Hauptstraße 1',
addressLocality: 'Berlin',
postalCode: '10115',
addressCountry: 'DE',
}}
employmentType="FULL_TIME"
baseSalary={{ value: 80000, currency: 'EUR', unit: 'YEAR' }}
/>For remote positions, use jobLocationType instead of (or in addition to) jobLocation:
<JobPostingSchema
title="Remote Frontend Engineer"
description="<p>Fully remote position open worldwide.</p>"
hiringOrganizationName="ACME GmbH"
datePosted="2024-06-01"
jobLocationType="TELECOMMUTE"
applicantLocationRequirements="DE"
employmentType="FULL_TIME"
/>Note: Following Google's rules, on-site jobs need
jobLocation; fully remote jobs setjobLocationType="TELECOMMUTE"plusapplicantLocationRequirements; hybrid jobs set all three.applicantLocationRequirementsis only allowed for remote jobs.
| Prop | Type | Required | Description |
|---|---|---|---|
title |
string |
Yes | Job title |
description |
string |
Yes | Job description (HTML accepted by Google) |
datePosted |
string | Date |
Yes | ISO date the posting was published |
validThrough |
string | Date |
No | ISO date the posting expires (not before datePosted) |
employmentType |
'FULL_TIME' | 'PART_TIME' | 'CONTRACTOR' | 'TEMPORARY' | 'INTERN' | 'VOLUNTEER' | 'OTHER' |
No | Employment type (omitted if not set) |
hiringOrganizationName |
string |
Yes* | Hiring company name (*unless defaultArticlePublisher.name is set) |
hiringOrganizationUrl |
string |
No | Hiring company URL |
hiringOrganizationLogo |
string |
No | Hiring company logo URL |
jobLocation |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
No* | Job location — required unless jobLocationType is 'TELECOMMUTE' |
baseSalary |
{ value: number | string; currency: string; unit: 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR' } |
No | Salary details |
identifier |
{ name: string; value: string } |
No | Employer-specific job ID (e.g. { name: 'Acme', value: 'JR-12345' }) |
directApply |
boolean |
No | Shows "Apply on your site" badge in Google rich results |
jobLocationType |
'TELECOMMUTE' |
No | Set for remote positions |
applicantLocationRequirements |
string | string[] |
No* | Country/region where remote applicants must be located — required for 'TELECOMMUTE' jobs |
Docs: schema.org/SoftwareApplication · Google: Software App
<SoftwareAppSchema
name="My App"
operatingSystem="Web"
applicationCategory="BusinessApplication"
price={0}
priceCurrency="EUR"
ratingValue={4.8}
reviewCount={320}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | App name |
description |
string |
No | Short app description |
url |
string |
No | Link to the app or its landing page |
operatingSystem |
string |
No | e.g. 'Web', 'Windows, macOS' (omitted if not set) |
applicationCategory |
string |
No | e.g. 'BusinessApplication', 'Game' (omitted if not set) |
price |
string | number |
No | Price (use 0 for free apps) |
priceCurrency |
string |
No | ISO 4217 currency code |
ratingValue |
number |
No | Aggregate rating (0–5), only together with reviewCount |
reviewCount |
number |
No | Number of reviews, only together with ratingValue |
Docs: schema.org/Recipe · Google: Recipe
<RecipeSchema
name="Classic Chocolate Chip Cookies"
description="Crispy edges, chewy and soft in the center."
imageUrl="https://example.com/cookies.jpg"
authorName="Baker Bob"
prepTime="PT15M"
cookTime="PT10M"
recipeYield="12 cookies"
recipeCategory="Dessert"
recipeCuisine="American"
calories={220}
ingredients={[
"200g butter",
"150g brown sugar",
"2 eggs",
"300g flour",
"200g chocolate chips"
]}
instructions={[
{ text: "Preheat oven to 190°C.", name: "Preheat" },
{ text: "Mix butter, sugar, and eggs. Fold in dry ingredients and chocolate chips.", name: "Make Dough" },
{ text: "Scoop cookie balls onto sheet and bake for 10 minutes.", name: "Bake" }
]}
ratingValue={4.9}
reviewCount={45}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Recipe name |
description |
string |
Yes | Recipe description |
imageUrl |
string | string[] |
Yes | Image URL(s) |
authorName |
string | string[] |
Yes | Author name(s) |
authorType |
'Person' | 'Organization' |
No | Default: 'Person' |
prepTime |
string |
No | ISO duration, e.g. 'PT15M' |
cookTime |
string |
No | ISO duration, e.g. 'PT10M' |
totalTime |
string |
No | ISO duration |
recipeYield |
string | number |
No | Servings or yield |
recipeCategory |
string |
No | e.g. 'Dessert' |
recipeCuisine |
string |
No | e.g. 'American' |
calories |
number | string |
No | Calorie count |
ingredients |
string[] |
Yes | List of ingredient descriptions |
instructions |
string[] | InstructionStep[] |
Yes | Step-by-step instructions |
ratingValue |
number |
No | Rating value (0-5), only together with reviewCount |
reviewCount |
number |
No | Number of ratings, only together with ratingValue |
datePublished |
string | Date |
No | Publication date |
Docs: schema.org/VideoObject · Google: Video
<VideoSchema
name="Astro v6 Server Islands Tutorial"
description="Learn how to use server islands in Astro v6."
thumbnailUrl="https://example.com/thumb.jpg"
uploadDate="2026-06-01"
duration="PT8M45S"
contentUrl="https://example.com/video.mp4"
interactionCount={15420}
/>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Video title |
description |
string |
Yes | Video description |
thumbnailUrl |
string | string[] |
Yes | Thumbnail image URL(s) |
uploadDate |
string | Date |
Yes | Video upload date |
duration |
string |
No | ISO duration, e.g. 'PT8M45S' |
contentUrl |
string |
No* | URL to the actual video file — at least one of contentUrl or embedUrl required for indexing |
embedUrl |
string |
No* | URL to the embeddable video player — at least one of contentUrl or embedUrl required for indexing |
interactionCount |
number | string |
No | Total view counts |
expires |
string | Date |
No | Expiration date |
publisher |
{ name: string; logoUrl?: string } |
No | Publishing organization (recommended by Google) |
Generic component for schema.org types without a dedicated component. Only @type is required; the object is output as given (except keys that only drive meta tags or the sitemap, such as robots, alternates or changefreq), so checking its content is up to you. Like every component it accepts the sitemap props changefreq and priority.
---
import { Schema } from '@casoon/astro-structured-data/components';
---
<Schema item={{ '@type': 'Course', name: 'Astro Basics', provider: { '@type': 'Organization', name: 'ACME' } }} />| Prop | Type | Required | Description |
|---|---|---|---|
item |
{ '@type': string | string[]; [key: string]: unknown } |
Yes | schema.org object |
Renders all schemas of the page as a single @graph block when useGraph: true is set (renders nothing otherwise). Place it once in your base layout where the block should appear, e.g. at the end of <body>. It only marks the position: the @graph is filled in by the integration's middleware after the whole page has rendered, so schema components can be placed anywhere — before or after it, and in components that await data. A page with schemas but without <SchemaGraph />, or with more than one, fails the build instead of silently losing data.
---
import { SchemaGraph } from '@casoon/astro-structured-data/components';
---
<SchemaGraph />No props. Reads the schemas the other components registered for the current page.
All components ship with a matching Zod schema, exported from @casoon/astro-structured-data/zod. Use them in Content Collections, form validation, or any runtime validation. The components validate their props with exactly these schemas, and the component prop types are exported from the same entry point (ArticleProps, EventProps, ProductProps, …).
import {
articleZodSchema,
faqZodSchema,
productZodSchema,
localBusinessZodSchema,
eventZodSchema,
organizationZodSchema,
webPageZodSchema,
webSiteZodSchema,
profilePageZodSchema,
jobPostingZodSchema,
softwareAppZodSchema,
collectionPageZodSchema,
breadcrumbZodSchema,
autoBreadcrumbZodSchema,
recipeZodSchema,
videoZodSchema,
} from '@casoon/astro-structured-data/zod';Check any schema object for missing recommended fields (matching what the build warning reports):
import { validateRecommended } from '@casoon/astro-structured-data/zod';
import type { SchemaType, RecommendedWarning } from '@casoon/astro-structured-data/zod';
const warnings: RecommendedWarning[] = validateRecommended('Organization', {
name: 'ACME Corp',
url: 'https://acme.com',
});
// → [{ field: 'sameAs', message: 'Organization should include "sameAs" ...' }, ...]The type argument uses schema.org @type names: 'Article', 'BlogPosting', 'NewsArticle', 'FAQPage', 'Product', 'LocalBusiness', 'Event', 'Organization', 'WebPage', 'WebSite', 'ProfilePage', 'JobPosting', 'SoftwareApplication', 'CollectionPage', 'BreadcrumbList', 'Recipe', 'VideoObject'.
Fields marked as recommended are a subset of optional props that Google's Rich Results guidelines list as strongly beneficial — omitting them won't break validation but may reduce search result richness. Each recommended prop is declared once in the Zod schema together with the schema.org property it produces; validateRecommended (props) and the build-time check (rendered JSON-LD) are both derived from that declaration.
import { calculateReadingTime } from '@casoon/astro-structured-data/utils';Calculates word count and reading time from a plain-text or HTML string. Useful for populating wordCount and readingTimeMinutes on ArticleSchema from MDX content.
const { wordCount, readingTimeMinutes, timeRequired } = calculateReadingTime(content);
// timeRequired is ISO 8601 duration, e.g. 'PT4M'---
import { calculateReadingTime } from '@casoon/astro-structured-data/utils';
import { ArticleSchema } from '@casoon/astro-structured-data/components';
import { getEntry } from 'astro:content';
const post = await getEntry('blog', Astro.params.slug);
const { wordCount, readingTimeMinutes } = calculateReadingTime(post.body);
---
<ArticleSchema
title={post.data.title}
description={post.data.description}
datePublished={post.data.date}
authorName={post.data.author}
wordCount={wordCount}
readingTimeMinutes={readingTimeMinutes}
/>| Parameter | Type | Default | Description |
|---|---|---|---|
text |
string |
— | Plain text or HTML string |
wordsPerMinute |
number |
200 |
Reading speed used for the calculation |
Returns { wordCount: number; readingTimeMinutes: number; timeRequired: string }.
After every build the integration scans all output HTML for <script type="application/ld+json"> blocks. A block that is not valid JSON fails the build. With warnOnMissingRecommended: true (the default) it also logs one warning per missing recommended field per type (nested properties as dot paths, e.g. mainEntity.sameAs):
[structured-data] Organization is missing recommended field "sameAs" — add it for richer search results.
To disable:
structuredData({ warnOnMissingRecommended: false })npm test # unit tests and end-to-end tests (real static build, dev server and Node SSR)
npm run check # type-check, including the .astro components
npm run buildMIT