Domphy

Frontmatter

A markdown file can begin with a YAML block called frontmatter — metadata such as a page title, description, publication date, or any custom fields your app needs. @domphy/markdown strips the block before markdown parsing and returns the parsed values alongside the document body.

Format

A frontmatter block is delimited by --- lines and must appear at the very start of the file:

---
title: Getting Started
description: A quick tour of the API.
author: Jane
tags:
  - tutorial
  - intro
published: 2024-01-15
---

# Getting Started

The actual document begins here.

The closing delimiter may also be ... (the YAML document-end marker). Leading or trailing whitespace on the closing line is ignored. Everything after the closing --- or ... line is the document body.

TIP

A leading --- with no matching closing fence is treated as a thematic break, not frontmatter. The --- remains in the content and the markdown parser emits an <hr> for it.

splitFrontmatter

splitFrontmatter(markdown) is the lowest-level helper. It splits the YAML block from the rest of the content without doing any markdown parsing:

import { splitFrontmatter } from "@domphy/markdown"

const source = `---
title: My Page
draft: true
tags:
  - alpha
  - beta
---

# Body starts here
`

const { frontmatter, content } = splitFrontmatter(source)

frontmatter  // { title: "My Page", draft: true, tags: ["alpha", "beta"] }
content      // "\n# Body starts here\n"
FieldTypeDescription
frontmatterRecord<string, unknown>Parsed YAML key-value pairs, or {} when the block is absent or unparseable.
contentstringThe markdown text with the frontmatter block removed.

splitFrontmatter never throws. If the YAML inside the block is malformed, the block is still stripped from content and frontmatter returns as {}. This ensures a broken header never crashes a build.

parseMarkdown returns frontmatter too

parseMarkdown calls splitFrontmatter internally. When using parseMarkdown you do not need to call splitFrontmatter yourself:

import { parseMarkdown } from "@domphy/markdown"

const { frontmatter, body, toc } = parseMarkdown(`---
title: My Page
description: Page description here.
tags:
  - guides
---

# My Page

Content.
`)

frontmatter.title        // "My Page"
frontmatter.description  // "Page description here."
frontmatter.tags         // ["guides"]

The body array contains the parsed document and toc collects the heading entries — neither includes any frontmatter content.

Type-safe access

frontmatter is typed as Record<string, unknown>. Narrow the values before use. A lightweight approach with typeof guards:

const { frontmatter } = parseMarkdown(source)

const title   = typeof frontmatter.title   === "string"  ? frontmatter.title   : "Untitled"
const draft   = typeof frontmatter.draft   === "boolean" ? frontmatter.draft   : false
const tags    = Array.isArray(frontmatter.tags)          ? frontmatter.tags as string[] : []

For projects with many content fields, a schema validator like Zod keeps the contract explicit:

import { z } from "zod"
import { parseMarkdown } from "@domphy/markdown"

const PageSchema = z.object({
  title:       z.string(),
  description: z.string().optional(),
  published:   z.coerce.date().optional(),
  draft:       z.boolean().default(false),
  tags:        z.array(z.string()).default([]),
})

function loadPage(source: string) {
  const { frontmatter, body, toc } = parseMarkdown(source)
  const meta = PageSchema.parse(frontmatter)
  return { meta, body, toc }
}

Building a page index

A common pattern for documentation sites: parse all source files up front, extract metadata into a page index, then render on demand.

import { ElementNode } from "@domphy/core"
import { parseMarkdown } from "@domphy/markdown"

// Assume these are loaded from the file system at build time.
const sources: Record<string, string> = {
  "intro.md":  introSource,
  "guide.md":  guideSource,
}

const pages = Object.entries(sources).map(([file, source]) => {
  const { frontmatter, body, toc } = parseMarkdown(source)
  return {
    slug:        file.replace(/\.md$/, ""),
    title:       String(frontmatter.title ?? ""),
    description: String(frontmatter.description ?? ""),
    draft:       frontmatter.draft === true,
    body,
    toc,
  }
})

const published = pages.filter((page) => !page.draft)

function renderPage(slug: string): string {
  const page = published.find((p) => p.slug === slug)
  if (!page) return new ElementNode({ p: "Not found" }).generateHTML()
  return new ElementNode({ article: page.body }).generateHTML()
}

Using splitFrontmatter in a custom pipeline

When you bring your own markdown-it instance (via tokensToDomphy), call splitFrontmatter manually to separate the frontmatter before parsing the tokens:

import MarkdownIt from "markdown-it"
import { splitFrontmatter, tokensToDomphy } from "@domphy/markdown"

const md = new MarkdownIt({ html: true, linkify: true })

function parse(source: string) {
  const { frontmatter, content } = splitFrontmatter(source)
  const tokens = md.parse(content, {})
  const { body, toc } = tokensToDomphy(tokens)
  return { frontmatter, body, toc }
}

Supported YAML types

All standard YAML scalar types work:

---
title:    "Hello World"           # string
version:  3                       # number
draft:    false                   # boolean
date:     2024-06-01              # Date (parsed as JS Date object)
tags:
  - alpha
  - beta                          # array of strings
author:
  name:   Jane Doe                # nested object
  email:  jane@example.com
---

Frontmatter must be a YAML mapping (key-value pairs) at the top level. A top-level list or scalar is parsed by the YAML library but silently discarded — frontmatter returns {} in that case.

FrontmatterSplit type

The return type of splitFrontmatter is exported:

import type { FrontmatterSplit } from "@domphy/markdown"

function stripHeaders(files: string[]): FrontmatterSplit[] {
  return files.map((source) => splitFrontmatter(source))
}