---
title: "Document Model"
description: "Understand the serializable Markdown document returned by Comark, including its nodes, frontmatter, metadata and rendering workflow."
canonical_url: "https://comark.dev/getting-started/document-model"
---
# Document Model

> Understand the serializable Markdown document returned by Comark, including its nodes, frontmatter, metadata and rendering workflow.

`parseMarkdown()` returns a `MarkdownDocument`: a plain, serializable object containing parsed nodes, frontmatter, and plugin metadata.

```ts
import { parseMarkdown } from 'comark'

const document = await parseMarkdown(`---
title: Hello Comark
---

# Getting **started**`)

// {
//   frontmatter: {
//     title: 'Hello Comark'
//   },
//   meta: {},
//   nodes: [
//     ['h1', { id: 'getting-started' }, 'Getting ', ['strong', {}, 'started']]
//   ]
// }
```

The document is the shared boundary between parsing and rendering. You can parse once, serialize or cache the result, inspect or transform its nodes, and render it with HTML, ANSI, Vue, React, Svelte, or Angular.

## Document structure

The root object has three fields:

```ts
interface MarkdownDocument {
  frontmatter: Record<string, any>
  meta: Record<string, any>
  nodes: Node[]
}
```

| Field         | Description                                                                                     |
| ------------- | ----------------------------------------------------------------------------------------------- |
| `frontmatter` | Structured data from the leading YAML frontmatter block                                         |
| `meta`        | Data collected by [plugins](https://comark.dev/plugins), such as a table of contents or summary |
| `nodes`       | Parsed Markdown and component content                                                           |

### Frontmatter

Comark parses a leading YAML block into `document.frontmatter` (see [Frontmatter syntax](https://comark.dev/syntax/frontmatter)):

<code-group>
```mdc [Markdown]
---
title: Getting Started
draft: false
---

# Hello
```


```typescript [Result]
document.frontmatter
// { title: 'Getting Started', draft: false }
```
</code-group>

### Plugin metadata

Plugins store derived document data in `document.meta`. For example, the table-of-contents plugin adds `meta.toc`:

```typescript
import { parseMarkdown } from 'comark'
import toc from 'comark/plugins/toc'

const document = await parseMarkdown('# Guide\n\n## Install', {
  plugins: [toc()],
})

console.log(document.meta.toc)
```

Plugin types flow into `MarkdownDocument`, so metadata and frontmatter can stay strongly typed throughout your application.

## Node model

Comark uses compact tuples instead of an object for every syntax node. A `Node` is a text string, an element tuple, or a comment tuple:

```typescript
type Node = TextNode | ElementNode | CommentNode

type TextNode = string

type ElementNode = [string, ElementNodeAttributes, ...Node[]]

type CommentNode = [null, ElementNodeAttributes, string]
```

This format follows the shape of rendered markup: the tag is at index `0`, attributes are at index `1`, and child nodes start at index `2`.

### Text nodes

Text is stored directly as a string:

```json
"Hello, world!"
```

### Element nodes

Elements use `[tag, attributes, ...children]`:

<code-group>
```mdc [Markdown]
This is **important**.
```


```json [Nodes]
[
  "p",
  {},
  "This is ",
  ["strong", {}, "important"],
  "."
]
```
</code-group>

Markdown elements use familiar HTML tag names such as `h1`, `p`, `strong`, `a`, `ul`, and `li`. Comark components use their component name as the tag:

<code-group>
```mdc [Markdown]
::alert{type="warning"}
Check your configuration.
::
```


```json [Node]
[
  "alert",
  { "type": "warning" },
  "Check your configuration."
]
```
</code-group>

### Attributes

Element attributes are stored in a key-value object. The optional `$` field contains parser metadata used by renderers and tooling:

```typescript
interface ElementNodeAttributes {
  [key: string]: unknown
  $?: {
    line?: number
    html?: 0 | 1
    block?: 0 | 1
  }
}
```

Treat `$` as structural metadata. Component props and standard HTML attributes live alongside it.

### Comment nodes

Comments use `null` as their tag:

<code-group>
```mdc [Markdown]
<!-- Keep this note -->
```


```json [Node]
[null, {}, " Keep this note "]
```
</code-group>

## Parse once, render many

Every renderer accepts the same `MarkdownDocument`. Parsing once is useful for server rendering, build pipelines, content APIs, caches, and multiple output targets.

```typescript
import { parseMarkdown } from 'comark'
import { renderHtmlFromDocument } from '@comark/html'
import { renderAnsiFromDocument } from '@comark/ansi'

const document = await parseMarkdown(source)

const html = await renderHtmlFromDocument(document)
const ansi = await renderAnsiFromDocument(document)
```

Framework renderers expose a `MarkdownDocument` component for the same workflow:

<code-group>
```vue [Vue]
<MarkdownDocument :value="document" />
```


```tsx [React]
<MarkdownDocument value={document} />
```


```svelte [Svelte]
<MarkdownDocument value={document} />
```


```html [Angular]
<comark-markdown-document [value]="document" />
```
</code-group>

These components render a parsed document without importing or running the parser in the client bundle.

## Inspect documents

Use the utilities from `comark/utils` instead of writing traversal logic for common operations.

### Extract text

`textContent()` returns the plain text inside a node:

```typescript [text-content.ts]
import { textContent } from 'comark/utils'

const heading = document.nodes[0]
console.log(textContent(heading)) // Hello world
```

### Visit nodes

`visit()` walks every node that matches a checker. A visitor can inspect, replace, or remove matching nodes:

```typescript [external-links.ts]
import type { ElementNode, Node } from 'comark'
import { visit } from 'comark/utils'

const isExternalLink = (node: Node): node is ElementNode => {
  return Array.isArray(node) && node[0] === 'a' && String(node[1].href).startsWith('https://')
}

visit(document, isExternalLink, (node) => {
  return [node[0], { ...node[1], target: '_blank', rel: 'noopener' }, ...node.slice(2)]
})
```

The visitor mutates `document.nodes` when a node is replaced or removed. Create a copy first when your application requires immutable updates.

## Serialize and cache documents

A `MarkdownDocument` contains only plain arrays, objects, strings, and values contributed by plugins. You can send it over an API boundary or persist it as JSON:

```typescript
import type { MarkdownDocument } from 'comark'

const document = await parseMarkdown(source)

await cache.set(key, JSON.stringify(document))

const cachedDocument = JSON.parse(await cache.get(key)) as MarkdownDocument
```

Use the same Comark and plugin versions when reading persisted documents. Rebuild cached documents after an upgrade that changes node output or plugin metadata.

## Type documents

Pass metadata and frontmatter types to `MarkdownDocument` when documents cross application boundaries:

```typescript
interface ArticleMeta {
  readingTime: number
}

interface ArticleFrontmatter {
  title: string
  description?: string
}

type ArticleDocument = MarkdownDocument<ArticleMeta, ArticleFrontmatter>
```

Parser plugins can contribute these types automatically when you parse content directly.

Use “AST” when discussing the node representation itself. Use “document” for the root object returned by the parser and accepted by renderers.

```tsx
const document = await parseMarkdown(markdown)
return <MarkdownDocument value={document} />
```

---

- [Parsing API](https://comark.dev/reference/parse)
- [Rendering](https://comark.dev/rendering/html)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
