Vite

Set up Comark Content in a Vite app with the official plugin.

The comark-content/vite plugin does the wiring for you: it serves content.handler() as a dev middleware, pushes content edits over HMR, generates source types, and emits static artifacts at build time so the same URLs keep working in production.

Install the packages

pnpm add comark-content @comark/vue

Add the plugin

Create the Content instance in vite.config.ts and pass it to the plugin:

vite.config.ts
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import comark from 'comark-content/vite'

const content = comarkContent({
  source: fs(fileURLToPath(new URL('./content', import.meta.url))),
})

export default defineConfig({
  plugins: [comark({ content })],
})

Everything is on by default:

OptionDefaultRole
servertrueDev middleware forwarding <prefix>/* to content.handler() (plus media files at their manifest path)
watchtrueWatches the source and forwards updates over HMR as comark:update / comark:remove events
prefix'/api/content'URL prefix the middleware is mounted under
prerendertrueEmits manifest.json, per-source snapshots, and media as static assets at build time
typestrueWrites comark-content.d.ts on dev start and build

Create the browser Content

A Vite SPA has no server at runtime, so instead of a thin HTTP client the app runs the whole Content instance in the browser, hydrated from the artifacts the plugin serves in dev and emits at build:

src/composables/useContent.ts
import { ref } from 'vue'
import { comarkContent, type ContentFile } from 'comark-content'

const PREFIX = '/api/content'

export const content = comarkContent({
  cache: {
    loadManifest: () => fetch(`${PREFIX}/manifest.json`).then((r) => (r.ok ? r.json() : null)),
    loadSnapshot: (source) => fetch(`${PREFIX}/snapshot/${source}.json`).then((r) => (r.ok ? r.json() : null)),
  },
})

// Bumps whenever an HMR event has patched the content.
export const version = ref(0)

if (import.meta.hot) {
  import.meta.hot.on('comark:update', async ({ source, file }: { source: string; file: ContentFile }) => {
    await content.update(source, file)
    version.value++
  })
  import.meta.hot.on('comark:remove', async ({ source, key }: { source: string; key: string }) => {
    await content.remove(source, key)
    version.value++
  })
}

The HMR handlers keep the in-browser manifest in sync while you edit content — no page reload.

Render a page

src/App.vue
<script setup lang="ts">
import { MarkdownDocument } from '@comark/vue'
import { content } from './composables/useContent'

const page = await content.get('/')
</script>

<template>
  <MarkdownDocument v-if="page" :value="page" />
</template>
Hydrating a browser-side Content from manifest.json and snapshots is covered in depth in Artifacts & Hydration.

Going further

Vite + Vue example

The full runnable app: content edits hot-reload in the browser.

Vite + React example

The same wiring with React and a useSyncExternalStore-based refresh.

Type Safety

How the generated comark-content.d.ts narrows get() and query().

Database in the browser

Add SQL queries and full-text search client-side with sqlite-wasm.