---
title: "Build a Markdown Docs Site or Blog in Nuxt, Next.js, Astro, or SvelteKit"
description: "Parse Markdown files with frontmatter, a table of contents, and highlighted code, then render them with your own components in Nuxt, Next.js, Astro, or SvelteKit."
canonical_url: "https://comark.dev/use-cases/docs-and-blogs"
---
# Build a Markdown Docs Site or Blog in Nuxt, Next.js, Astro, or SvelteKit

> Parse Markdown files with frontmatter, a table of contents, and highlighted code, then render them with your own components in Nuxt, Next.js, Astro, or SvelteKit.

Parse each Markdown file on the server or at build time with `parseMarkdown()`, then render the result with `<MarkdownDocument>`. The browser receives the rendered page, and it doesn't need the parser.

A Markdown file for a post holds YAML frontmatter, Markdown, and components:

```mdc [content/posts/hello-world.md]
---
title: Hello World
pubDate: 2026-10-07
---

## Getting started

::alert{type="info"}
Components render with your own design system.
::
```

## Parse with frontmatter, a table of contents, and code highlighting

One parse returns the content, the frontmatter, and plugin metadata. The [shiki plugin](https://comark.dev/plugins/built-in/shiki) highlights code blocks. The [toc plugin](https://comark.dev/plugins/built-in/toc) builds a table of contents from the `h2` to `h6` headings:

```typescript [lib/posts.ts]
import { readFile } from 'node:fs/promises'
import { createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'
import toc from 'comark/plugins/toc'

const parse = createMarkdownParser({ plugins: [shiki(), toc({ depth: 3 })] })

export async function getPost(slug: string) {
  const tree = await parse(await readFile(`content/posts/${slug}.md`, 'utf-8'))

  tree.frontmatter.title // 'Hello World'
  tree.meta.toc.links // [{ id: 'getting-started', text: 'Getting started', depth: 2 }]
  return tree
}
```

`createMarkdownParser()` sets up the parser and its plugins once. Reuse the returned function for every file.

## Render in your framework

Each framework calls `getPost()` from the previous section on the server. It then renders the document with `<MarkdownDocument>`.

### Nuxt

Move `getPost()` to `server/utils/posts.ts`, so server routes can use it. With the `@comark/nuxt` module, `<MarkdownDocument>` is auto-imported:

```typescript [server/api/posts/[slug\\].get.ts]
export default defineEventHandler(event => getPost(getRouterParam(event, 'slug')!))
```

```vue [app/pages/blog/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const { data: tree } = await useFetch(`/api/posts/${route.params.slug}`)
</script>

<template>
  <MarkdownDocument v-if="tree" :value="tree" />
</template>
```

### Next.js

A React Server Component parses and renders the post. No client JavaScript ships for the content:

```tsx [app/blog/[slug\\]/page.tsx]
import { MarkdownDocument } from '@comark/react'
import { getPost } from '@/lib/posts'
import Alert from '@/components/Alert'

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  return <MarkdownDocument value={await getPost(slug)} className="prose" components={{ Alert }} />
}
```

### Astro

Parse the body of a content collection entry, and render it with the React renderer. Astro renders it to HTML on the server:

```astro [src/pages/posts/[...id\\].astro]
---
import { parseMarkdown } from 'comark'
import { MarkdownDocument } from '@comark/react'
import Alert from '../../components/Alert'

const document = await parseMarkdown(Astro.props.post.body!)
---

<MarkdownDocument value={document} className="prose" components={{ Alert }} />
```

### SvelteKit

Parse in a server `load` function. The Svelte `<Markdown>` component doesn't render during SSR, so use `<MarkdownDocument>`:

```svelte [src/routes/blog/[slug\\]/+page.svelte]
<script lang="ts">
  import { MarkdownDocument } from '@comark/svelte'
  import Alert from '$lib/components/comark/Alert.svelte'

  let { data } = $props() // from +page.server.ts: { tree: await getPost(params.slug) }
</script>

<MarkdownDocument value={data.tree} components={{ alert: Alert }} />
```

Each framework has a complete example: [Nuxt](https://comark.dev/examples/frameworks/nuxt), [Next.js](https://comark.dev/examples/frameworks/nextjs), [Astro](https://comark.dev/examples/frameworks/astro), and [SvelteKit](https://comark.dev/examples/frameworks/sveltekit). For VitePress, the [VitePress example](https://comark.dev/examples/frameworks/vitepress) adds Comark component syntax to its markdown-it setup.

## Override Markdown elements

Map a native tag in `components` to change how it renders, for example `pre` for code blocks with a copy button. In Nuxt, place components such as `ProseH2.vue` in `~/components/prose`. See [overriding HTML elements](https://comark.dev/rendering/nuxt#overriding-html-elements) and [custom code blocks](https://comark.dev/kb/custom-code-block).

## FAQ

::accordion
  :::accordion-item{label="Do I need a content collection or a build plugin?"}
  No. `parseMarkdown()` takes a string, so the Markdown can come from files, a collection, or an API. The Astro example uses a content collection to load the files.
  :::

  :::accordion-item{label="Where is the table of contents?"}
  With the `toc` plugin, it is in `tree.meta.toc`. Render `tree.meta.toc.links` as a list of anchor links to the heading IDs.
  :::

  :::accordion-item{label="How does this compare to MDX?"}
  MDX compiles content to JSX at build time. Comark parses at runtime or build time into a JSON document. See [Comark vs MDX](https://comark.dev/compare/comark-vs-mdx).
  :::
::

## Next steps

- [Frontmatter syntax](https://comark.dev/syntax/frontmatter)
- [Component syntax](https://comark.dev/syntax/components)
- [Shiki plugin options](https://comark.dev/plugins/built-in/shiki)
- [Render Markdown from a CMS or database](https://comark.dev/use-cases/cms-runtime-content)

---

- [Examples](https://comark.dev/examples/frameworks/nuxt)
- [Parse API](https://comark.dev/reference/parse)


## Sitemap

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