---
title: "Nitro"
description: "Serve Markdown over HTTP from a standalone Nitro server with Comark Content."
canonical_url: "https://content.comark.dev/integrations/nitro"
---
# Nitro

> Serve Markdown over HTTP from a standalone Nitro server with Comark Content.

Nitro is the thinnest server home for a Content instance: one instance, one catch-all route, and every other route reads content in-process. At the end of this guide you have `/api/content/get/about` answering with a parsed page, and a route that returns page data by path.

You need a Nitro project. Create one with `npm create nitro@latest` if you're starting fresh. The imports below are for Nitro 3; on Nitro 2, `defineEventHandler` and `useStorage` are auto-imported and the event exposes `toWebRequest(event)` instead of `event.request`.

::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 [server/content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'

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

### Mount the handler

A catch-all route hands the web-standard `Request` to [`content.handler()`](https://content.comark.dev/reference/content/handler):

```ts [server/routes/api/content/[...path\\].ts]
import { defineHandler } from 'nitro'
import { content } from '../../../content'

export default defineHandler((event) => content.handler(event.request))
```

Start the server and read a page:

```bash [Terminal]
pnpm dev
curl http://localhost:3000/api/content/get/about
```

You get the page as JSON: its `path`, `data` with `title: "About"`, and `nodes`. That's the whole data surface: `GET /api/content/get/<path>`, `/api/content/list`, `/api/content/navigation`, plus any [custom sections](https://content.comark.dev/reference/content/add-serve-handler) you register. A front-end reads it with [`createContentClient()`](https://content.comark.dev/reference/client/create-content-client).

### Read content from other routes

Routes on the same server skip HTTP and call the instance directly:

```ts [server/routes/[...slug\\].ts]
import { defineHandler, HTTPError } from 'nitro'
import { content } from '../content'

export default defineHandler(async (event) => {
  const page = await content.get(event.path)
  if (!page) throw new HTTPError({ status: 404, message: 'Page not found' })

  return page
})
```

To return HTML instead of JSON, render `page` with [`@comark/html`](https://comark.dev/rendering/html) as the [Node guide](https://content.comark.dev/integrations/node#render-to-html) shows.

### See edits while you develop

The instance parses a file once and keeps the result in memory, so an edit isn't visible until you tell it. Start a watcher in development:

```ts [server/plugins/content-watch.ts]
import { definePlugin } from 'nitro'
import { content } from '../content'

export default definePlugin(async () => {
  if (import.meta.dev) {
    await content.watch()
  }
})
```

[Watch content changes](https://content.comark.dev/guide/watch) explains what the watcher does.
::

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

## Deploy

`nitro build` writes a self-contained server to `.output/`, and it doesn't include `content/`. You have two ways to ship the files, and one to ship parsed content:

- **Keep the folder next to the output** and start the server from the project root, so `fs('./content')` resolves. [Deploy with content files](https://content.comark.dev/deployment/with-content-files) has the checklist.
- **Bundle the folder as a server asset.** Nitro copies it into `.output/` and exposes it through `useStorage()`. Read it with the [key-value source](https://content.comark.dev/sources/unstorage) instead of `fs()`; in development it reads the same folder, so nothing branches on the environment:
  ```ts [nitro.config.ts]
  import { defineConfig } from 'nitro'

  export default defineConfig({
    serverAssets: [{ baseName: 'content', dir: 'content' }],
  })
  ```
  ```ts [server/content.ts]
  import { comarkContent } from 'comark-content'
  import unstorage from 'comark-content/sources/unstorage'
  import { useStorage } from 'nitro/storage'

  export const content = comarkContent({
    source: unstorage({ driver: useStorage('assets/content') }),
  })
  ```

  Server assets have no file watcher, so pair this with `cache: false` in development if you want edits reflected on every read, or keep `fs()` in dev and switch sources by environment.
- **Ship a snapshot** for serverless and edge presets where cold starts matter. [Deploy with a content snapshot](https://content.comark.dev/deployment/with-a-snapshot#nuxt) shows it with Nitro server assets; the same steps apply without Nuxt.

## Going further

::card-group{cols="2"}
  :::card{icon="i-unjs-nitro" title="Nitro example" to="https://content.comark.dev/examples/server/nitro"}
  The full runnable server: an unstorage source with schema validation and SSR.
  :::

  :::card{icon="i-lucide-plug" title="Client and handler" to="https://content.comark.dev/advanced/client-and-handler"}
  How the handler and client pair works, and how plugins extend it.
  :::
::


## Sitemap

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