Content Collections
Content collections
Write content in Markdown with frontmatter, and x automatically turns it into pages. Perfect for blogs, documentation, and any content-driven site.
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 — no await 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 blog template is a working example: content/posts/*.md with a [slug].tsx page that renders each post via renderMarkdown.