@matthesketh/utopia-content
v0.7.5
Published
Content collections, markdown pipeline, and MCP content server for UtopiaJS
Maintainers
Readme
@matthesketh/utopia-content
Content collections, markdown pipeline, and MCP content server for UtopiaJS.
Install
pnpm add @matthesketh/utopia-contentQuick Start
Define a content collection with a typed schema, then query entries from the filesystem.
import { createContent, defineCollection, getCollection, getEntry } from '@matthesketh/utopia-content';
// Initialize the content system
createContent({ contentDir: 'content' });
// Define a blog collection
const blog = defineCollection({
name: 'blog',
directory: 'blog',
schema: {
title: { type: 'string', required: true },
date: { type: 'date', required: true },
tags: { type: 'array', items: 'string' },
draft: { type: 'boolean', default: false },
},
formats: ['md', 'utopia', 'json', 'yaml'],
});
// Query all published posts
const posts = await getCollection('blog', {
filter: (e) => !e.data.draft,
sort: 'date',
order: 'desc',
});
// Get a single post by slug
const post = await getEntry('blog', 'hello-world');Content Formats
Markdown — YAML frontmatter + markdown body, rendered to HTML automatically:
---
title: Hello World
date: 2026-03-01
tags: [tutorial]
---
# Hello World
Content goes here.JSON — structured data:
{ "title": "Data Entry", "count": 42 }YAML — structured data in YAML format.
.utopia — interactive content with components. Export metadata from the script block:
<template>
<article>
<h1>{{ metadata.title }}</h1>
<button @click="increment">Count: {{ count() }}</button>
</article>
</template>
<script>
import { signal } from '@matthesketh/utopia-core';
export const metadata = { title: 'Interactive Post', date: '2026-03-01' };
const count = signal(0);
const increment = () => count.set(count() + 1);
</script>Schema Validation
Schemas validate frontmatter at query time. Missing required fields or type mismatches throw clear errors with the file path and field name.
| Type | Validates | Options |
|------|-----------|---------|
| string | typeof value === 'string' | required, default |
| number | typeof value === 'number' | required, default |
| boolean | typeof value === 'boolean' | required, default |
| date | Date object or valid date string | required |
| array | Array.isArray(value) | items: 'string' \| 'number' \| 'boolean' |
MCP Content Server
Expose content operations as MCP tools so AI agents can manage your blog.
import { createContentMCPServer } from '@matthesketh/utopia-content/mcp';
const server = createContentMCPServer({
contentDir: 'content',
collections: [blog],
});
// JSON-RPC 2.0 — same pattern as @matthesketh/utopia-ai/mcp
const response = await server.handleRequest(request);Tools: list_collections, list_entries, get_entry, create_entry, update_entry, delete_entry, search_entries, list_tags, publish_entry
Resources: content://{collection}, content://{collection}/{slug}
Vite Plugin
Hot-reload content files during development and generate a virtual module with the content manifest.
import utopia from '@matthesketh/utopia-vite-plugin';
import content from '@matthesketh/utopia-content/vite';
export default {
plugins: [utopia(), content({ contentDir: 'content' })],
};Import the manifest in route components:
import { collections } from 'virtual:utopia-content';Blog Template
Scaffold a complete blog project:
npx create-utopia my-blog # select "Content / Blog" in featuresThis creates a project with content directory, collection config, blog listing and single post routes, and the Vite content plugin pre-configured.
API Reference
createContent(options)
Initialize the content system. Call once before defineCollection.
contentDir— base directory for content files
defineCollection(config)
Register a content collection.
name— collection name (used in queries)directory— subdirectory withincontentDirschema— optional schema for frontmatter validationformats— allowed file formats (default: all)
getCollection(name, options?)
Query all entries in a collection. Returns ContentEntry[].
Options: filter, sort, order ('asc' | 'desc'), limit, offset
getEntry(name, slug)
Get a single entry by slug. Returns ContentEntry | null.
ContentEntry
interface ContentEntry {
slug: string;
collection: string;
data: Record<string, unknown>; // validated frontmatter
body: string; // raw content
html?: string; // rendered HTML (markdown only)
format: 'md' | 'utopia' | 'json' | 'yaml';
filePath: string;
}renderMarkdown(source, options?)
Render markdown to HTML. Options: remarkPlugins, rehypePlugins, highlight (default: true).
parseFrontmatter(content)
Extract YAML frontmatter and body from a string.
createFilesystemAdapter(baseDir?)
Create a filesystem-based content adapter (default).
