Content Collections
Content collections
Write content in Markdown with frontmatter and X turns it into pages for blogs, documentation, and most other content-driven sites.
Configuration
Point the content directory in x.config.ts to a folder with your markdown files.
import { defineConfig } from "@thexjs/core";export default defineConfig({ contentDir: "content",});Markdown with frontmatter
Each markdown file starts with frontmatter (YAML between --- delimiters) followed by markdown content.
---title: Hello Worlddate: 2026-03-15tags: [getting-started, tutorial]author: Jane Doe---## Welcome to X!This is your first post using X's content collection system.You can write **markdown** with all the usual syntax:- Lists- **Bold** and *italic* text- `inline code` and code blocks```tsconst greeting = "Hello from x!";console.log(greeting);```Reading content in a loader
Use scanContent to discover files and renderMarkdown to convert markdown to HTML in your loaders.
import type { RouteProps, LoaderArgs } from "@thexjs/core";import { scanContent, renderMarkdown } from "@thexjs/core";export async function loader({ params }: LoaderArgs) { const posts = scanContent("content/posts"); const post = posts.find((p) => p.slug === params.slug); if (!post) return new Response(null, { status: 404 }); const html = renderMarkdown(post.body); return { post: { ...post, html } };}export default function BlogPost({ loaderData }: RouteProps) { const { post } = loaderData as { post: { frontmatter: Record<string, unknown>; html: string }; }; return ( <article className="prose max-w-none"> <h1 className="text-4xl font-bold">{post.frontmatter.title}</h1> <p className="text-sm text-muted-foreground"> {String(post.frontmatter.date)} · {String(post.frontmatter.author)} </p> <div className="mt-8 leading-relaxed" dangerouslySetInnerHTML={{ __html: post.html }} /> > </article> );}scanContent API
scanContent(directory) scans a subdirectory of your content folder and returns an array of content entries. Each entry includes slug, body (the raw markdown), and frontmatter (parsed YAML). Both this and renderMarkdown are synchronous, so no await is needed.
interface ContentEntry { slug: string; // "posts/hello-world" — route-safe path body: string; // markdown after the frontmatter block frontmatter: Record<string, string | number | boolean | string[] | null>;}renderMarkdown API
renderMarkdown(markdownString) converts markdown to an HTML string. It is a lightweight, dependency-free renderer: headings, paragraphs, lists, links, inline code, code blocks, bold/italic, and blockquotes are supported, and all output is HTML-escaped by default (escapeHtml is exported separately too). It does not run a full markdown engine or syntax highlighter, so for heavy-duty content you can swap in your own renderer and feed the result to dangerouslySetInnerHTML.
Auto-routes
Every .md/.mdx file under contentDir becomes a route at its own path during build and dev. The examples/blog app in the repo is a working example: content/posts/*.md with a [slug].tsx page that renders each post via renderMarkdown.