newX 1.3: islands to disk, server-mode islands, and an image proxy

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.

x.config.ts
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.

content/posts/hello-world.md
---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.

src/pages/blog/[slug].tsx
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.

content entry
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.