---
title: "How it works"
description: "What happens between a Markdown file and the page your app receives."
canonical_url: "https://content.comark.dev/advanced/how-it-works"
---
# How it works

> What happens between a Markdown file and the page your app receives.

You don't need this page to use Comark Content. It explains what the instance does when you call `get()`, `list()`, or `navigation()`, which helps when you tune performance, pick a deployment shape, or read the reference.

## What a read does

Start from the instance you create in every guide:

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'

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

Nothing happens at construction. The instance reads files the first time you ask for something:

- **`await content.get('/about')`** looks up the file for `/about`, reads it from the source, parses frontmatter and body with [Comark](https://comark.dev), stores the result in memory, and returns it. A second `get('/about')` returns the stored copy.
- **`await content.list()`** needs every page's frontmatter but no bodies. On the first call, the instance lists the source's files and reads the frontmatter of each one into an **index**. Later calls read from that index.
- **`await content.navigation()`** arranges the index into a tree by folder. It never parses a body.

Two ideas fall out of this. Frontmatter is read for every page and is cheap. Bodies are parsed one page at a time, on demand, and kept.

## The index

The index (called the **manifest** in the reference) holds one entry per file: its public path, its frontmatter, and its `meta`. It's what `list()`, `navigation()`, and `stat()` return from, and it's how `get()` knows which file a path belongs to.

Building it costs one frontmatter read per file. For a local folder that's fast. For a remote source such as GitHub, it's one request per file, which is why the [GitHub source](https://content.comark.dev/sources/github) caches raw bodies during that walk and why [snapshots](https://content.comark.dev/advanced/artifacts-and-hydration) exist to skip the walk on a cold start.

## Lazy bodies and the cache

A body is parsed the first time a page is `get()`, then written to the **cache**. The cache is on by default and lives in memory, so a second read in the same process skips the parse. It also stores the index, so a process that shares a persistent cache driver with another one can restore the index instead of walking the source.

You can change the driver, add a TTL, or turn the cache off. [Configure caching](https://content.comark.dev/advanced/caching) covers when each matters. For local development, the [watcher](https://content.comark.dev/guide/watch) invalidates cached pages as you edit them.

## The pipeline

```mermaid {height="182px" theme="zinc-light" theme-dark="zinc-dark"}
graph LR
  Source["Source"] --> Comark["Comark"]
  Comark -- "frontmatter" --> Index["Index"]
  Comark -- "body, on get()" --> Cache["Cache"]
  Index -- "list · navigation" --> API["API"]
  Cache -- "get" --> API
  API --> App["Your app"]
```

Only the source is required. Each other piece is built in with a sensible default or added as a plugin:

| Piece                                                         | Required | Role                                        | Examples                                                    |
| ------------------------------------------------------------- | -------- | ------------------------------------------- | ----------------------------------------------------------- |
| [**Source**](https://content.comark.dev/sources)              | Yes      | Where files come from                       | `fs()`, `github()`, `unstorageSource()`, `snapshot()`       |
| [**Cache**](https://content.comark.dev/advanced/caching)      | Built in | Where parsed pages and the index are kept   | In memory (default), any unstorage driver                   |
| [**Plugins**](https://content.comark.dev/plugins)             | Optional | Parsers, search, queries, media, validation | `yaml()`, `sqliteFullTextSearch()`, `sqlQuery()`, `media()` |
| [**Database**](https://content.comark.dev/reference/database) | Optional | Backs the search and query plugins          | `sqlite()`, `sqliteWasm()`                                  |

## Serving over HTTP

Everything above runs in the process that holds the instance. When browser code needs content, the server exposes the instance with [`content.handler()`](https://content.comark.dev/reference/content/handler), a function from `Request` to `Response`, and the browser calls it through [`createContentClient()`](https://content.comark.dev/reference/client/create-content-client). The client has the same `get()`, `list()`, and `navigation()` methods; only the data crosses the wire. [Client and handler](https://content.comark.dev/advanced/client-and-handler) explains the protocol.

## Snapshots

A snapshot is the index plus every parsed body, saved as JSON. An instance can read from a snapshot instead of files, which is how content ships to a serverless function that doesn't carry your `content/` folder, and how a Vite app runs the whole instance in the browser. [Deploy with a content snapshot](https://content.comark.dev/deployment/with-a-snapshot) is the recipe; [Artifacts and hydration](https://content.comark.dev/advanced/artifacts-and-hydration) is the contract.

## Several sources

One instance reads one source. When two bodies of content need different sources or settings, create two instances and compose them with [`contentHub()`](https://content.comark.dev/advanced/hub), which merges their indexes into one navigation tree and one handler.


## Sitemap

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