---
title: "Snapshot source"
description: "Read content from stored, already-parsed data instead of raw files."
canonical_url: "https://content.comark.dev/sources/snapshot"
---
# Snapshot source

> Read content from stored, already-parsed data instead of raw files.

Every other source hands over **raw** file text that Comark parses. A snapshot source hands over
content that is **already parsed**, which is what lets an instance run where the original files are
unreachable: a browser application, or a serverless function whose filesystem was left behind at
build time.

Two factories cover the two situations:

::card-group
  :::card{icon="i-lucide-file-box" title="snapshot(snapshotLoader, manifestLoader?)" to="#snapshot"}
  The artifact **is** the origin. Nothing else reads content.
  :::

  :::card{icon="i-lucide-layers" title="withSnapshot(source, snapshotLoader, manifestLoader?)" to="#withsnapshot"}
  The wrapped source stays the authority; the artifact only accelerates cold starts.
  :::
::

## `snapshot()`{lang="ts"}

Use it when no raw source exists at runtime.

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

const content = comarkContent({
  source: snapshot('/api/content/snapshot/default.json'),
})

await content.navigation() // one fetch, fully hydrated
```

**Parameters:** [`snapshotLoader`](#parameters-snapshotloader),
[`manifestLoader?`](#parameters-manifestloader)

**Returns:** [`ParsedSource`](https://content.comark.dev/reference/types/content#parsedsource). Pass it to `comarkContent({ source })`, or into a named `sources` map.

## `withSnapshot()`{lang="ts"}

Use it when a real source still exists and the artifact is only a cold-start accelerator.

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

export const content = comarkContent({
  source: withSnapshot(
    fs('./content'),
    () => useStorage('assets:comark').getItem('default/snapshot.json'),
    () => useStorage('assets:comark').getItem('default/manifest.json'),
  ),
})
```

**Parameters:** [`source`](#parameters-source), [`snapshotLoader`](#parameters-snapshotloader),
[`manifestLoader?`](#parameters-manifestloader)

**Returns:** [`ParsedSource`](https://content.comark.dev/reference/types/content#parsedsource) that carries `source` as its
`origin`.

One configuration handles all three phases:

::steps{level="3"}
### Development

No artifact exists yet, so the loader returns nothing and the instance walks `fs('./content')`.
Watching and live edits work as usual.

### Build

`comark-content snapshot` walks the wrapped source and writes the artifacts. See the
[CLI reference](https://content.comark.dev/reference/cli#comark-content-snapshot).

### Production

The loader finds the artifact, so a cold start hydrates from it instead of walking the source.
::

::tip
Keep the generated directory out of version control. If a stale local artifact exists, development
hydrates from it instead of reading your edits.
::

## Parameters

Both factories take the same two loaders; `withSnapshot()` adds the raw source in front of them:

| Parameter                                      | Type             | Default     | Description                                                     |
| ---------------------------------------------- | ---------------- | ----------- | --------------------------------------------------------------- |
| [`source`](#parameters-source)                 | `Source`         | -           | The raw source that stays the authority. `withSnapshot()` only. |
| [`snapshotLoader`](#parameters-snapshotloader) | `SnapshotLoader` | -           | Where the stored snapshot comes from.                           |
| [`manifestLoader`](#parameters-manifestloader) | `SnapshotLoader` | `undefined` | The light tier: the index, without bodies.                      |

### `source`

The wrapped source stays the authority for [`refresh()`](https://content.comark.dev/reference/content/refresh),
[`watch()`](https://content.comark.dev/reference/content/watch), `get(key, { fresh: true })`, media bytes, and the
`comark-content snapshot` command.

Ordinary reads prefer the artifact: a body the snapshot holds is served without touching the source,
whether or not the source is reachable and whether or not a manifest loader is passed. The source is
read only for a body the snapshot can't supply — a document added or changed at runtime whose cached
body has expired. See [which bodies hydrate](https://content.comark.dev/advanced/artifacts-and-hydration#which-bodies-hydrate).

### `snapshotLoader`

A string is fetched for you. Anything else works too: the data itself, a promise, or a function
returning either.

```ts [loaders.ts]
snapshot(() => useStorage('snapshots').get('content.json')) // storage read
snapshot(() => import('../.content/default/snapshot.json')) // bundled module
snapshot(storedSnapshotData) // a value you already hold
```

Loaders also accept the checksummed artifact form served by
[`content.handler()`](https://content.comark.dev/reference/content/handler), which is what makes
`snapshot('/api/content/snapshot/default.json')` work.

A function loader receives the ref the instance is pinned to, if any (see [`content.withRef()`](https://content.comark.dev/reference/content/with-ref)). Ignore it to serve the build artifact for every ref: an artifact built on a pinned instance carries its own ref, so at another ref it supplies bodies only, by `meta.hash`. Use the argument when you store one artifact per ref.

::note
A loader returning `null` or `undefined` means "nothing stored yet". The instance logs that and falls back to its origin when it has one, instead of failing.
::

### `manifestLoader`

Passing a second loader adds the light tier: cold starts restore the index only, and document
bodies hydrate on the first [`get()`](https://content.comark.dev/reference/content/get).

```ts [two-tier.ts]
comarkContent({
  source: snapshot(
    () => useStorage('snapshots').get('content.json'), // bodies
    () => useStorage('snapshots').get('manifest.json'), // index only
  ),
})
```

Reach for it when the snapshot grows large enough that downloading every body to render a
navigation becomes wasteful. With one loader, the snapshot alone is enough.

## Building the artifacts

Both factories consume what the instance itself produces, so there is no separate export format:

| Producer                                                                      | Output                                   |
| ----------------------------------------------------------------------------- | ---------------------------------------- |
| [`content.snapshot()`](https://content.comark.dev/reference/content/snapshot) | Every parsed file plus a freshness stamp |
| [`content.manifest()`](https://content.comark.dev/reference/content/manifest) | The index, without bodies                |

```ts [build.ts]
await useStorage('snapshots').set('content.json', await content.snapshot())
await useStorage('snapshots').set('manifest.json', await content.manifest())
```

The [`comark-content snapshot`](https://content.comark.dev/reference/cli#comark-content-snapshot) command and the
[`writeSnapshots()`](https://content.comark.dev/reference/cli#comark-content-snapshot-in-a-build-script) helper wrap the same producers for build steps.

## Custom parsed sources

The factories above are conveniences over one interface, so a CMS that stores Comark output can
serve content directly, with per-document reads instead of a bulk download:

```ts [cms.ts]
import type { ParsedSource } from 'comark-content'

const cms: ParsedSource = {
  parsed: true,
  manifest: () => $fetch('https://cms.acme.dev/comark/manifest'),
  get: (key) => $fetch(`https://cms.acme.dev/comark/files/${key}`),
  keys: () => $fetch('https://cms.acme.dev/comark/keys'),
}

const content = comarkContent({ source: cms })
```

| Member       | Description                                              |
| ------------ | -------------------------------------------------------- |
| `parsed`     | Required. Marks the source as providing parsed content.  |
| `snapshot()` | Every parsed file at once, plus an optional `time`.      |
| `manifest()` | The index only, for body-free cold starts.               |
| `get(key)`   | One parsed file, for lazy per-document reads.            |
| `keys()`     | Enumerate keys, so an instance can walk `get()`.         |
| `origin`     | A raw source behind this one (what `withSnapshot` sets). |
| `schema`     | Explicit `data` schema, as on a raw source.              |

Every provider is optional, but an instance needs one way to enumerate (`manifest`, `snapshot`,
`keys`, or `origin`) and one way to read bodies (`snapshot`, `get`, or `origin`).
`comarkContent()` throws at construction when neither is possible.

## Plugin compatibility

Parsed content is trusted as produced: the parser and the `file:parsed` transforms don't run again
on the consuming instance, because they already ran upstream. Plugins therefore belong to one side
or the other:

| Plugin kind                             | Runs on                                     | Examples                                                                     |
| --------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------- |
| Parsers and Markdown plugins            | The instance that **produces** the snapshot | `json()`, `yaml()`, `markdownFields()`, `markdown({ comark })`, highlighting |
| `file:parsed` transforms and validation | The producing instance                      | `schemaValidation()`                                                         |
| Index builders                          | The instance that **consumes** the snapshot | `sqliteFullTextSearch()`, `sqlQuery()`                                       |

Both mistakes fail quietly. A parser registered only on the consumer never runs, so its files are
missing. A search plugin registered only on the producer builds an index the consumer can't see.
With [`withSnapshot()`](#withsnapshot) the producing and consuming instance are the same object, so
this only matters when a build-time instance and a runtime instance are configured separately, as in
the [Vite guide](https://content.comark.dev/integrations/vite#which-plugins-go-where).

## Smaller bundles with `comark-content/runtime`

When your browser app reads pre-parsed content from a `snapshot()` source, import from
`comark-content/runtime` instead of the main package. This entry point excludes the parser pipeline
(Markdown, YAML, HTML parsing):

| Entry point              | Minified | Gzipped |
| ------------------------ | -------- | ------- |
| `comark-content`         | ~295 KB  | ~113 KB |
| `comark-content/runtime` | ~46 KB   | ~15 KB  |

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

const content = comarkContent({
  source: snapshot('/api/content/snapshot.json'),
})
```

The runtime entry exports the same `comarkContent` function with identical types. Parsing methods
throw if called, but that never happens when your source is a snapshot.

::warning
Use `comark-content/runtime` only with pre-parsed content. If you parse raw files at runtime
(filesystem, GitHub, or HMR updates), use the full `comark-content` package.
::

### When to use each entry point

| Entry point              | Use case                                        |
| ------------------------ | ----------------------------------------------- |
| `comark-content`         | Server-side, build scripts, raw sources, HMR    |
| `comark-content/runtime` | Browser SPAs with `snapshot()` only             |
| `comark-content/client`  | HTTP client for server-rendered apps (smallest) |


## Sitemap

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