---
title: "Artifacts and hydration"
description: "Parse once, store the result, hydrate anywhere."
canonical_url: "https://content.comark.dev/advanced/artifacts-and-hydration"
---
# Artifacts and hydration

> Parse once, store the result, hydrate anywhere.

Artifacts let one Content instance reuse work performed by another. Produce JSON from an initialized
instance, store or ship it, then use a [snapshot source](https://content.comark.dev/sources/snapshot) to hydrate a runtime that
can't reach the original files or needs a faster cold start.

This page is the contract behind [Deploy with a content snapshot](https://content.comark.dev/deployment/with-a-snapshot). Read
that recipe first if you want a serverless deployment working; come here when you need to know what
the artifacts contain, how two indexes are reconciled, or how they travel over HTTP.

Two producers cover it:

| Producer                                                                      | Returns           | Contains                                                                   |
| ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------- |
| [`content.manifest()`](https://content.comark.dev/reference/content/manifest) | `Manifest`        | The index: every entry's path, listing data, meta, and schemas. No bodies. |
| [`content.snapshot()`](https://content.comark.dev/reference/content/snapshot) | `{ items, time }` | Every parsed file, bodies included.                                        |

Both return **plain data**, so you can store them anywhere that holds JSON. A
[snapshot source](https://content.comark.dev/sources/snapshot) reads them back.

::note
With a shared or persistent [cache driver](https://content.comark.dev/advanced/caching#options-driver), an instance already
persists and restores its index and bodies on its own. Reach for stored data when there is no such
driver to restore from — a browser application, a bundled serverless function, or a first cold boot
you want to be fast.
::

## Produce artifacts

```ts [build.ts]
import { content } from './content'

await useStorage('snapshots').set('content.json', await content.snapshot())
await useStorage('snapshots').set('manifest.json', await content.manifest())
```

`snapshot()` reports the instance's current state rather than re-reading the source. In a build step,
prefer the [CLI](https://content.comark.dev/reference/cli#comark-content-snapshot), which rebuilds from the source of truth first so a warm
development instance cannot leak stale data into a build:

```bash [terminal]
npx comark-content snapshot
```

That writes one directory per instance:

```
.content/
  default/
    snapshot.json
    manifest.json
```

## Hydrate an instance

Point a [snapshot source](https://content.comark.dev/sources/snapshot) at the stored data. With one loader the snapshot alone
hydrates everything; add the manifest loader when you want cold starts to skip bodies.

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

export const content = comarkContent({
  source: snapshot(
    () => useStorage('snapshots').get('content.json'),
    () => useStorage('snapshots').get('manifest.json'),
  ),
})

const page = await content.get('/posts/hello')
```

When a raw source still exists at runtime, wrap it instead so it stays the authority and the stored
data only accelerates cold boots:

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

comarkContent({
  source: withSnapshot(fs('./content'), () => useStorage('snapshots').get('content.json')),
})
```

### Which index wins

An instance may find two indexes on a cold start: the one persisted in its cache, which runtime
[`update()`](https://content.comark.dev/reference/content/update), [`remove()`](https://content.comark.dev/reference/content/remove) and
[`refresh()`](https://content.comark.dev/reference/content/refresh) calls re-stamp, and the one its parsed source provides,
which a newer deployment may have rebuilt. The newer `time` wins, and ties keep the persisted one.

That is what lets a live source keep receiving updates without a redeploy while still booting fast
from the last build's data. The [GitHub warm start](https://content.comark.dev/sources/github#warm-start-from-a-build-time-seed)
walks through each scenario, and names the one assumption the comparison makes: the build machine and
the host [agree on the time](https://content.comark.dev/sources/github#warm-start-from-a-build-time-seed-which-index-wins-the-stamps-come-from-two-clocks).

### Which bodies hydrate

The index decides which documents exist; the snapshot only supplies bodies. When the persisted index
wins, the snapshot may be older than some of what the index describes, so each body is checked before
it enters the cache:

- A document the index doesn't know (removed at runtime) is never re-cached.
- A body whose [`meta.hash`](https://content.comark.dev/reference/types/content#contentfilemeta) matches its index entry is
cached. A different hash means the document changed since the build, and the seed body is refused.
- Artifacts built before `meta.hash` existed fall back to the timestamps: if the index is newer than
the snapshot, a seed body only fills an empty cache slot and never overwrites one. Rebuild your
snapshot to get the exact check.

Bodies the snapshot can't supply are read from the origin on the next `get()`. With no reachable
origin, that read returns `null` rather than an outdated body.

On an instance pinned with [`withRef()`](https://content.comark.dev/reference/content/with-ref), an artifact built on another pinned instance carries that
ref. When the refs differ, the artifact is never used as the index (a file removed since would still
be listed): the index is read from the source at the instance's ref, and the artifact only supplies
the bodies whose `meta.hash` still match. A new commit therefore costs one frontmatter walk plus a
full parse of the files that changed, nothing for the rest.

## Transport artifacts

Over HTTP the same data travels as a [`CacheArtifact`](https://content.comark.dev/reference/types/content#cacheartifact): the
JSON, gzipped and base64-encoded, with a checksum.

```ts
interface CacheArtifact {
  name: string // 'manifest', or the instance name
  data: string // JSON, optionally gzipped + base64
  checksum: string // SHA-256 of `data`
  size: number // bytes
  time?: number // when it was built
}
```

[`content.handler()`](https://content.comark.dev/reference/content/handler) serves them from `/manifest` and
`/snapshot/<name>`, which is why a loader can be a URL:

```ts [spa.ts]
comarkContent({ source: snapshot('/api/content/snapshot/default.json') })
```

Those responses are produced once per content change and served from the cache in between, so a warm
request costs a single cache read rather than re-serializing the corpus. Any mutation drops them.

Loaders accept both forms — plain data or a checksummed artifact — and verify the checksum before
installing anything. A tampered or truncated payload is rejected rather than served.

::note
`readArtifact<T>(artifact)` decodes one by hand, verifying its checksum. Plugin authors building
their own index on top of a snapshot need it; applications do not.
::

## Choose one or two tiers

Start with the snapshot loader alone. Add a manifest loader when downloading every parsed body delays
requests that only need `list()` or `navigation()`. Measure your own artifacts and runtime traffic;
document count alone doesn't predict the payload because body size and parsed node shape vary.

For a sense of scale, the repository's `pnpm bench` runs both tiers over generated corpora. With 2,000
markdown documents of about 1.7 KB each, the snapshot artifact is 4.5 MB and the manifest 630 KB. A
cold `list()` moves 630 KB with the manifest tier and 4.5 MB without; a cold single `get()` moves
both artifacts with the manifest tier (5.2 MB) and only the snapshot without (4.5 MB). Neither touches
the source. At the next commit on a pinned instance with 5% of files changed, 94% of bodies come from
the snapshot without a parse; the cost is one frontmatter walk of the source (2,001 reads, 3.5 MB)
plus one full parse per changed or added file.

## Lifecycle

```mermaid {height="320px" theme="zinc-light" theme-dark="zinc-dark"}
sequenceDiagram
  participant B as Build
  participant S as Storage
  participant C as Runtime
  B->>S: manifest.json (content.manifest())
  B->>S: snapshot.json (content.snapshot())
  Note over C: Cold start: init()
  C->>S: read the manifest
  S-->>C: verify + install the index
  Note over C: First read: content.get('/posts/hello')
  C->>S: read the snapshot
  S-->>C: verify + install the bodies
  Note over C: Later reads are served from the cache
```

A document body is looked up in this order: the cache, then the snapshot, then the wrapped source.
The snapshot comes before the source because it's the accepted version: it costs no network and no
parse, the source may be absent in production, and a live source may already be ahead of the index.
Adding a manifest loader changes what's loaded and when, never which content a read returns. Pass
`fresh: true` to [`get()`](https://content.comark.dev/reference/content/get#fresh) when you want the source read regardless.

An instance fetches the snapshot once on the first body read, and once more only when a body the
snapshot is known to hold has left the cache (a TTL or an eviction on a shared driver). A document
that was never in the snapshot can't trigger a reload. A loader that returns nothing is not treated
as a final answer, so a transient failure stays retryable rather than disabling hydration.

For framework wiring, see the [Vite + Vue](https://content.comark.dev/examples/vite/vue) and [Nuxt](https://content.comark.dev/examples/framework/nuxt)
examples.

## Where this is used

The deployment shapes built on these artifacts, each written out end to end:

::card-group
  :::card{icon="i-simple-icons-vite" title="Vite" to="https://content.comark.dev/integrations/vite#create-the-browser-content-instance"}
  No server at runtime: the snapshot is the origin, and search runs client-side.
  :::

  :::card{icon="i-simple-icons-nuxtdotjs" title="Nuxt" to="https://content.comark.dev/deployment/with-a-snapshot#nuxt"}
  The files are left behind at build time; the bundle carries parsed content instead.
  :::

  :::card{icon="i-simple-icons-nextdotjs" title="Next.js" to="https://content.comark.dev/deployment/with-a-snapshot#nextjs"}
  The same shape, with `outputFileTracingIncludes` carrying the artifacts.
  :::

  :::card{icon="i-simple-icons-github" title="GitHub warm start" to="https://content.comark.dev/sources/github#warm-start-from-a-build-time-seed"}
  A live origin that stays authoritative, with a seed so cold boots skip the API.
  :::
::


## Sitemap

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