---
title: "Deploy with a content snapshot"
description: "Parse your content at build time and let a serverless function or edge runtime serve the result without the files."
canonical_url: "https://content.comark.dev/deployment/with-a-snapshot"
---
# Deploy with a content snapshot

> Parse your content at build time and let a serverless function or edge runtime serve the result without the files.

Serverless platforms bundle the code your routes import and leave the rest behind. Your `content/` folder is read at runtime, not imported, so a deployed function has nothing to read. A **snapshot** solves that: parse everything during the build, ship the parsed result as a file the bundler does carry, and read it back at runtime.

Development doesn't change. The same configuration keeps reading and [watching](https://content.comark.dev/guide/watch) real files locally, and uses the snapshot only where it exists.

## How it fits together

Three pieces, in order:

1. **`comark-content snapshot`** runs before your framework's build. It reads your source and writes, per instance, the parsed content and its index:
   ```text
   .content/
     default/
       snapshot.json   every parsed page
       manifest.json   the index, without bodies
   ```
2. **Your framework's bundler** is told to carry `.content/` into the deployment. Each framework has its own switch for that; the sections below show them.
3. **[`withSnapshot()`](https://content.comark.dev/sources/snapshot#withsnapshot)** wraps your source. It takes the raw source plus one or two loader functions that read the files from wherever step 2 put them. A loader that returns `null` isn't an error: that's what happens in development, and the instance reads the raw source instead.

Add `.content/` to `.gitignore`. It's derived and rebuilt on every build.

::warning
Don't replace `withSnapshot(fs('./content'), …)` with a bare [`snapshot()`](https://content.comark.dev/sources/snapshot#snapshot). That drops the origin, and `comark-content snapshot` refuses to snapshot an instance whose only provider is a stored snapshot, because there would be nothing to rebuild from. Bare `snapshot()` is for runtimes that never have the files, such as a [browser app](https://content.comark.dev/integrations/vite).
::

## Nuxt

Nitro's [server assets](https://nitro.build/guide/assets#server-assets) bundle a folder into the server output and expose it through `useStorage()`.

::steps{level="3"}
### Write the snapshot during the build

```json [package.json]
{
  "scripts": {
    "build": "comark-content snapshot && nuxt build"
  }
}
```

### Bundle it as a server asset

```ts [nuxt.config.ts]
import { fileURLToPath } from 'node:url'

export default defineNuxtConfig({
  nitro: {
    serverAssets: [
      // Readable at runtime through useStorage('assets:comark').
      { baseName: 'comark', dir: fileURLToPath(new URL('./.content', import.meta.url)) },
    ],
  },
})
```

### Read it, keeping the filesystem as origin

```ts [server/utils/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(
    // The origin: walked in dev, and what `comark-content snapshot` reads.
    fs('./content'),
    // The bundled tiers. Absent in dev, so the origin is used instead.
    () =>
      import('nitropack/runtime').then(({ useStorage }) => useStorage('assets:comark').get('default/snapshot.json')),
    () => import('nitropack/runtime').then(({ useStorage }) => useStorage('assets:comark').get('default/manifest.json'))
  ),
})
```
::

Don't wrap this in an `import.meta.dev` ternary. `comark-content snapshot` loads the file outside Nuxt, where `import.meta.dev` is undefined, so the snapshot branch would win and you'd snapshot a snapshot. Keeping both tiers in one expression avoids the question.

## Next.js

Next traces the files your routes import and copies them into the deployment. Files read with `fs` at runtime have to be listed.

::steps{level="3"}
### Write the snapshot during the build

```json [package.json]
{
  "scripts": {
    "dev": "comark-content prepare && next dev",
    "build": "comark-content snapshot && next build"
  }
}
```

### Trace it into the deployment

```ts [next.config.ts]
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  outputFileTracingIncludes: {
    '/**/*': ['./.content/**/*'],
  },
}

export default nextConfig
```

### Read it, keeping the filesystem as origin

```ts [src/lib/content.ts]
import 'server-only'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import { withSnapshot } from 'comark-content/sources/snapshot'

async function artifact(file: string) {
  try {
    return JSON.parse(await readFile(join(process.cwd(), '.content', 'default', file), 'utf8'))
  } catch {
    return null
  }
}

export const content = comarkContent({
  source: withSnapshot(
    fs('./content'),
    () => artifact('snapshot.json'),
    () => artifact('manifest.json')
  ),
})
```
::

Server components and route handlers read through the same instance, so neither needs the content folder in production.

## Any Node runtime

Without a framework helper, read the files yourself and make sure your deployment includes `.content/`. The Next.js `artifact()` function above is the whole pattern: read the JSON, return `null` when the file is missing.

## Verify the cold start

The promise is that content serves without the content folder. Check it after `pnpm build`, never before: `comark-content snapshot` needs the source folder while it runs, and a missing folder at that point produces an empty snapshot where every route 404s.

::tabs{.gap-0}
  :::tab-item{label="Nuxt" icon="i-logos-nuxt-icon"}
  ```bash [Terminal]
  pnpm build
  mv content content-away
  trap 'mv content-away content' EXIT
  node .output/server/index.mjs
  ```

  The pages and `/api/content/**` must respond without reading `content/`. The shell trap restores the folder when you stop the server.
  :::

  :::tab-item{label="Next.js" icon="i-logos-nextjs-icon"}
  Set `output: 'standalone'` so the build produces a self-contained server, then:

  ```bash [Terminal]
  pnpm build
  cd .next/standalone/<path-to-your-app>
  test ! -d content
  test -f .content/default/snapshot.json
  node server.js
  ```

  Every page must answer from the bundled files.
  :::
::

Both cache settings work: the default in-memory cache, and `cache: false`, where every read comes from the snapshot.

## What a snapshot doesn't carry

A snapshot holds **parsed** content, not the original files. Two things still need the files:

- **Raw Markdown.** A "view source" panel or a raw Markdown route reads file text. Bundle `content/` too (a second `serverAssets` entry in Nuxt, `./content/**/*` in `outputFileTracingIncludes` for Next) and read it from there.
- **Media bytes.** Media entries appear in the snapshot, but their bytes don't. Bundle the files as above and serve them yourself, or keep an origin the [`media` plugin](https://content.comark.dev/plugins/built-in/media) can read at runtime.

Bundling `content/` also brings the filesystem origin back in production, which gives you [`refresh()`](https://content.comark.dev/reference/content/refresh) and [`get(path, { fresh: true })`](https://content.comark.dev/reference/content/get#options-fresh). Skip it when you want the deployment to carry parsed content only.

::note{to="https://content.comark.dev/sources/snapshot#plugin-compatibility"}
Parsers, transforms, and Markdown plugins run on the instance that *produces* the snapshot. Check plugin compatibility before moving a plugin between build and runtime.
::

## Going further

- The two loaders are two tiers: the manifest is small and serves `list()` and `navigation()`; the snapshot carries bodies for `get()`. [Artifacts and hydration](https://content.comark.dev/advanced/artifacts-and-hydration) explains when to pass one or both, how a runtime edit and a newer snapshot are reconciled, and the HTTP form the handler serves.
- Reading from GitHub and want faster cold starts? The same `withSnapshot()` wraps a [GitHub source](https://content.comark.dev/sources/github#warm-start-from-a-build-time-seed).
- Running in the browser with no server at all? The [Vite guide](https://content.comark.dev/integrations/vite) uses a bare `snapshot()` there.


## Sitemap

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