structured-data-kit
v0.1.3
Published
TypeScript toolkit for producing and validating correct schema.org JSON-LD: typed builders, an ajv-backed validator, an HTML extractor, a CLI, and a GitHub Action.
Downloads
560
Maintainers
Readme
structured-data-kit
Valid, typed schema.org JSON-LD — and a way to catch regressions before they cost you rich results.
Structured data drives rich results, but it regresses silently: a renamed CMS
field, an optional object emitted as null, a rating with zero reviews, or
JSON-LD that describes data not actually on the page. structured-data-kit is a
focused TypeScript toolkit that makes those mistakes hard:
- Typed builders for the common types (
Product,Article/BlogPosting,BreadcrumbList,Organization,FAQPage) that construct valid JSON-LD and enforce the tricky inclusion rules at the API level — so you can't accidentally emit an invalidaggregateRatingor anullfield. - A validator (
ajv+ bundled JSON Schemas) that checks an object is valid JSON-LD and a valid schema.org shape — required fields, enum URLs, ISO dates,@context/@type. - An extractor, CLI, and GitHub Action that pull every
<script type="application/ld+json">out of rendered HTML (a file or a live URL) and validate it — so structured data can be checked in CI against real output.
It's a standalone library with no external services. Tested in CI from the first commit.
⚠️ Structured data must match the visible page
These builders make valid JSON-LD. They cannot make it truthful — that's on you. Feeding them data your page doesn't actually show is a Google policy violation ("don't cloak"), not just a quality issue. Always build from the same data object the page renders, and only mark up content a user can see. This kit refuses to invent defaults precisely so it never fabricates fields for you.
For developers: build JSON-LD
npm install structured-data-kitimport { buildProduct, Availability } from "structured-data-kit";
const product = buildProduct({
name: "Aeropress Go Travel Coffee Press",
image: "https://example.com/img/aeropress-go.jpg",
brand: "Aeropress",
sku: "AP-GO-001",
offer: {
price: 39.95,
priceCurrency: "USD",
availability: Availability.InStock, // -> "https://schema.org/InStock"
url: "https://example.com/brewers/aeropress-go",
},
aggregateRating: { ratingValue: 4.8, reviewCount: 312 },
});
// product is a schema-dts-typed WithContext<Product>Emitting in React/JSX
JSX text escaping corrupts JSON, so emit JSON-LD via dangerouslySetInnerHTML
(serializing the same object your page renders from):
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(product) }}
/>The library is framework-agnostic; this is just the correct way to put a JSON
string into a <script> from JSX.
Supported types & inclusion rules
| Builder | @type | Required input |
| --- | --- | --- |
| buildProduct | Product | name, image, offer (price, priceCurrency, availability) |
| buildArticle / buildBlogPosting / buildNewsArticle | Article / BlogPosting / NewsArticle | headline, image, datePublished, author |
| buildBreadcrumbList | BreadcrumbList | items[] (name, optional item) |
| buildOrganization | Organization | name, url, logo |
| buildFAQPage | FAQPage | items[] (question, answer) |
| buildRecipe | Recipe | name, image, recipeIngredient[], recipeInstructions[] |
| buildEvent | Event | name, startDate, location (Place or virtual { url }) |
| buildVideoObject | VideoObject | name, description, thumbnailUrl, uploadDate |
The rules the builders (and validator) enforce — the ones that quietly cost rich results:
aggregateRatingonly with real reviews. It is included only whenreviewCount > 0andratingValue > 0. EmittingreviewCount: 0is a Rich Results violation, so the Product builder omits the rating otherwise.- Omit optionals; never
nullor"". Absent optional fields are dropped entirely (the builders never set keys tonull, andJSON.stringifydropsundefined). No emptybrand, nonullimage. - Enumerations are schema.org URLs.
availabilityishttps://schema.org/InStock, never the bare string"InStock". Use the exportedAvailability,EventStatus, andEventAttendanceModeconstants. - Durations are ISO 8601. Recipe times (
prepTime,cookTime,totalTime) and videodurationare durations likePT20M— the validator enforces the format. - Dates are ISO 8601.
datePublished,dateModified, etc. @contextis exactlyhttps://schema.organd every node has a correct@type.BreadcrumbListpositions are 1-indexed, assigned automatically.
Note: the FAQ rich result is deprecated
Google deprecated the FAQ rich result (announced 2026-05-15); FAQ
structured data no longer produces an enhanced result for the vast majority of
sites. buildFAQPage and the FAQPage schema are retained because the markup
is still valid schema.org, but validate() emits a warning for FAQPage
nodes so you're not surprised when the rich result doesn't appear.
For CI: validate real output
Both the CLI and the Action extract every JSON-LD block from rendered HTML
(handling multiple <script> tags and @graph arrays), validate each, and fail
on any invalid or unparseable block.
CLI
# Validate a file or a live URL (exits non-zero on any invalid block)
npx sdk validate examples/product-page.html
npx sdk validate https://example.com/product/123
# Build an object from an input JSON file
npx sdk build product --input data.json
npx sdk build product -i data.json -o product.jsonld
# Just extract the JSON-LD nodes (no validation)
npx sdk extract examples/product-page.htmlvalidate prints a per-node summary (✓/✗, plus warnings) and sets the exit
code; add --json for a machine-readable report.
GitHub Action
# .github/workflows/structured-data.yml
name: structured-data
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: your-org/structured-data-kit/action@v1
with:
files: |
dist/product.html
dist/article.html
# urls: https://staging.example.com/product/123
fail-on-invalid: "true" # defaultSee action/README.md for inputs and outputs.
Validate programmatically
import { validate, extract, checkHtml } from "structured-data-kit";
validate(product); // { valid, type, errors[], warnings[] }
// Pull and validate everything from a page's HTML in one call:
const report = checkHtml(htmlString);
if (!report.valid) {
/* report.parseErrors + report.nodes[].result.errors */
}
// Or just the raw nodes:
const { nodes, errors } = extract(htmlString);Development
npm install
npm run typecheck # tsc --noEmit
npm test # vitest
npm run lint # eslint
npm run build # bundle the library + CLI, emit .d.ts
npm run build:action # bundle the GitHub Action into action/dist
npx tsx examples/build-product.tsContributing
See CONTRIBUTING.md. In short: add a builder + its JSON Schema
- tests together, keep the inclusion rules airtight, and never fabricate fields.
License
MIT.
