---
title: Frontmatter
description: A small frontmatter reader for `key: value` and inline lists. Not YAML, deliberately.
source: https://adamjurek.com/components/frontmatter
---

## Home


Frontmatter parses a Markdown file's frontmatter block for `key: value` lines
and inline `[a, b]` lists, nothing more. Reach for it to read a content
file's metadata without pulling in a YAML parser for a format that never
needs anchors, nesting, or block scalars.

<!-- ::start:showcase demo="frontmatter" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

TanStack Markdown hands back the frontmatter block as a raw string and stops
there, which is the right call for a Markdown parser, not a YAML one. This
covers only the subset content files in this repo actually use. It is
deliberately not YAML: no anchors, no nesting, no block scalars. If a file
ever needs those, the honest fix is a real YAML parser, not teaching this one
another feature at a time until it becomes a worse one.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/frontmatter.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/frontmatter.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Parsing |
| Files | `frontmatter.ts` |
| Dependencies | None |
| Tags | markdown, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { parseFrontmatter, readList, readString } from "@sushindustries/ui";

const raw = `title: "Shipping a changelog"
tags: [release, writing]`;

export function Example() {
	const meta = parseFrontmatter(raw);

	return (
		<dl>
			<dt>title</dt>
			<dd>{readString(meta, "title")}</dd>
			<dt>tags</dt>
			<dd>{readList(meta, "tags").join(", ")}</dd>
		</dl>
	);
}
```

## What you should see

There is nothing to look at - this is a parser, not a component. What you
should see is `title` reading "Shipping a changelog" and `tags` reading
"release, writing": quoted strings lose their quotes, and `[a, b]` becomes a
real array, both without a YAML library anywhere in the dependency tree.

## If nothing happens

A key with no value, or a line with no `:` at all, is silently skipped rather
than thrown - `parseFrontmatter` never errors on malformed input, it just
omits what it could not read. If a value comes back empty, check the raw
block does not still have its `---` fences on it: this function expects the
frontmatter content only, not the delimiters around it.


## Guides


## What it actually parses

```yaml
title: "Shipping a changelog"
draft: false
tags: [release, writing]
# a comment, ignored
```

`key: value` and inline `[a, b]` lists, both on one line. Quotes around a
value (single or double) are stripped; everything else is read as a plain
string, including `false` and `12` - there is no type coercion, so
`readString(meta, "draft")` gives back the string `"false"`, not a boolean.

## Why it stops there

This is deliberately not a YAML parser: no anchors, no nesting, no block
scalars.

```yaml
title: &ref Anchors
subtitle: *ref     # not read - this is a plain string "*ref"
nested:
  key: value       # not read - nesting is not walked
```

Content files in this repo only ever need flat keys and flat lists, and
`@tanstack/markdown` already stops handing back structure at the same
boundary. The honest fix for a file that needs more is a real YAML parser,
not teaching this one another feature at a time until it is a worse one.


## API


<!-- generated:api -->

## Signature

```ts
splitFrontmatter(raw: string): { frontmatter: string; body: string; }
```

The `---` block and everything after it, told apart. `parseFrontmatter` reads a block; this is what finds one. They are separate because a Markdown parser that reports frontmatter usually hands back the original source with it - so a caller that only parses renders the metadata as a paragraph of text at the top of the page. This existed five times in this repository, once per catalogue, and the copies had drifted: four matched `\r?\n` and the fifth did not, so a page saved with Windows line endings would have had no title, no summary, and its own frontmatter printed as prose. Nothing failed, nothing warned, and the only symptom was one page looking wrong. That is the whole argument for it living here. A regular expression copied five times is five chances to be right, and the one that is wrong is the one nobody reads.

```ts
parseFrontmatter(raw: string | undefined): Frontmatter
```

```ts
readString(frontmatter: Frontmatter, key: string, fallback = ""): string
```

Frontmatter values are `string | string[]`; most call sites want one string.

```ts
readList(frontmatter: Frontmatter, key: string): readonly string[]
```

<!-- /generated:api -->

## Notes

`parseFrontmatter(undefined)` returns `{}` rather than throwing, so a call
site does not need to guard against a missing frontmatter block before
reading from it.

`readString` and `readList` disagree on purpose about a value of the other
shape: `readString` on a list value returns `fallback`, and `readList` on a
string value wraps it in a one-element array rather than returning `[]`. That
asymmetry exists because a single tag written without brackets (`tags: solo`)
is common enough to be worth treating as a list of one, while a title that
came through as a list has no sensible string to fall back to.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="frontmatter" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { parseFrontmatter, readList, readString } from "@sushindustries/ui";

export function loadPost(raw: string) {
	const [, frontmatterBlock, body] = raw.split("---");
	const meta = parseFrontmatter(frontmatterBlock);

	return {
		title: readString(meta, "title", "Untitled"),
		tags: readList(meta, "tags"),
		draft: readString(meta, "draft") === "true",
		body: body?.trim() ?? "",
	};
}
```

## What this example is not

Not how the split should be done for real content - `split("---")` breaks on
a post whose body also contains a `---` rule. The site's own catalogue uses
the boundary `@tanstack/markdown` already finds while parsing, rather than
re-finding it with a string split.
