.do

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:

Why $ instead of @?

Traditional JSON-LD uses @ prefixes (@type, @id, @context), but we use $ because:

  1. JavaScript/TypeScript compatibility: $ is a valid identifier prefix in JS/TS, @ is not

    const data = { $type: "BlogPost" }  // ✅ Valid JS
    const data = { @type: "BlogPost" }  // ❌ Syntax error
  2. YAML compatibility: $ requires no special quoting in YAML

    $type: BlogPost     # ✅ Clean YAML
    "@type": BlogPost   # ❌ Requires quotes
  3. TypeScript discriminators: $type works naturally as a discriminated union field

    type 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 $type and $id fields
  • 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 | GS1Product

Accessing 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/Product

This 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:

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

Next Steps