Structured Linked Data
Use YAML frontmatter to define schemas, metadata, and semantic types
Structured Linked Data
Use YAML frontmatter to define schemas, metadata, and semantic types:
---
$type: BlogPost
$id: https://blog.example.com/posts/hello-world
title: Hello World
author: Jane Developer
date: 2025-10-27
tags: [tutorial, mdx, introduction]
status: published
---
# Hello World
This is a blog post with structured frontmatter.The frontmatter is parsed as structured data, validated against schemas (using Zod or JSON Schema), and can be queried like a database. The $type and $id fields enable semantic web patterns, making your documents part of a global knowledge graph.
YAML-LD: YAML + Linked Data
MDX frontmatter uses YAML-LD (YAML with Linked Data), which combines the readability of YAML with the semantic capabilities of JSON-LD.
Learn more:
- JSON-LD Specification - W3C standard for linked data
- YAML Specification - Human-friendly data serialization
- Schema.org - Shared vocabularies for structured data
Why $ instead of @?
Traditional JSON-LD uses @ prefixes (@type, @id, @context), but we use $ because:
-
JavaScript/TypeScript compatibility:
$is a valid identifier prefix in JS/TS,@is notconst data = { $type: "BlogPost" } // ✅ Valid JS const data = { @type: "BlogPost" } // ❌ Syntax error -
YAML compatibility:
$requires no special quoting in YAML$type: BlogPost # ✅ Clean YAML "@type": BlogPost # ❌ Requires quotes -
TypeScript discriminators:
$typeworks naturally as a discriminated union fieldtype Content = | { $type: 'BlogPost', title: string } | { $type: 'Product', price: number }
This means your frontmatter is simultaneously valid YAML, valid TypeScript, and semantically linked data.
YAML-LD → JSON-LD Conversion
YAML-LD frontmatter converts directly to JSON-LD:
# YAML-LD (frontmatter)
$type: schema.org/BlogPost
$id: https://blog.example.com/posts/hello-world
$context: https://schema.org
title: Hello World// JSON-LD (output)
{
"@type": "BlogPost",
"@id": "https://blog.example.com/posts/hello-world",
"@context": "https://schema.org",
"title": "Hello World"
}The conversion happens automatically when generating JSON-LD for SEO or graph databases.
Why Structured Data?
Structured data in MDX frontmatter enables:
- Type safety: Validate documents against TypeScript/Zod schemas
- Semantic web: Link documents using
$typeand$idfields - Database queries: Query collections of documents by frontmatter fields
- SEO optimization: Generate rich snippets and structured data for search engines
- Knowledge graphs: Build interconnected knowledge bases with relationships
TypeScript Discriminators
The $type field (and optionally $type + $context) provides TypeScript discriminated unions:
// Define content types
type BlogPost = {
$type: 'BlogPost'
title: string
author: string
date: string
}
type Product = {
$type: 'Product'
name: string
price: number
sku: string
}
// Discriminated union
type Content = BlogPost | Product
// TypeScript narrows the type based on $type
function processContent(content: Content) {
if (content.$type === 'BlogPost') {
// TypeScript knows this is BlogPost
console.log(content.title, content.author)
} else {
// TypeScript knows this is Product
console.log(content.name, content.price)
}
}With $context, you can have even more specific discriminators:
type SchemaOrgProduct = {
$type: 'Product'
$context: 'https://schema.org'
name: string
offers: { price: number }
}
type GS1Product = {
$type: 'Product'
$context: 'https://gs1.org'
gtin: string
glnLocation: string
}
// Same $type, different context
type Product = SchemaOrgProduct | GS1ProductAccessing Frontmatter in MDX
The YAML frontmatter is automatically injected into your rendered MDX as the data object, alongside any props passed during rendering:
---
$type: BlogPost
title: Hello World
author: Jane Developer
publishDate: 2025-10-27
views: 1234
---
# {data.title}
**Author:** {data.author}
**Published:** {new Date(data.publishDate).toLocaleDateString()}
**Views:** {data.views.toLocaleString()}
{props.showFullContent && (
<div>Full content here (controlled by props)</div>
)}
export function getStats() {
return {
type: data.$type,
title: data.title,
author: data.author,
isRecent: new Date(data.publishDate) > new Date('2025-01-01')
}
}data vs props
data: Comes from YAML frontmatter (static, defined in the file)props: Passed when rendering (dynamic, from parent component or build process)
// Rendering MDX with props
import BlogPost from './blog-post.mdx'
function App() {
return (
<BlogPost
showFullContent={true}
currentUser="[email protected]"
theme="dark"
/>
)
}---
$type: BlogPost
title: My Post
author: Jane Developer
---
# {data.title}
{/* data from frontmatter */}
Author: {data.author}
{/* props from parent */}
Current user: {props.currentUser}
Theme: {props.theme}
{/* Combine both */}
{props.currentUser === data.author && (
<div>You are viewing your own post!</div>
)}Semantic Fields
Three special fields enable semantic web patterns:
$type
Defines the semantic type of the document, typically from schema.org or a custom ontology:
$type: schema.org.ai/ProductThis makes the document part of a global knowledge graph and enables validation against type-specific schemas.
$id
Provides a globally unique identifier (URI) for the document:
The $id enables other documents to reference this one using links or relationships.
$context
Optional field for specifying JSON-LD context:
$context:
schema: https://schema.org/
onet: https://services.onetcenter.org/Validation
Validate documents against Zod schemas:
import { parseMDXLD, validateSchema } from 'mdxld'
import { z } from 'zod'
const BlogPostSchema = z.object({
$type: z.literal('BlogPost'),
$id: z.string().url(),
title: z.string(),
author: z.string(),
date: z.string().datetime(),
tags: z.array(z.string()),
status: z.enum(['draft', 'published', 'archived']),
})
const parsed = parseMDXLD(mdxContent)
const validated = validateSchema(parsed.frontmatter, BlogPostSchema)Ontology Integration
Leverage 70+ .org.ai ontologies for standardized vocabularies:
---
$type: schema.org.ai/Product
sku: WIDGET-X-001
name: Widget X
brand:
$type: schema.org.ai/Brand
name: Acme Corp
offers:
$type: schema.org.ai/Offer
price: 99.99
priceCurrency: USD
---