@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-markdown

npm · 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.ts JSDoc, 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, a draft that is not a boolean, a date that does not parse, a slug that is not lowercase letters, digits and hyphens, or a slug another file already uses: each throws a ContentValidationError naming the file. The build that reads the content should let it fail.

  • Slug. From the front matter's slug when present, else the file name without its extension, lowercased (slugFrom: 'filename' ignores the front matter). It is the URL, so it is validated.

  • Date. date may 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 in frontMatter.date. Records without a date sort last.

  • Drafts. draft: true marks a draft; readDirectory drops drafts unless includeDrafts is set. parse returns them either way.

  • Body. Everything after the closing fence, with one leading blank line removed. It is raw markdown: render it with @molecule/app-markdown and a provider such as @molecule/app-markdown-marked.

  • Files. .md and .markdown, recursively, sorted by path before parsing so errors are reported in a stable order. Configure both with createProvider({ extensions, recursive }).