Skip to content
Get started

zel.toml

Say once what every command would otherwise need told.

Optional. Every setting has a flag, and every flag has a default. zel.toml exists so a repository can state its answers once instead of every command carrying them.

What it is really for

Not saving keystrokes. The value is that the answers become reviewable.

zel deploy netlify docs --site acme-docs is a decision made at a terminal by whoever happened to be there. It is invisible to everyone else, it lives in one person's shell history, and when the site is deployed to the wrong project nobody can see why.

The same decision in a committed file is a diff. It gets reviewed, it is the same for everyone, it is what CI uses, and it answers "how do we ship the docs?" without anyone having to remember.

That is also why there is no key for a credential.

zel.toml
content = "docs/external"

[build]
target = "static"
out = "dist"
features = ["diagrams"]

[deploy]
provider = "netlify"
site = "acme-docs"

With that file, the whole workflow is three words:

terminal
zel check
zel dev
zel deploy

Where it is found

Searched upward from the current directory, stopping at the first one found or at a .git directory. So the file sits at the root of the repository and works from any subdirectory inside it.

Paths inside it are relative to the file, not to where you ran the command.

Precedence

  1. A command-line flag

    Always wins.

  2. An environment variable

    ZEL_CONTENT, ZEL_TARGET, ZEL_REGISTRY. For CI, where a flag would have to be threaded through a script.

  3. zel.toml

  4. The built-in default

Keys

Top level

contentThe content folder./content, then discovery
engineEngine image, when you mirror it internallyghcr.io/mylife-inc/zellij-base
engine_versionPin the enginelatest

Pinning engine_version is worth doing for anything that has to build the same way in a year.

latest is right while you are writing — you want fixes as they land. It is wrong in CI, where the property you want is that today's build and last month's build differ only by what you changed. An unpinned engine turns "the docs broke and nobody touched them" into a real sentence.

[build]

targetstatic or imagestatic
outOutput folder for staticdist
tagImage tag for image<site-name>:latest
featuresmaths, diagramsdetected from content
base_pathServe under a prefixnone
site_urlAbsolute originsiteUrl in site.yaml
platformImage architecturesthe host's

[deploy]

providernetlify, cloudflare, vercel, ghpages, s3, fly, railway
siteThe site or project on the provider
registryFor container providers

No credentials, ever. There is no key for a token and there will not be one.

The reasoning is not that secrets in files are theoretically bad. It is that this file exists to be committed — that is its whole purpose — and a format which can hold a secret will eventually hold one. Somebody adds it locally to make something work, forgets, and commits. Leaving the field out entirely is the only version of this that stays true.

Credentials come from the environment. See deploy credentials for which variables each provider reads.

[check]

strictWarnings are errorsfalse

A worked example

Documentation living inside a product repository, published to Netlify on every push to main:

zel.toml
content = "docs/external"
engine_version = "0.4"

[build]
target = "static"
features = ["diagrams"]

[deploy]
provider = "netlify"
site = "acme-docs"

[check]
strict = true
.github/workflows/docs.yml
      - run: zel check
      - run: zel deploy
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

The pipeline carries the secret and nothing else. Everything about what is built is in the repository, where it can be reviewed.

See also

  • zel ci — writes the pipeline above
  • zel doctor — prints the resolved configuration