Content API#

Everything importable from @janux/content. Guide: Content collections.

import {
  defineCollection,
  getCollection,
  getEntry,
  render,
  slugify,
  parseFrontmatter,
  splitFrontmatter,
  validateFrontmatter,
} from '@janux/content';

defineCollection(config)#

Declares a collection and returns it. There is no registry: the returned object is what you pass to getCollection and getEntry, which is what makes the entries typed without a codegen step.

import { defineCollection } from '@janux/content';
import { list, schema, str } from 'janux';

export const notes = defineCollection({
  dir: 'content/notes',
  schema: schema({ title: str(), tags: list(str()).default([]) }),
});
Option Meaning
dir Where the files live. Absolute, or relative to the app root (JANUX_APP_ROOT, published by the framework; the working directory otherwise). Resolved once, when this call runs.
schema A schema validating every file's frontmatter. Infer of it is what entry.data reads as.

Files ending in .md and .mdx are content; anything else in the directory is ignored. Two files that resolve to the same id (a.md and a.mdx) throw.

getCollection(collection, filter?)#

Every entry, ordered by id. The optional filter runs on validated data, so it is a plain predicate over typed fields.

const published = getCollection(notes, (note) => !note.data.draft);

Synchronous, so it can feed staticParams(). A directory that does not exist is empty, not an error; a file whose frontmatter fails the schema throws, naming the file and every bad field.

getEntry(collection, id)#

One entry by id, or undefined.

const note = getEntry(notes, 'guide/schema');

Ids come from URLs, so they are matched against the collection's own listing rather than joined onto a path: ../secrets finds nothing.

CollectionEntry#

Field Type What it is
id string Path inside the collection, extension dropped, POSIX separators.
data Infer<schema> Validated frontmatter.
body string Content with the frontmatter block removed.
format 'md' | 'mdx' Which reading the file gets.
file string Absolute path of the source file.

CollectionEntry<typeof notes> is the type of one entry — parameterised by the collection, not by its schema.

render(entry, options?)#

Compiles an entry's body into a Janux component. Async, cached by the body's own source.

const { Content, headings } = await render(note, { components: { Poll } });
Returns What it is
Content The body as a component: <Content />.
headings { depth, id, text }[] in document order, with the same ids stamped on the rendered elements.

options.components maps names the content may use. A capitalised key is a component the body can mount — a component() becomes a real island, a foreign() mounts React unchanged. A lowercase key overrides an element (h2, code, a).

Compilation happens on the server: no MDX runtime reaches the browser, and with output: 'static' it happens once, at build time.

The compiler is an optional peer dependencybun add @mdx-js/mdx. Collections do not need it; only render() does, and calling it without the compiler installed says so.

An .mdx body is trusted code: its expressions are evaluated on the server with the access any module of yours has, and components scopes which tags it may name, not what it may do. See content files are code.

slugify(text)#

The heading-id function render uses. Exported so an app can produce the same id for a heading it writes itself.

slugify('Deep, with punctuation!'); // 'deep-with-punctuation'

parseFrontmatter(source)#

Splits a file into { data, body } and parses the frontmatter block as YAML. Nothing is validated — this is the layer underneath a collection, for tooling that needs the raw fields.

const { data, body } = parseFrontmatter('---\ntitle: Hi\n---\n# Hi\n');

Parsed with YAML's core schema, so 2026-07-01 stays a string rather than becoming a Date the schema layer has no kind for.

splitFrontmatter(source)#

Just the split: { yaml, body }, with yaml undefined when the file opens with content. Only column 0 of line 1 opens a block; a --- anywhere else is a thematic break. An unterminated block throws rather than being read as body.

validateFrontmatter(schema, data, file)#

Validates parsed frontmatter and returns the typed value, or throws an error naming the file and every failing field. This is the framework's own validate() — the same function intents run their input through.

const data = validateFrontmatter(schema({ title: str() }), { title: 'Hi' }, 'content/hi.md');