npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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.

npm version GitHub repository Website

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 |

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.org in 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 from item.robots or item.noindex / item.nofollow)
  • Author: <meta name="author" content="..."> (derived from schema item.author)
  • Reading Time: <meta name="reading-time" content="..."> (non-standard; derived from item.readingTime or parsed from ISO duration item.timeRequired)
  • Alternates (hreflang): <link rel="alternate" hreflang="..." href="..."> (extracted from item.alternates or 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's siteUrl)
  • 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 jobLocation or applicantLocationRequirements — 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