Skip to content

Configuration

One shotlist.config.yaml in the project root. Every path in it resolves from its own directory, not from wherever the command was run.

Key Default What it sets
site.urlrequiredWhere the site is running
site.serveCommand that starts the site, when nothing answers at site.url
site.allow[]Hosts a shot may open besides this site's own and everything under it
site.viewport1280 × 800Browser size
site.scale2Device pixel ratio; 2 is Retina
site.themelightlight, dark or no-preference
site.reducedMotiontrueDisables animation before capture
site.readySelector waited for after each navigation
site.settle0Extra milliseconds to wait after ready
site.timeout15000How long a step or a query may take
paths.recipesscreenshots/recipesWhere recipes live
paths.macrosscreenshots/macrosWhere macros live
paths.datascreenshots/dataWhere data files live
paths.outscreenshots/outWhere images are written
image.formatpngpng, jpeg or webp
image.quality901100, for the two that are lossy
install{}Named destinations a recipe refers to by name
deny[]File or folder names this project will not have read, written or opened
finders{}Named query aliases
check.threshold0.002Fraction of pixels that may differ before a shot counts as changed
check.tolerance8How far one channel may move, out of 255, before a pixel differs

Starting the site

To have a run start the site itself, give site.serve the command that starts it. Without it, shotlist expects something to already be answering at site.url.

shotlist.config.yaml
site:
  url: http://localhost:3000
  serve: npm run dev

A server already up is used as it is. shotlist fetches site.url first and starts nothing when something answers, so this can stay in the config while you write recipes with the dev server open in another terminal. CI, where nothing is listening, is where it actually launches one.

What a run starts, it stops — including on Ctrl-C. The command runs in its own process group, because npm run dev is npm, which spawns the server: signalling only the process shotlist launched would leave the one holding the port.

shotlist.config.yaml
site:
  serve:
    command: npm run dev
    ready: 3000 # a http(s) URL, a port, or { log: <pattern> }
    cwd: apps/web # resolved from the config file's directory
    env: { PORT: '3000' }
    timeout: 30000

ready is what proves the server is up, and defaults to fetching site.url. A URL is ready on any response at all, including a 404: that something answered is the question, not what it said. A port is ready when it accepts a connection, and { log: … } when the pattern matches the server's output.

There is no shell. The command is run directly, so &&, |, > and a VAR=value prefix are refused rather than half-understood — environment goes under env:, and anything needing a shell goes in a script you run instead. A run of only source: file recipes never opens the site, and does not start one.

Image format

png keeps every pixel and is the default. webp is the one worth reaching for: a shot of this site's homepage is 87 KB as a PNG and 33 KB as a WebP. jpeg is lossy in the way that shows worst on what a screenshot of an interface is mostly made of, which is text.

shotlist.config.yaml
image:
  format: webp
  quality: 90

A recipe may set its own format and quality. quality runs from 1 to 100 and does nothing to a PNG, which has no detail to trade away. The file is named for the format — .png, .webp, and .jpg for jpeg — and --check compares against a committed file of the same name.

There is no AVIF. The only encoder here is the browser doing the drawing, and Chromium reads AVIF but will not write it — it answers a request for one with a PNG rather than an error, so every conversion checks what it got back.

Style

Every drawing constant is configurable, and a recipe may override any of them under its own style: key. Sizes are in image pixels, so they do not change when scale does. These are the defaults.

shotlist.config.yaml
style:
  color: '#DC2626' # boxes, arrows, discs
  canvas: '#FFFFFF' # fills space added around the image for labels
  box: { width: 6, radius: 10, pad: 8 }
  arrow: { shaft: 6, headHalf: 19, headLength: 38 }
  label:
    font: 'Arial, Helvetica, sans-serif'
    weight: 700
    size: 44
    fill: '#FFFFFF' # glyph fill
    stroke: '#DC2626' # outline around the glyphs; defaults to `color`
    strokeWidth: 6 # centred on the outline, so half is under the fill
    gap: 40 # distance from the box
    fontUrl: # optional stylesheet to load before drawing
  number:
    radius: 26
    size: 40
    fill: '#DC2626' # the disc; defaults to `color`
    text: '#FFFFFF' # the numeral
  mask:
    fill: '#94A3B8' # what a masked region is painted with

Fonts

Set the label typeface under style.label.font. It has to be resolvable by the browser doing the drawing, which by default means installed on the machine, and a family that is not installed falls back silently — every label renders in another typeface with nothing to say so. shotlist measures the rendered text to catch that and warns. It is a warning rather than an error: a fallback still produces an image, and which faces a machine has is not something a recipe can know.

To use a font that is not installed, give fontUrl a stylesheet. A http(s) or data: URL is fetched when the callouts are drawn, so that one needs network access at shoot time.

Anything else is a path, resolved from the config file's directory the way every other path is — for a font the project ships. It is read from disk and inlined, along with the font files it points at, because the page the callouts are drawn in is built in memory, and a browser gives such a page no file: subresource. An absolute file: URL works too.

shotlist.config.yaml
style:
  label:
    fontUrl: https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap
    font: 'Inter, Arial, sans-serif'
shotlist.config.yaml
style:
  label:
    fontUrl: fonts/inter.css # read from disk, fonts and all
    font: 'Inter'

strokeWidth is centred on the glyph outline, so half of it is painted under the fill. A 6-pixel stroke reads as a 3-pixel outline; tools that draw the stroke entirely outside need roughly double the number here to match.