Client & handler

Run the Content instance anywhere, serve it over HTTP.

The Comark Content instance can run on a server, at the edge, or entirely in the browser.

Runtimes

Comark is plain TypeScript with no Node-only core, so an instance runs anywhere JavaScript does. There are three common shapes:

RuntimeWhat runs thereHow you read it
Server / edgeA full Content instance (source, cache, plugins)Directly in-process, or exposed via content.handler()
Browser (client)A thin HTTP client, no parser or contentcreateContentClient() calls the server's handler
Browser (standalone)The whole Content, client-sideHydrate a snapshot with a sqlite-wasm database, no server

The rest of this page covers the common case: a server handler with a browser client.

One contract, two halves

The most common setup runs the Content instance on a server and reads it from the browser over HTTP:

  • content.handler(request) exposes the Content instance server-side. It's a single web-standard function — Request in, Response out — that dispatches the built-in sections (get, list, navigation) and any custom serve handlers. Because it's just Request/Response, it drops into Nuxt, Next.js, SvelteKit, Nitro, Hono, or any web-standard server.
  • createContentClient() consumes it from the browser with the same read API: same method names, same return shapes (get(), list(), navigation()).

The parser and your content stay on the server. Only the data crosses the wire.

The client and handler meet at one URL: the handler strips the Content instance's basePath (default /api/content) from the request pathname, and the client issues requests against its own basePath (same default). Keep the two in sync and everything else follows.

Mounting the handler and creating the client for your framework is covered step by step in the Quick Start and the integration guides.

Custom plugins

A custom capability is always a pair: a Content plugin that registers a serve handler, and a client plugin that calls it. The two line up one to one, so every section you add on the server becomes a typed method on the browser client:

  • Server with defineContentPluginctx.addServeHandler(name, handler), passed to comarkContent({ plugins }).
  • Client with defineContentClientPlugin → a method that fetches name, passed to createContentClient({ plugins }).
server/content.ts
import { comarkContent, defineContentPlugin } from 'comark-content'
import fs from 'comark-content/sources/fs'

// Register a custom `search-sections` section on the Content instance.
const searchSectionsPlugin = defineContentPlugin(() => ({
  setup(ctx) {
    ctx.addServeHandler('search-sections', async () => Response.json(await buildSearchSections(content)))
  },
}))

export const content = comarkContent({
  source: fs('./content'),
  plugins: [searchSectionsPlugin()],
})
client.ts
import { createContentClient, defineContentClientPlugin } from 'comark-content/client'
import { joinURL } from 'ufo'

// Call the matching section, fully typed.
const searchSectionsClient = defineContentClientPlugin(() => ({
  setup: ({ options }) => ({
    searchSections: () => options.fetch(joinURL(options.baseURL, options.basePath, 'search-sections')),
  }),
}))

const content = createContentClient({ plugins: [searchSectionsClient()] })
await content.searchSections()

The handler name on the server ('search-sections') and the method on the client (searchSections()) are the two halves of one contract. Reach for this instead of inventing bespoke routes: the built-in query and search plugins wire themselves up exactly this way.

For every client option (baseURL, basePath, fetch, plugins) and method, see the createContentClient reference.