Your Site Repository

The CMS holds your page content. This repository holds everything that renders it: layouts, partials, Tailwind CSS, static assets and the build script that ties them together. There is exactly one per site, it is called raw-content, and it lives at gitlab.com/leed-ai/customers/<environment>/<domain-slug>/raw-content. You get a copy of it by running leed site init — or leed setup, which runs it for you — and it clones the repository using a repository token the CMS mints for you on the spot.

If the words repository, branch, build and deployment do not yet mean something precise to you in Leed, Core Concepts defines them before this page assumes them.

The directory the CLI creates

leed site init does not create a bare checkout. It creates a small project directory around the repository:

<folder>/                          # the current directory, or --dir <path>
└── leed-ai/                       # <domain-slug> — slugify() of your site's domain
    └── raw-content/               # the git repository — everything below is inside it

Two details about that layout are worth internalizing early.

Nothing about the site is kept beside the repository. leed site init records the site — its company id, environment, domain and where its checkouts are — in the CLI’s own site registry, ~/.config/leed/sites.json (under $XDG_CONFIG_HOME/leed/ when that is set), not in a file in the folder. A checkout is recognized by its own identity.json, so leed site commands work from anywhere inside raw-content/, and from outside a checkout you name the site with --site. Moving or renaming the folder is fine: the next command run inside it updates the record. leed site list shows every site the registry knows.

There is no config file to download. leed site init --company <companyId or domain> -e <environment> signs in with your saved session, fetches the site’s details from the CMS, clones the repository and installs its tooling; nothing is guessed, so outside a site you name both. Running it again for a site already set up at that path is refused with Site configuration already complete!, pointing you at leed site build instead. A checkout made by an older CLI still has a leed.config.json in the folder above it: the first current CLI that meets the file imports it into the registry, once, and never reads it again. Getting to this point at all is Installing the Leed CLI.

Inside raw-content/

raw-content/
├── src/
│   ├── _layouts/          ✏️  one complete page shell per file — yours
│   ├── _includes/         ✏️  partials, composed into layouts — yours
│   │   └── leed/          🔒 Leed's own partials; copied in at build, deleted after
│   ├── _data/             🔒 generated from CMS state on every publish
│   ├── static/            ✏️  images, js, webfonts, site.webmanifest — mostly yours
│   ├── <page-type>/       🔒 one folder per page type with a slug, e.g. blog/ or docs/
│   ├── index.hbs          ✏️  your home page
│   ├── 404.hbs            ✏️  your not-found page
│   ├── robots.hbs         ✏️  renders /robots.txt
│   └── src.11tydata.json  🔒 company-level settings, written from the CMS
├── tailwind/
│   ├── site.config.css    ✏️  the Tailwind entry point — the one required file
│   ├── site/              ✏️  base, theme, components, utilities
│   ├── layouts/           ✏️  per-layout CSS
│   ├── partials/          ✏️  per-component CSS
│   └── docs/              ✏️  your documentation color theme, code theme, chrome
├── .ci/build.sh           🔒 the Cloudflare Builds entry point
├── identity.json          🔒 restored automatically if you change it
├── .gitignore             🔒 restored automatically if you change it
├── README.md              🔒 repository plumbing
└── .build/                ⏳ build output — gitignored, recreated every build

✏️ is yours to edit, 🔒 is owned by Leed or the CMS, ⏳ is generated and transient. The boundary is enforced, not advisory, and it is stricter than the tree suggests — the full rule set is on Editable and Read-Only Files.

src/

Everything Eleventy reads. _layouts/ holds complete page shells, one of which each page type is bound to; _includes/ holds the partials those layouts compose. Both are Handlebars, and the order in which Leed processes them matters — How Templates Work walks that through.

_data/ holds the JSON projections of CMS state (menus, page types, labels, users, forms, autolinks) that your templates read. src.11tydata.json holds company-level settings. Neither is editable, and both are mapped file by file on Global Site Data and the Data Cascade.

The per-page-type folders — one for every page type with a slug, such as blog/ once you add a blog — fill up with published pages as .md files. A new site has none: its only page type is Root, whose pages sit directly under src/. An internal page type never gets a folder, because nothing about it is ever written to the repository. The folders exist so your templates have real content to render against locally. They are locked, with one deliberate escape hatch for .hbs files.

The scaffold every new repository starts from ships three root .hbs pages: index.hbs, 404.hbs and robots.hbs. They are ordinary pages you own, and you can add more beside them.

tailwind/

All yours, unconditionally. tailwind/site.config.css is the only file the build requires; everything else under tailwind/ is whatever structure suits you. On leed.ai that is site/ for the base layer, layouts/ and partials/ for component CSS, and docs/ for the documentation brand layer. The compilation model, the entry point’s contract and the --site-* tokens are on Tailwind Build.

.ci/build.sh

The script Cloudflare Builds runs when a deployment fires. Reading it is the fastest way to understand what a real build actually does, in order:

# 1. Refuse to start without the two variables the build cannot work without.
missing=()
[ -z "${ENV_NAME:-}" ] && missing+=("ENV_NAME")
[ -z "${LEED_API_TOKEN:-}" ] && missing+=("LEED_API_TOKEN")

# 2. Install the CLI from public npm, with no token: production installs the
#    `release` tag, every other environment `staging`.
bun install -g @leed-ai/cli@$TAG wrangler

# 3. Install Font Awesome Free for this build, unless the repository has its own.
leed site init --font-awesome

# 4. Build the site, then upload the worker version and notify the CMS.
leed site build
leed site upload

That is the same leed site build you run locally — the build that produces your preview and your live site is not a different program from the one on your laptop. What happens on the other side of leed site upload is What Happens After You Push.

Repository plumbing

identity.json binds the checkout to your company: it is how every leed site command, on your machine and in CI, knows which site it is in. README.md is generated. bunfig.toml is reserved: it is on the validator’s rejection list, but it is not in the shipped scaffold and is not present in a working repository — treat it as a filename you may not introduce, not as a file Leed maintains.

.build/

The build output directory, always <raw-content>/.build, with the rendered site at .build/site. It is gitignored and recreated on every run.

There is exactly one output directory. Preview and public builds are not two folders — they are the same folder, produced by the same command, differing in the branch that triggered them, the worker they are uploaded to, and the variables baked into them. Building and serving locally is Local Development.

The two branches

staging — the working branch

leed site init leaves you on staging, and it is the only branch leed site commit and leed site push accept. On any other branch they refuse with:

You are only allowed on specific branches. Change back to staging!

Committing to staging and pushing it builds the preview site.

main — the live branch

main is what the public build is made from: leed site build reads the branch (WORKERS_CI_BRANCH in CI, the local branch otherwise) and treats anything that is not main as a preview build. No CLI command promotes anything to main. Promotion happens in the CMS, through the publication flow described in What Happens After You Push.

Locally you can force either mode with --preview or --public, which is how you check a production-shaped build without leaving staging.

Scratch branches

Create as many local branches as you like for your own work — nothing stops you. You simply have to be back on staging before you can commit, because the gate is on the branch name at commit time. The merge and rebase mechanics, including what leed site push does to your history, are on Git Workflow.

What the CLI installs into the repository

leed site init installs two things into the repository: git hooks and the build’s dev dependencies. It writes no editor files and no AI skills — the VS Code files come from a command of their own, and the skills from the Leed plugin for Claude. The CLI records which version installed the hooks, and every later leed site command compares that record against the CLI you are running and re-installs them when it has changed.

Husky hooks

Installed into .husky/: the three hooks pre-commit, pre-push and commit-msg, plus the scripts they call — valid-command.mjs, valid-commit-msg.mjs and colors.mjs.

The enforcement is blunt and worth knowing before it surprises you. valid-command.mjs asks git for leed.invoked, a config flag the CLI sets on its own git calls. If it is not true, the hook prints the command you should have run and exits 18:

	You cannot 'commit' directly, please use:

		leed site commit -m "update details"

So a bare git commit or git push in this repository fails, every time, by design. That exit code and the rest of the CLI’s failure vocabulary are cataloged on CLI Troubleshooting and Exit Codes.

Dev dependencies

Installed as devDependencies with bun install --dev. package.json is copied in only when the repository does not already have one, and it is gitignored either way — these packages exist to make your local build work, not to be committed.

PackageVersionWhy it is there
tailwindcss4.2.4The CSS engine the build compiles tailwind/site.config.css with
@tailwindcss/forms0.5.11Form-control resets, used by the form partial
@tailwindcss/typography0.5.19The prose classes long-form page bodies rely on
husky9.1.7Runs the git hooks above

VS Code workspace and tasks

Written only when you ask, by leed site vscode. Two artifacts: <domain-slug>.code-workspace sits beside raw-content/ and opens it directly, so git and every editable file are at the root of your editor rather than one folder down, and .vscode/tasks.json goes inside the repository and registers twelve tasks, reachable with Tasks: Run Task from the Command Palette. Both are overwritten each time you run it, so running it again restores Leed’s defaults; nothing else in the CLI touches them.

The VS Code task picker on the raw-content workspace, listing Build, Build (debug), Run Webserver, Site Validation, Site Validation - Reset and Site Validation - Reset (Dry-Run)
TaskRunsWhen you would use it
Run Webserverleed site build --serveThe everyday loop: watch and serve on port 8080. --serve implies a debug (unminified) build
Buildleed site buildA full production-shaped build, minified
Build (debug)leed site build -dA build you can read — no minification
Site Validationleed site validateCheck your changes may be committed
Site Validation - Reset (Dry-Run)leed site validate -r --dry-runList what a reset would touch, changing nothing
Site Validation - Resetleed site validate -rUndo every change to a protected file
Commit Changesleed site commit -m <message>Prompts for the message, then commits
Push to Stagingleed site pushPublish the commits and trigger a preview build
Pull Latest from Staginggit pullPick up commits the CMS has written since you last pulled
Loginleed auth login --browserAuthenticate the CLI
Logoutleed auth logoutDrop stored credentials
Auth Statusleed auth statusCheck which environment and identity you are signed in as

Skills for Claude

The CLI no longer installs skills into the repository. The site skills — site-builder (templates, CSS conventions, Handlebars and front matter), site-preview, site-publish, site-assets, site-settings and the rest — ship in the Leed plugin for Claude, which keeps them current on its own. An older CLI copied its skills into .claude/skills/ and recorded their names; the current CLI removes exactly those recorded copies, once, the first time it meets that site’s old configuration, so they cannot shadow the plugin’s newer versions. The rest of .claude/ is left alone.

The version marker

sites.json keyWhat it tracksWhat happens when it drifts
huskyVersionThe CLI version that last wrote .husky/ in the checkoutThe hooks are re-installed and the key is updated

The comparison runs on any leed site command run inside the checkout — upgrade the CLI, run leed site build, and your hooks are brought up to date before the build starts. leed self-update refreshes the dev dependencies and hooks of the site you run it in as well. You do not re-run leed site init to refresh them; init refuses a site that is already set up.

What is invisible in git status

The build writes real files into your working tree and then deletes them, and the tooling installs real files that are never yours to commit. All of it is gitignored, so git status stays quiet throughout:

The complete .gitignore
#
# Primary dirs we control
#
.claude
.vscode
.husky

#
# Builder files
#
eleventyComputed.js
src/_includes/leed
leed-*.hbs
leed-*.json
# leed asset upload state, its .lock and .tmp-<pid> copies (hold resumable upload URLs)
leed-assets*
# The documented default source directory for `leed import markdown|html` (#406).
# It is the user's own content, not site source: the import COPIES a cleaned set
# into src/<pageTypeSlug>/ and leaves this directory read-only.
leed-import/
# Originals `leed import html` set aside: each <name>.html becomes <name>.md and
# the original is renamed, so the site build does not pick it up. A derived
# artifact of a conversion the user ran locally — `leed import clean` reverses it.
# Suffixed on a real filename, so the glob is `*.html.bak`, not `.html.bak`.
*.html.bak
# Images a sandbox, /leed:design-import or site-migrate staged locally (#457). The
# sandbox move uploads each one with `leed asset upload` and rewrites its references,
# so an image staged here is never committed.
src/static/images/tmp/
src/static/js/l.*.js

#
# build artifacts / install files
#
.build
node_modules
package.json
package-lock.json
bun.lock

#
# Local AI agent configuration (leed mcp add)
#
.mcp.json

#
# Build result files
#
.leed-validation.json
buildResults.json

Four consequences follow, and each one has caught somebody out:

  • Files appear mid-build and vanish afterwards. src/_data/eleventyComputed.js, the whole of src/_includes/leed/, leed-*.hbs templates and l.*.min.js bundles are copied in before Eleventy runs and swept when the build finishes. If a build crashes, some of them are still on disk — that is normal, and the next successful build tidies up.
  • .husky/, .vscode/ and .claude/ are per machine, not content. A teammate who clones the repository gets the hooks by running leed site init, not by pulling; .vscode/ appears only if they run leed site vscode; and .claude/ holds whatever their own tools put there — the CLI no longer writes into it.
  • Some CLI commands leave local working files that are never committed. leed asset upload keeps its resumable upload state in leed-assets.json, with a .lock file and temporary copies beside it while it writes; leed import reads from leed-import/ and sets converted HTML originals aside as *.html.bak; leed mcp add --cms --scope project writes .mcp.json; and images staged for a sandbox move, a design import or a site migration wait in src/static/images/tmp/ until they are uploaded as assets. All of them are yours and local to your machine.
  • .leed-validation.json is a legacy entry. The validation tracking file no longer lives in the repository at all — see below. The line remains so older checkouts stay clean.

Where the tooling keeps its state

The validation tracking file and the CLI’s import manifests live outside the repository, under a per-company cache directory:

${XDG_CACHE_HOME:-~/.cache/leed}/<environment>/<companyId>/leed-validation.json

Keying by environment and company id matters because one person can belong to several companies on one machine; keeping the directory out of the git tree matters because a stray git add there would otherwise commit your local validation state. Both the environment and the company id come from the site’s entry in sites.json, and there is no fallback — run a command outside a site checkout and it refuses rather than guessing.

Note the path quirk: when XDG_CACHE_HOME is set, the CLI uses it directly and the leed segment is not appended. XDG_CACHE_HOME=/var/cache puts the file at /var/cache/<environment>/<companyId>/, not /var/cache/leed/....

Some of the files mapped above are locked more tightly than the tree markers suggest, and the lock changes shape the moment you create a page type — Editable and Read-Only Files has the exact rules.

ESC