@molecule/api-content-markdown
Provider bond · content · API (Node) · v1.0.2 · Apache-2.0
Content provider for front-matter markdown files: YAML front matter between --- fences plus a markdown body, read from a directory, with loud validation of malformed files
npm install @molecule/api-content-markdownnpm · Source on GitHub · Implements @molecule/api-content
How it works
@molecule/api-content-markdown is a provider bond on the API (Node) side: it implements the content core interface (@molecule/api-content) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { readContentDirectory, setProvider } from '@molecule/api-content'
import { provider as markdown } from '@molecule/api-content-markdown'
setProvider(markdown) // once, at startup (bonds.ts) or in a build script
const posts = await readContentDirectory('content/posts', {
requiredFields: ['title', 'date', 'description'],
})
for (const post of posts) console.log(post.slug, post.title, post.date)Works with: @molecule/api-content
Live demos
Running apps built with Molecule that install this package — open the app or study the template.
Reference
Content provider for @molecule/api-content backed by markdown files with
YAML front matter: ---, a YAML mapping, ---, then the body. Reads one
file or a whole directory into ContentRecords, newest first, drafts
excluded, and fails loudly on anything malformed. The natural source for a
blog, a docs site or a changelog: files in the repo, no database.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { readContentDirectory, setProvider } from '@molecule/api-content'
import { provider as markdown } from '@molecule/api-content-markdown'
setProvider(markdown) // once, at startup (bonds.ts) or in a build script
const posts = await readContentDirectory('content/posts', {
requiredFields: ['title', 'date', 'description'],
})
for (const post of posts) console.log(post.slug, post.title, post.date)
A post file:
---
title: Hello
date: 2026-09-07
description: The first post.
tags: [intro]
---
The body, in markdown.
Type
provider
Installation
npm install @molecule/api-content-markdown @molecule/api-content yaml
API
Interfaces
MarkdownContentConfig
Provider configuration for createProvider. Everything has a
default; createProvider() with no argument is the common case.
interface MarkdownContentConfig {
/**
* File extensions read by `readDirectory`, lowercase with the dot.
* Defaults to `['.md', '.markdown']`.
*/
extensions?: string[]
/** Whether `readDirectory` descends into subdirectories. Defaults to `true`. */
recursive?: boolean
/**
* Where the slug comes from: `'frontMatter'` uses the front matter's `slug`
* when present and falls back to the file name; `'filename'` always uses
* the file name without its extension. Defaults to `'frontMatter'`.
*/
slugFrom?: 'frontMatter' | 'filename'
/** The front-matter field that marks a draft. Defaults to `'draft'`. */
draftField?: string
/** The front-matter field that holds the date. Defaults to `'date'`. */
dateField?: string
}
Functions
contentError(path, reason, line)
Builds the error every malformed file raises, shaped exactly as
@molecule/api-content documents it (code, path, reason, line?).
function contentError(path: string, reason: string, line?: number): ContentValidationError
path— The offending file.reason— What was wrong, one sentence.line— The 1-based line when known.
Returns: The error, ready to throw.
splitFrontMatter(source, path)
Splits a file into its front matter (raw YAML text) and body. A file with no opening fence has no front matter and is all body.
function splitFrontMatter(
source: string,
path: string,
): { yaml: string | null; body: string; bodyLine: number }
source— The file's text.path— For the error.
Returns: The raw YAML (or null), the body, and the body's first line number.
parseFrontMatter(yaml, path)
Parses the raw YAML into a plain object, or throws with the YAML line.
function parseFrontMatter(yaml: string, path: string): Record<string, unknown>
yaml— Raw YAML text (may be empty).path— For the error.
Returns: The mapping, {} when the YAML is empty.
createProvider(config)
Creates a content provider for front-matter markdown files.
function createProvider(config?: MarkdownContentConfig): ContentProvider
config— Provider configuration; every field has a default.
Returns: A ContentProvider for @molecule/api-content.
Constants
SLUG_RE
A slug: lowercase letters, digits and hyphens; starts with a letter or digit.
const SLUG_RE: RegExp
provider
The default provider: .md/.markdown, recursive, slug from front matter then file name.
const provider: ContentProvider
Core Interface
Implements @molecule/api-content interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-content'
import { provider } from '@molecule/api-content-markdown'
export function setupContentMarkdown(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-content^1.0.0
Runtime Dependencies
-
@molecule/api-content -
yaml -
Malformed input throws, never skips. An opening
---without a closing one, invalid YAML (with the YAML line), a front matter that is not a mapping, a missing or empty required field, adraftthat is not a boolean, adatethat does not parse, a slug that is not lowercase letters, digits and hyphens, or a slug another file already uses: each throws aContentValidationErrornaming the file. The build that reads the content should let it fail. -
Slug. From the front matter's
slugwhen present, else the file name without its extension, lowercased (slugFrom: 'filename'ignores the front matter). It is the URL, so it is validated. -
Date.
datemay be a YAML date or a string; it is normalized to an ISO-8601 string on the record (record.date), and the raw value stays infrontMatter.date. Records without a date sort last. -
Drafts.
draft: truemarks a draft;readDirectorydrops drafts unlessincludeDraftsis set.parsereturns them either way. -
Body. Everything after the closing fence, with one leading blank line removed. It is raw markdown: render it with
@molecule/app-markdownand a provider such as@molecule/app-markdown-marked. -
Files.
.mdand.markdown, recursively, sorted by path before parsing so errors are reported in a stable order. Configure both withcreateProvider({ extensions, recursive }).