@casoon/astro-structured-data
v1.4.1
Published
An Astro integration to automatically generate structured data (JSON-LD) for articles, FAQs, products, recipes, videos, breadcrumbs, local businesses, events, and more.
Maintainers
Readme
@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-dataSetup
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 |
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'.
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
