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, which clones it using a repository token the CMS mints for you on the spot and re-issues every time you run init again.

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:

<project>/                         # the folder you saved leed.config.json into
└── leed-ai/                       # <domain-slug> — slugify() of your site's domain
    ├── leed.config.json           # finalized config, written by `leed site init`
    ├── leed-ai.code-workspace     # VS Code workspace; opens raw-content directly
    └── raw-content/               # the git repository — everything below is inside it

Two details about that layout are worth internalizing early.

leed.config.json lives beside the repository, not inside it. That is deliberate: it carries your company id, your environment and the managed-file version markers, and keeping it one level up means a stray git add -A can never commit it and validation never has to defend it. The CLI looks for the file in the current directory and then in the parent, so leed site commands work from either the company folder or from inside raw-content/.

The config you download from the CMS is consumed, not kept. leed site init reads the file you downloaded, clones the repository, installs its tooling, then writes a finalized leed.config.json into the company folder and deletes the original. Running leed site init a second time against that finalized file is refused with <domain> site is already initialized.... Every field init writes, and the rules it uses to find the file in the first place, are documented on leed.config.json, Files and Environment; 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
│   ├── blog/              🔒 one folder per page type; published pages land here
│   ├── docs/              🔒 …and one for every other page type you create
│   ├── 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 colour 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 — blog/, docs/, and one for every page type you create in the CMS — fill up with published pages as .md files. They 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 three variables the build cannot work without.
missing=()
[ -z "${ENV_NAME:-}" ] && missing+=("ENV_NAME")
[ -z "${LEED_NPM_REGISTRY_TOKEN:-}" ] && missing+=("LEED_NPM_REGISTRY_TOKEN")
[ -z "${LEED_API_TOKEN:-}" ] && missing+=("LEED_API_TOKEN")

# 2. production installs the `release` npm tag; every other environment `staging`.
bun install -g --trust @leed/site-management@$TAG wrangler

# 3. 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 and is what lets the CLI run in direct mode inside CI, where there is no leed.config.json. 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 runs four installers. Each records a version in leed.config.json, and every later leed site command compares that record against the CLI you are running and re-installs whatever has drifted.

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

Two artifacts. <domain-slug>.code-workspace sits in the company folder and opens raw-content/ directly, so git, .claude/ and every editable file are at the root of your editor rather than one folder down. .vscode/tasks.json goes inside the repository and registers twelve tasks, reachable with Tasks: Run Task from the Command Palette.

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 -d --serveThe everyday loop: debug build, watch, serve on port 8080
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

Claude Code skills

Three skills are installed into .claude/skills/: site-builder (templates, CSS conventions, Handlebars and front matter), site-preview and site-publish. Between versions, the installer removes the skills the previous version recorded before copying the current set, so a renamed or retired skill does not linger.

The version markers

leed.config.json keyWhat it tracksWhat happens when it drifts
huskyVersionThe CLI version that wrote .husky/The hooks are re-installed and the key is updated
vscodeVersionThe CLI version that wrote the workspace and .vscode/tasks.jsonBoth are rewritten and the key is updated
aiConfiguration.claude.skillsVersionThe CLI version that wrote .claude/skills/The recorded skills are removed, the current set is copied in, the key is updated
aiConfiguration.claude.installedSkillsThe skill directory names the last install producedRewritten alongside skillsVersion; it is what makes clean removal possible

The comparison runs on any leed site command except init — upgrade the CLI, run leed site build, and your hooks, tasks and skills are brought up to date before the build starts. You do not re-run leed site init to refresh them; init refuses to run against an initialized config.

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
src/static/js/l.*.js

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

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

Three 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.
  • .claude/, .vscode/ and .husky/ are tooling, not content. They are installed per machine. A teammate who clones the repository gets them by running leed site init, not by pulling.
  • .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 leed.config.json, and there is no fallback — run a command outside an initialized site directory 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