From content files
to typed queries.

Turn Markdown and structured data into validated collections, connected queries, and rendered articles. Bring your own framework.

Markdown + dataYour content
ContentGraphValidate · Prepare
Your applicationQuery · Render

From source to data

Start with a content file, define its schema, and query the fields your application needs.

Learn more
content/posts/hello-world.md
---
title: Hello, world
draft: false
---

Content starts here.
contentgraph.config.ts
export const collections = {
  posts: defineCollection({
    source: "./content/posts",
    schema: schema.object({
      title: schema.string(),
      draft: schema.boolean(),
    }),
  }),
};
app.ts
const posts = await content.select("title", "draft").from("posts")
  .all();
[
  {
    "title": "Hello, world",
    "draft": false
  }
]

Flexible type-safe query syntax

Compare fluent chains and object-based queries. The same controls update both examples and their shared result.

Learn more
titledraft
Hello, worldfalse
What's nexttrue
Working with contentfalse
Writing with Markdownfalse
Fluent
const posts = await content
  .select("title")
  .from("posts");
Relational
const posts = await content.query.posts.findMany({
  columns: { title: true },
});
[
  {
    "title": "Hello, world"
  },
  {
    "title": "What's next"
  },
  {
    "title": "Working with content"
  },
  {
    "title": "Writing with Markdown"
  }
]

Typed queries

Select the fields you need. Catch misspelled fields before running your code.

Learn more
titledraft
Hello, worldfalse
What's nexttrue
Working with contentfalse
Writing with Markdownfalse
Typed query
const posts = await content
  .select("title")
  .from("posts");
[
  {
    "title": "Hello, world"
  },
  {
    "title": "What's next"
  },
  {
    "title": "Working with content"
  },
  {
    "title": "Writing with Markdown"
  }
]

Relationships

Connect posts to their authors. Select related fields in one query, with authored references checked during preparation.

Learn more

posts

  • Hello, worldauthorId: ada
  • What's nextauthorId: grace
  • Working with contentauthorId: ada

authors

  • Ada Lovelaceid: ada
  • Grace Hopperid: grace
Relationship definition
relationships: {
  author: {
    collection: "authors",
    type: "one",
    from: "authorId",
    to: "id",
    referenceSide: "from",
  },
}
Query
const posts = await content.select("title").from("posts")
  .where((fields) => eq(fields.authorId, "ada"))
  .all();
[
  {
    "title": "Hello, world"
  },
  {
    "title": "Working with content"
  }
]

Flexible sources

Markdown, JSON, YAML, or TOML. Different formats, the same query API. Use Astro-compatible or custom loaders for other sources.

Learn more
content/posts/hello-world.md
---
title: Hello, world
draft: false
authorId: ada
---

Content starts here.
contentgraph.config.ts
source: {
  type: "markdown",
  directory: "./content/posts",
}
{
  "id": "hello-world",
  "title": "Hello, world",
  "draft": false,
  "authorId": "ada"
}

Typed collections

Validate content with Zod or another Standard Schema library. Query results follow your schema.

Learn more
content/posts/hello-world.md
---
title: Hello, world
draft: false
authorId: ada
---
Schema
defineCollection({
  source: "./content/posts",
  schema: schema.object({
    title: schema.string(),
    draft: schema.boolean(),
    authorId: schema.string(),
  }),
});
Validation passed
File
content/posts/hello-world.md

Computed fields

Use built-in reading time, word count, and excerpt helpers, or write custom computed fields. Query them alongside authored metadata.

Learn more

Hello, world

3 min read

Computed field
import { excerpt, readingTime, wordCount } from "contentgraph/computed";

computed: {
  readingMinutes: readingTime(),
  wordCount: wordCount(),
  excerpt: excerpt({ maxLength: 160 }),
}
Query the computed field
Typed query
const post = await content.select("title", "readingMinutes").from("posts")
  .first();

Markdown rendering

Get HTML, syntax highlighting, and headings for your table of contents. Article output stays separate from metadata queries.

Learn more
content/posts/hello-world.md
## Hello, world

Content starts here.

```ts
const greeting = "Hello, world";
```

Hello, world

Content starts here.

const greeting = "Hello, world";
Render API
content.render()
const article = await content.render(
  "examples",
  "hello-world",
);

Vite, Next.js, Angular, or standalone

Keep content fresh during development, or build portable artifacts with the CLI. Your framework owns routes and rendering.

Learn more
  1. Ready to edit
  2. Edit the title
  3. Save the file
  4. Page data refreshed
content/posts/hello-world.md
---
title: Hello, world
draft: false
authorId: ada
---
Hello, world

Content starts here.

Make your first collection.

Start with a Markdown file and a schema.

Follow the quick start

ContentGraph

Built with Void