Build a Markdown Docs Site or Blog 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:
---
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 highlights code blocks. The toc plugin builds a table of contents from the h2 to h6 headings:
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:
export default defineEventHandler(event => getPost(getRouterParam(event, 'slug')!))<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:
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:
---
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>:
<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, Next.js, Astro, and SvelteKit. For VitePress, the VitePress example 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 and custom code blocks.
FAQ
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.toc plugin, it is in tree.meta.toc. Render tree.meta.toc.links as a list of anchor links to the heading IDs.Next steps
Generative UI
Let a model write Comark component syntax such as ::flight-card and render real Vue, React, Svelte, or Angular components, with no code execution.
CMS and runtime content
Parse Markdown with components at runtime, store the JSON document, and render it in Vue, React, Svelte, Angular, or HTML with no build step.