---
title: "Node.js"
description: "Read, list, and render Markdown from a plain Node.js script with Comark Content."
canonical_url: "https://content.comark.dev/integrations/node"
---
# Node.js

> Read, list, and render Markdown from a plain Node.js script with Comark Content.

No framework, no server: a script imports the instance and reads content in-process. This is the shape for static site generators, build scripts, migrations, and feeding content to an LLM pipeline. At the end of this guide you read a page, list every page, and render one to HTML.

You need [Node.js](https://nodejs.org) 22.18 or later, which runs TypeScript files directly, and a project with `"type": "module"` in its `package.json`. If you followed the [Quick start](https://content.comark.dev/getting-started/installation), you have both.

::steps{level="3"}
### Install the package

  :::code-group
  ```bash [pnpm]
  pnpm add comark-content
  ```

  ```bash [npm]
  npm install comark-content
  ```

  ```bash [yarn]
  yarn add comark-content
  ```

  ```bash [bun]
  bun add comark-content
  ```
  :::

### Write a Markdown file

```md [content/about.md]
---
title: About
---

# About us

We write documentation for a living.
```

### Create the Content instance

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'

export const content = comarkContent({
  source: fs('./content'),
})
```

### Read your content

Nothing is read until you ask, so the script needs no setup step:

```ts [index.ts]
import { content } from './content.ts'

// One page, body parsed
const page = await content.get('/about')
console.log(page?.data.title) // About

// Every page's frontmatter, no body parsed
const pages = await content.list()
console.log(pages.map((page) => page.path)) // [ '/about' ]

// The navigation tree
const nav = await content.navigation()
console.log(nav.map((item) => item.title)) // [ 'About' ]
```

Run it from the project root:

```bash [Terminal]
node index.ts
```

Each run is a fresh process, so it reads your files as they are. Edit `content/about.md` and run again to see the change.

### Render to HTML

`page.nodes` is a parsed tree. `@comark/html` turns it into an HTML string, which is what a static generator or an email pipeline wants:

```bash [Terminal]
pnpm add @comark/html
```

```ts [render.ts]
import { renderHtmlFromDocument } from '@comark/html'
import { content } from './content.ts'

const page = await content.get('/about')
if (!page) throw new Error('No page at /about')

const html = await renderHtmlFromDocument(page)
console.log(html) // <h1>About us</h1><p>We write documentation for a living.</p>
```

Pass `components` to render Comark components with your own HTML, and add `@comark/html/plugins/highlight` for syntax highlighting; see the [HTML renderer](https://comark.dev/rendering/html).
::

You have a working setup. Everything below is optional; pick what you need.

## Generate a static site

Combine `list()` and the renderer to write one HTML file per page:

```ts [build.ts]
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { renderHtmlFromDocument } from '@comark/html'
import { content } from './content.ts'

for (const entry of await content.list()) {
  const page = await content.get(entry.path)
  if (!page) continue

  const html = await renderHtmlFromDocument(page)
  const file = join('dist', entry.path === '/' ? 'index.html' : `${entry.path}/index.html`)
  await mkdir(dirname(file), { recursive: true })
  await writeFile(file, `<!doctype html><title>${page.data.title}</title>${html}`)
}
```

## SQL queries and search

Back the [SQL query](https://content.comark.dev/plugins/built-in/sql-query) and [full-text search](https://content.comark.dev/plugins/built-in/full-text-search) plugins with the Node SQLite database:

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import sqlite from 'comark-content/database/sqlite-node'
import sqlQuery from 'comark-content/plugins/sql-query'
import sqliteFullTextSearch from 'comark-content/plugins/sqlite-full-text-search'

const database = sqlite()

export const content = comarkContent({
  source: fs('./content'),
  plugins: [sqlQuery({ database }), sqliteFullTextSearch({ database })],
})
```

```ts [index.ts]
const drafts = await content.query().where('data.draft', '=', true).all()
const hits = await content.search('content layer', { limit: 5 })
```

`node:sqlite` ships with Node 22.5 and later. See the [database reference](https://content.comark.dev/reference/database) for the file-backed and WASM variants.

## Generate types

Type `content.get()` and `content.query()` from your frontmatter:

```bash [Terminal]
npx comark-content prepare
```

Add `comark-content.d.ts` to `.gitignore` and to your `tsconfig.json` `include`. See [Add TypeScript types](https://content.comark.dev/guide/typescript).

## Going further

::card-group{cols="2"}
  :::card{icon="i-logos-nodejs-icon" title="Node example" to="https://content.comark.dev/examples/runtime/node"}
  The full runnable script: get, list, query, navigation, search, and media.
  :::

  :::card{icon="i-lucide-server" title="Serve it over HTTP" to="https://content.comark.dev/integrations/hono"}
  Put the same instance behind a Hono or Nitro route when a front-end needs it.
  :::
::


## Sitemap

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