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)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.
enabledboolean | stringdefault trueWhich assets to run. true runs every asset that is not individually disabled; false runs none; a group string (e.g. "quick-test") runs only the assets whose own enabled matches it — a fast way to swap between quality passes. Explicit --asset selection on the CLI overrides this.
outDirstringdefault "output"Output directory, relative to the pro-visu/ config dir (so the default lands in pro-visu/output/). Holds generated assets + manifest.json.
concurrencynumberdefault 1How many assets to generate in parallel (shared browser, separate contexts). Raise it on a machine with RAM headroom for faster runs.
logLevel"silent" | "error" | "warn" | "info" | "debug"default "info"Log verbosity for generate and list. --verbose forces "debug" on generate; doctor always reports in full. (Render quality is an iteration choice — set it with --draft, not here.)
cachebooleandefault falseSkip assets whose inputs + options + tool fingerprint are unchanged. Opt-in; can be stale. Same as --cache.
browserobject
Playwright launch controls.
headlessbooleandefault trueRun headless. Set false to watch captures in a visible window.
channelstringUse an installed browser channel, e.g. "chrome" or "msedge", instead of the managed Chromium. When set, the managed Chromium is not required or installed.
executablePathstringAbsolute path to a browser executable (overrides channel and the managed Chromium).
argsstring[]default []Extra launch args, e.g. ["--no-sandbox"] on CI.
launchTimeoutMsnumberdefault 30000Browser launch timeout in ms.
serverobject
Optional managed server — the tool builds, starts, waits for, and stops your site (see below). Every field defaults, so `server: {}` works; build/command fall back to your project's package scripts.
commandstringdefault <pm> startCommand that starts the server, run via the shell. Defaults to your project's start script (e.g. "pnpm start"), detected from the lockfile — so you rarely set it. The tool sets PORT/HOST in its environment to the readiness port/host, so frameworks that honour PORT (Next, Vite, …) bind it automatically. An explicit flag (e.g. next start -p 4000) still wins.
buildstring | falsedefault <pm> buildOne-shot build run before starting. Defaults to your project's build script (e.g. "pnpm build"); set false to skip it (already-built or dev-server setups).
urlstringHealth-check URL polled until it responds. Defaults to http://127.0.0.1:<port>.
portnumberdefault 3101Port the readiness check probes (and derives url from), also passed to the command as PORT so it binds the same port.
cwdstringdefault repo rootWorking directory for build + command, relative to the repo root (where the CLI is run), not the pro-visu/ folder — so "next build" runs against your app.
readyTimeoutMsnumberdefault 120000Max time to wait for the server to become reachable.
reuseExistingbooleandefault trueIf a server is already reachable at the URL, use it as-is (don't start or stop one).
captureobject
Applied to every URL-based asset (scroll-reel, screenshots, interaction) and folded into the cache key — any asset can override it via its own `capture` block (see "Per-asset capture overrides"). Two halves: signals into the site, and cleanup the tool applies itself.
signalsobject
Capture-mode flags delivered into the site. The site must READ one and render settled (reveals shown, count-ups final).
queryRecord<string, string>Query params appended to every URL-based asset, e.g. { capture: "1" } → ?capture=1.
cookies{ name, value }[]Cookies set on every capture context before navigation (scoped to the asset's origin). SSR-readable and persisted across in-app navigation — the best fit for multi-route reels. Also carries auth (a session cookie) to reach login-gated pages.
localStorageRecord<string, string>Entries seeded per origin before the page's own scripts run.
initScriptstringJS run in every page before its own scripts, e.g. window.__PV_CAPTURE__ = true.
cleanupobject
Noise the tool removes itself — no site cooperation needed.
hideSelectorsstring[]default []Hide elements matching these CSS selectors before capture (cookie banners, chat widgets, …).
clickSelectorsstring[]default []Click these selectors once after load to dismiss overlays (consent dialogs). Best-effort.
injectCssstringExtra CSS injected before capture — e.g. hide a sticky promo bar.
hideScrollbarsbooleandefault trueHide scrollbars so they don't appear in captures.
pauseAnimationsbooleandefault falsePause CSS animations/transitions for fully static, deterministic frames.
freezeClockbooleandefault falseFreeze Date.now / performance.now / Math.random (seeded) so time/random content is stable.
blockTrackersbooleandefault trueAbort common analytics/ads/session-replay requests during capture (cleaner, faster).
blockHostsstring[]default []Extra hostname substrings to block during capture.
blockResourceTypesstring[]default []Playwright resource types to block, e.g. ["media", "font"].
defaultsobjectdefault {}Per-generator option defaults, keyed by generator id, merged beneath each asset's options. Nested objects merge recursively; arrays and primitives replace wholesale.
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:
signalsinto 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;cookiesis the best fit for multi-route reels (SSR-readable, persisted across navigation).cleanupapplied by the tool (hideSelectors,freezeClock,blockTrackers, …) needs no site cooperation — pro-visu suppresses the noise itself.
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.
Realtime recordings (the interaction generator) apply the same cleanup but keep media
playing — a live recording wants its motion.
Auth:
cookiesisn'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.
defaults
Repo-wide defaults per generator. Keys are generator ids — 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.
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.
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:
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(orurl) only if your server can't readPORTfrom 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, 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 absoluteurls.)--skip-build— keep the managed server but drop itsbuildstep, 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.
namestringrequiredUnique across the config — used in filenames and the manifest id.
enabledboolean | stringdefault trueRun 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.
generatorstringrequiredOne of the generator ids (/docs/generators).
urlstringPage 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.
optionsobjectGenerator-specific options, merged over settings.defaults for this generator (the asset wins). Validated by the target generator.
captureobjectPer-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.
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:
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 tofalse.settings.enabled: falseruns nothing.settings.enabled: "quick"runs only assets whose ownenabledis"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 ownenabled.
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.
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 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):
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:
import type { ShowcaseSettingsInput } from "pro-visu";
export const settings: ShowcaseSettingsInput = {
outDir: "output",
server: {}, // build → `<pm> build`, start → `<pm> start`
};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",
},
];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.