# Configuration

URL: /docs/configuration

The pro-visu.config file — settings (every field at its default), the managed server, assets, options merging, and the input graph.

Everything pro-visu owns lives in a `pro-visu/` folder at your repo root: `pro-visu init` writes
`pro-visu/pro-visu.config.ts` with two blocks — `settings` (repo-level CLI behaviour) and `assets`
(what to generate) — and output renders into `pro-visu/output/`.

```
pro-visu/
├─ pro-visu.config.ts   # the config
├─ config/              # optional: modules the config is split into
└─ output/              # generated assets + manifest.json (gitignored)
```

```ts title="pro-visu/pro-visu.config.ts"
import { defineConfig } from "pro-visu";

export default defineConfig({
  // repo-level behaviour — every field is optional (full reference below)
  settings: { outDir: "output" },
  // one entry per thing to generate
  assets: [
    { name: "home-reel", url: "https://your-site.com", generator: "scroll-reel" },
  ],
});
```

`defineConfig` is a typed identity helper — it gives you autocomplete and inline docs for every
field, but the config is validated again at runtime.

## File discovery

Without `--config <path>`, the CLI looks **inside the `pro-visu/` folder** for the config:

- `pro-visu.config.ts` · `.js` · `.mjs` · `.cjs` · `.json`
- `.pro-visurc` / `.pro-visurc.json`

An explicit `--config <path>` escapes this convention and is loaded as given (paths resolve
against the repo root). A `.ts` config that imports `defineConfig` requires the `pro-visu` package
to be resolvable from that folder; a `.json` config does not. A JSON config gets the same
autocomplete + validation from a generated `pro-visu.schema.json` — `pro-visu init --json`
writes it (next to the config) and points the config at it via `$schema`.

### Multiple configs

You can keep more than one config in the folder — e.g. a main showcase and a smaller docs set —
and run the extra ones by name: `pro-visu generate --config pro-visu/docs.config.ts`. Give each its
own `outDir` (a nested subfolder such as `output/showcase` and `output/docs` is tidiest) so they
don't share one `manifest.json` — each run writes and prunes its own manifest, so a shared output
dir would have the two configs clobbering each other's records.

## Settings

The `settings` block controls repo-level CLI behaviour. Fields are ordered the way you'll reach
for them: output & run behaviour, then the capture environment (browser + managed server), then
capture-mode settings and per-generator defaults.

Every field has a default, so `settings` can be omitted entirely. **Reference** is the interactive
view; **TypeScript** is the same shape in code, every field at its default.

**Reference**

**TypeScript**

```ts
settings: {
  // output & run behaviour
  enabled: true,                       // false = run none; "quick-test" = run only that group
  outDir: "output",
  concurrency: 1,
  logLevel: "info",
  cache: false,

  // capture environment
  browser: {
    headless: true,
    // channel: "chrome",              // use an installed browser instead of managed Chromium
    // executablePath: "/path/to/chrome",
    args: [],                          // e.g. ["--no-sandbox"] on CI
    launchTimeoutMs: 30000,
  },
  // server: {}, // managed server (build → `<pm> build`, start → `<pm> start`) — see below

  // capture-mode + generator defaults
  capture: {
    // signals into the site (the site must read one and render settled)
    signals: {
      // query: { capture: "1" },      // → ?capture=1
      // cookies: [{ name: "pv_capture", value: "1" }],   // SSR-readable; also carries auth
      // localStorage: { pvCapture: "1" },
      // initScript: "window.__PV_CAPTURE__ = true",
    },
    // cleanup applied by the tool (no site cooperation needed)
    cleanup: {
      hideSelectors: [],               // e.g. ["#cookie-banner", ".intercom-launcher"]
      clickSelectors: [],              // e.g. ["#onetrust-accept-btn-handler"]
      // injectCss: "…",
      hideScrollbars: true,
      pauseAnimations: false,
      freezeClock: false,
      blockTrackers: true,
      blockHosts: [],
      blockResourceTypes: [],
    },
  },

  defaults: {},                        // per-generator; see below
}
```

> Memory is managed automatically: heavy frame-stepped plans (real media walls) re-exec the CLI
> with a larger Node heap sized from your machine's RAM, and a watchdog stops a run gracefully
> before an out-of-memory crash.

### `capture`

A page that animates in — reveal-on-scroll, count-ups, scroll-snap — or shows a cookie banner or
chat widget captures as gaps, zeros, or the wrong frame. `capture` makes every URL capture clean
and settled, in two complementary halves:

- **`signals`** into the site (`query`, `cookies`, `localStorage`, `initScript`) deliver a "capture
  mode" flag — but the **site has to read it** and render accordingly. Four channels, so a site can
  use whichever fits its rendering model; `cookies` is the best fit for multi-route reels
  (SSR-readable, persisted across navigation).
- **`cleanup`** applied by the tool (`hideSelectors`, `freezeClock`, `blockTrackers`, …) needs no site
  cooperation — pro-visu suppresses the noise itself.

```ts
settings: {
  capture: {
    signals: { query: { capture: "1" } },      // site reads ?capture=1 and renders settled
    cleanup: {
      hideSelectors: ["#cookie-banner", ".intercom-launcher"],
      clickSelectors: ["#onetrust-accept-btn-handler"],
      freezeClock: true,
    },
  },
}
```

The site half — reading the signal and rendering settled (hide the header, show reveals, freeze
count-ups) — is a copy-paste recipe: [Capture mode: render a settled
page](/docs/recipes#capture-mode-render-a-settled-page).

Realtime recordings (the `interaction` generator) apply the same cleanup but keep media
playing — a live recording wants its motion.

> **Auth:** `cookies` isn't limited to capture-mode signals — a session cookie carries auth, letting
> captures reach login-gated pages. Keep the value in an env var, not the config; see
> [Troubleshooting](/docs/troubleshooting#capturing-pages-behind-a-login).

### `defaults`

Repo-wide defaults per generator. Keys are [generator ids](/docs/generators) — a key that matches
no generator is flagged at generate time; the value is an options object merged underneath each
asset's own `options` (**the asset wins**). Nested objects merge recursively, while arrays and
primitives replace wholesale — so a field set here can be omitted on every asset of that generator,
and each asset overrides only what differs.

```ts
settings: {
  defaults: {
    "scroll-reel": {
      output: {
        width: 1440,
        height: 900,
        fps: 30,
      },
    },
    "screenshots": { output: { format: "jpeg", quality: 90 } },
  },
}
```

## Managed server

When `settings.server` is set — **even as an empty `{}`** — `pro-visu generate` builds your site,
starts it, waits for it to respond, runs the capture, then shuts it down, so a project's npm
script can be just `pro-visu generate`. `build` and `command` default to your project's own
package scripts (`<pm> build` / `<pm> start`, detected from the lockfile), so you rarely set
either — pro-visu just follows along with whatever those scripts do. Change your `build`/`start`
scripts and pro-visu follows automatically.

```ts
import { defineConfig } from "pro-visu";

export default defineConfig({
  // build → `pnpm build`, start → `pnpm start` (or npm/yarn/bun) — no fields needed:
  settings: { server: {} },
  assets: [
    // url omitted → captures the server root; relative paths resolve against it
    { name: "home", generator: "scroll-reel" },
    { name: "pricing", url: "/pricing", generator: "scroll-reel" },
  ],
});
```

Override any field only when your setup differs — e.g. `command: "next start -p 4000"`, or
`build: false` to skip the build for an already-built or dev-server target:

```ts
settings: {
  server: {
    build: false,             // don't build — just start
    command: "pnpm dev",      // point at a dev server instead of a production start
  },
},
```

The managed server's URL is the **default base** for capture targets: a url-based asset that
omits `url` captures the server root, and any relative `url` (e.g. `/pricing`) resolves
against it. Absolute URLs pass through unchanged.

> Set a custom `port` (or `url`) only if your server can't read `PORT` from the environment, or
> you need a non-default port. Otherwise leave both off — the tool keeps the command, the
> readiness check, and your asset URLs in sync for you.

### Lifecycle and flags

A normal run is: **build → start → wait for ready → capture all assets → stop**.

The server is **skipped automatically when nothing in the selection needs a URL** — i.e. every
selected asset is a local generator (`wall`, `specimen`, `palette`, `palette-reel`). So a
[`wall` in `test` mode](/docs/generators/wall#test-mode-fast-preview), or `pro-visu generate <a
local asset>`, renders without paying for a site build/boot it never uses. (A real `wall` still
pulls in its URL-based tile producers, so the server starts for it.)

Two flags adjust the lifecycle explicitly:

- `--skip-server` — don't manage a server at all; capture an already-running site at the asset
  URLs. Pair this with a deployed URL or a dev server you started yourself. (Without a managed
  server there's no base URL, so assets need absolute `url`s.)
- `--skip-build` — keep the managed server but drop its `build` step, for fast iteration when
  the site itself is unchanged.

If a run is killed hard before teardown, the next `pro-visu generate` stops any
orphaned server process tree and cleans up temp directories.

## Assets

Each entry in `assets` describes one thing to generate.

**Reference** is the interactive view; **TypeScript** is the same shape in code.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` |  | Unique across the config — used in filenames and the manifest id. _(required)_ |
| `enabled` | `boolean \| string` | `true` | Run this asset? true includes it; false leaves it out without deleting or commenting it; a group string (e.g. "quick-test") tags it so settings.enabled set to the same string runs only that group. Dependencies of a running asset are pulled in regardless of their own enabled. |
| `generator` | `string` |  | One of the generator ids (/docs/generators). _(required)_ |
| `url` | `string` |  | Page to capture. Required by URL-based generators (scroll-reel, interaction, screenshots) unless a managed server is configured — then omitting it captures the server root, and a relative "/path" resolves against it. Local generators (wall, palette, palette-reel, specimen) take no url. |
| `options` | `object` |  | Generator-specific options, merged over settings.defaults for this generator (the asset wins). Validated by the target generator. |
| `capture` | `object` |  | Per-asset overrides of settings.capture, merged over it for this asset only (see "Per-asset capture overrides" below). Signals merge; cleanup arrays are additive with showSelectors/unblockHosts to subtract; booleans override. Omit to inherit the global capture settings. |

**TypeScript**

```ts
assets: [
  {
    name: "home-shots",
    url: "https://your-site.com",   // required by URL-based generators (relative resolves against a managed server)
    generator: "screenshots",
    options: {
      // generator-specific; see /docs/generators/<id>
      viewports: [
        {
          name: "desktop",
          width: 1440,
          height: 900,
        },
      ],
      elements: [{ selector: "header", name: "nav" }],
    },
  },
]
```

### Enabling, disabling & grouping assets

Every asset has an `enabled` field (default `true`). Set it to `false` to leave an asset out of the
run without deleting or commenting it out. Set it to a **group name** to tag the asset, then flip
`settings.enabled` to that same string to run only that group — a fast way to swap between quality
passes without touching each asset:

```ts
export default defineConfig({
  // Swap this one line to switch passes: true (everything), "quick", or "full".
  settings: { enabled: "quick" },
  assets: [
    { name: "hero-quick", generator: "scroll-reel", url: "/", enabled: "quick" },
    { name: "hero-full",  generator: "scroll-reel", url: "/", enabled: "full", options: { output: { fps: 60 } } },
    { name: "wip",        generator: "screenshots", url: "/pricing", enabled: false }, // never runs
  ],
})
```

- `settings.enabled: true` (default) runs every asset except those set to `false`.
- `settings.enabled: false` runs nothing.
- `settings.enabled: "quick"` runs only assets whose own `enabled` is `"quick"`.
- An explicit `--asset <name>` on the CLI overrides all of this and runs exactly what you name
  (even a disabled one). Dependencies of a running asset are always pulled in, whatever their own
  `enabled`.

`pro-visu doctor` prints the resolved plan and marks which assets will run under the current
`enabled` setting.

### Per-asset capture overrides

`settings.capture` applies to every URL capture, but one asset can override it via its own `capture`
block — the two are merged (global first, the asset on top) for that asset only. The classic case:
you hide the cookie banner globally, but want one hero shot that shows it off.

Cleanup **arrays are additive** — an asset's `hideSelectors` layer on top of the global ones rather
than replacing them — and two subtraction escapes remove inherited entries: **`showSelectors`**
un-hides globally-hidden elements, **`unblockHosts`** un-blocks globally-blocked hosts. Booleans
(`freezeClock`, `blockTrackers`, …) and `injectCss` override (CSS is appended); signal records
(`query`, `localStorage`) merge and `cookies` merge by name. Omit a key to inherit the global value.

```ts
export default defineConfig({
  settings: {
    capture: { cleanup: { hideSelectors: ["#cookie-banner", "#chat-widget"] } },
  },
  assets: [
    // Inherits the global — hides both.
    { name: "home", generator: "scroll-reel", url: "/" },
    // Show the cookie banner off in this one, but keep hiding the chat widget:
    {
      name: "consent-hero",
      generator: "screenshots",
      url: "/",
      capture: { cleanup: { showSelectors: ["#cookie-banner"] } },
    },
    // Let this reel animate live (the global froze the clock elsewhere):
    {
      name: "ticker",
      generator: "scroll-reel",
      url: "/pricing",
      capture: { cleanup: { freezeClock: false } },
    },
  ],
})
```

## The asset graph

Assets can depend on other assets — but you never author the dependency map. A generator
**derives** its dependencies from its own options: the [`wall`](/docs/generators/wall) treats
each asset name in its columns as a producer that must run first (and local files ride in as
`{ src }` tiles, no producer needed):

```ts
assets: [
  {
    name: "hero-shot",
    url: "/",
    generator: "screenshots",
    options: { fullPage: false },
  },
  {
    name: "wall",
    generator: "wall",
    // hero-shot runs first (named as a tile); the photo is used directly from disk
    options: { columns: [{ tiles: ["hero-shot", { src: "public/img/coat.jpg" }] }, /* …≥3… */] },
  },
]
```

This forms a DAG; cycles and references to unknown assets are rejected at load time.
Selecting an asset with `--asset` automatically pulls in its dependencies.

## Splitting the config

Nothing requires one monolithic file. As a showcase grows, split it into modules under
`pro-visu/config/` — settings in one file, each asset family in its own — and compose them in
`pro-visu/pro-visu.config.ts`, the way a Payload config imports its collections:

```ts title="pro-visu/config/settings.ts"
import type { ShowcaseSettingsInput } from "pro-visu";

export const settings: ShowcaseSettingsInput = {
  outDir: "output",
  server: {}, // build → `<pm> build`, start → `<pm> start`
};
```

```ts title="pro-visu/config/films.ts"
import type { AssetSpecInput } from "pro-visu";

export const films: AssetSpecInput[] = [
  {
    name: "home",
    generator: "scroll-reel",
    options: { motion: { autoSections: { durationMs: 14000 } } },
  },
  {
    name: "shop",
    url: "/shop",
    generator: "scroll-reel",
  },
];
```

```ts title="pro-visu/pro-visu.config.ts"
import { defineConfig } from "pro-visu";
import { settings } from "./config/settings";
import { films } from "./config/films";
import { stills } from "./config/stills";

export default defineConfig({ settings, assets: [...films, ...stills] });
```

Annotating each module with its input type keeps full type-checking and autocomplete on the
value. Every author-facing type is exported from `pro-visu`, including per-generator option types
(`ScrollReelOptions`, `WallOptions`, …) and their fragments (`WallColumnInput`,
`ChoreographyStepInput`, `PulseInput`, `PaletteColorInput`, …), so shared recipes and helpers
in those modules can be fully typed.
