@casoon/astro-structured-data
Astro integration for automatic structured data (JSON-LD) generation. Supports Articles, FAQs, Products, Recipes, Videos, Breadcrumbs, Local Businesses, Events, Organizations, and more — with full TypeScript and Zod validation.
Landing Page | GitHub Repository | npm Package
Installation
npm install @casoon/astro-structured-data
Setup
Add 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,
})
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
siteUrl |
string |
— | Absolute base URL — falls back to Astro's site config |
useGraph |
boolean |
false |
Wrap all schemas in a @graph array |
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 |
LocalBusiness |
— | Site-wide local business defaults merged into LocalBusinessSchema |
defaultArticlePublisher |
Organization |
— | Default publisher for ArticleSchema and OrganizationSchema |
defaultBrand |
Brand | string |
— | Default brand for ProductSchema |
defaultShippingDetails |
OfferShippingDetails |
— | Default shipping details for ProductSchema |
defaultReturnPolicy |
MerchantReturnPolicy |
— | Default return policy for ProductSchema |
Compatibility
| Package version | Astro | Node.js |
|---|---|---|
| 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.
Dev Toolbar
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
Automated SEO & Sitemap Integration
When generateMeta: true is enabled, the integration automatically derives and renders corresponding <meta> and <link> elements inside the page <head> during render time:
- 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
Sitemap Metadata Support
You can also define sitemap crawl properties directly in your components (e.g. changefreq="weekly" priority={0.8}).
These properties are automatically encoded as data-attributes on the JSON-LD <script> tag. 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.
Components
Import components from @casoon/astro-structured-data/components:
---
import { ArticleSchema, FAQSchema } from '@casoon/astro-structured-data/components';
---
ArticleSchema
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 |
authorId |
string |
No | Author @id for linked data |
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 |
FAQSchema
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 |
ProductSchema
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 |
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) |
reviewCount |
number |
No | Number of reviews |
reviews |
ReviewItem[] |
No | Individual review objects |
shippingDetails |
object |
No | Shipping details (falls back to defaultShippingDetails) |
returnPolicy |
object |
No | Return policy (falls back to defaultReturnPolicy) |
LocalBusinessSchema
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.
| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
No | Business name |
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) |
BreadcrumbSchema
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 |
AutoBreadcrumbSchema
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 |
EventSchema
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"
/>
| 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 |
Yes | Venue name |
locationAddress |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
Yes | Venue address |
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 |
OrganizationSchema
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 |
No | Organization name (falls back to defaultArticlePublisher.name) |
url |
string |
No | Organization URL (falls back to siteUrl) |
logoUrl |
string |
No | Logo URL (falls back to defaultArticlePublisher.logo) |
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 |
WebSiteSchema
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' |
WebPageSchema
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 |
No | Publication date |
dateModified |
string |
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?: string } |
No | Page author |
publisher |
{ name: string; url?: string; logo?: string } |
No | Publisher (falls back to defaultArticlePublisher) |
robots |
string |
No | Robots directive, e.g. 'noindex' |
alternates |
{ href: string; hreflang: string }[] |
No | Alternate language versions |
ProfilePageSchema
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 |
CollectionPageSchema
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: '...', 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 |
JobPostingSchema
Docs: schema.org/JobPosting · Google: Job Posting
<JobPostingSchema
title="Senior Developer"
description="<p>We're looking for a senior developer...</p>"
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>"
datePosted="2024-06-01"
jobLocationType="TELECOMMUTE"
applicantLocationRequirements="DE"
employmentType="FULL_TIME"
/>
Note: Google requires either
jobLocationorapplicantLocationRequirements— at least one must be provided.
| Prop | Type | Required | Description |
|---|---|---|---|
title |
string |
Yes | Job title |
description |
string |
Yes | Job description (HTML accepted by Google) |
datePosted |
string |
Yes | ISO date the posting was published |
validThrough |
string |
No | ISO date the posting expires |
employmentType |
'FULL_TIME' | 'PART_TIME' | 'CONTRACTOR' | 'TEMPORARY' | 'INTERN' | 'VOLUNTEER' | 'OTHER' |
No | Default: 'FULL_TIME' |
hiringOrganizationName |
string |
No | Hiring company name (falls back to defaultArticlePublisher) |
hiringOrganizationUrl |
string |
No | Hiring company URL |
hiringOrganizationLogo |
string |
No | Hiring company logo URL |
jobLocation |
{ streetAddress, addressLocality, addressRegion?, postalCode, addressCountry } |
No* | Job location — required unless applicantLocationRequirements is set |
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 unless jobLocation is set |
SoftwareAppSchema
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'. Default: 'Web' |
applicationCategory |
string |
No | e.g. 'BusinessApplication', 'Game'. Default: 'DeveloperApplication' |
price |
string | number |
No | Price (use 0 for free apps) |
priceCurrency |
string |
No | ISO 4217 currency code |
ratingValue |
number |
No | Aggregate rating (0–5) |
reviewCount |
number |
No | Number of reviews |
RecipeSchema
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) |
reviewCount |
number |
No | Number of ratings |
datePublished |
string | Date |
No | Publication date |
VideoSchema
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) |
SchemaGraph
Renders all schemas registered via useGraph: true as a single @graph block. Place once in your base layout.
---
import { SchemaGraph } from '@casoon/astro-structured-data/components';
---
<SchemaGraph />
No props. Reads from Astro.locals.structuredDataGraph populated by the other components when useGraph is enabled.
Zod schemas
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.
import {
articleZodSchema,
faqZodSchema,
productZodSchema,
localBusinessZodSchema,
eventZodSchema,
organizationZodSchema,
webPageZodSchema,
webSiteZodSchema,
profilePageZodSchema,
jobPostingZodSchema,
softwareAppZodSchema,
collectionPageZodSchema,
breadcrumbZodSchema,
autoBreadcrumbZodSchema,
recipeZodSchema,
videoZodSchema,
} from '@casoon/astro-structured-data/zod';
validateRecommended
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.
Utilities
import { calculateReadingTime } from '@casoon/astro-structured-data/utils';
calculateReadingTime
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}
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 }.
Build-time warnings
When warnOnMissingRecommended: true (the default), the same recommended-field logic runs automatically after every build. The integration scans all output HTML for <script type="application/ld+json"> blocks and logs one warning per missing field per type:
[structured-data] Organization is missing recommended field "sameAs" — add it for richer search results.
To disable:
structuredData({ warnOnMissingRecommended: false })
License
MIT