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 itTwo 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 uploadThat 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.
| Package | Version | Why it is there |
|---|---|---|
tailwindcss | 4.2.4 | The CSS engine the build compiles tailwind/site.config.css with |
@tailwindcss/forms | 0.5.11 | Form-control resets, used by the form partial |
@tailwindcss/typography | 0.5.19 | The prose classes long-form page bodies rely on |
husky | 9.1.7 | Runs 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.
| Task | Runs | When you would use it |
|---|---|---|
| Run Webserver | leed site build --serve | The everyday loop: watch and serve on port 8080. --serve implies a debug (unminified) build |
| Build | leed site build | A full production-shaped build, minified |
| Build (debug) | leed site build -d | A build you can read — no minification |
| Site Validation | leed site validate | Check your changes may be committed |
| Site Validation - Reset (Dry-Run) | leed site validate -r --dry-run | List what a reset would touch, changing nothing |
| Site Validation - Reset | leed site validate -r | Undo every change to a protected file |
| Commit Changes | leed site commit -m <message> | Prompts for the message, then commits |
| Push to Staging | leed site push | Publish the commits and trigger a preview build |
| Pull Latest from Staging | git pull | Pick up commits the CMS has written since you last pulled |
| Login | leed auth login --browser | Authenticate the CLI |
| Logout | leed auth logout | Drop stored credentials |
| Auth Status | leed auth status | Check 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 key | What it tracks | What happens when it drifts |
|---|---|---|
huskyVersion | The CLI version that last wrote .husky/ in the checkout | The 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.jsonFour consequences follow, and each one has caught somebody out:
- Files appear mid-build and vanish afterwards.
src/_data/eleventyComputed.js, the whole ofsrc/_includes/leed/,leed-*.hbstemplates andl.*.min.jsbundles 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 runningleed site init, not by pulling;.vscode/appears only if they runleed 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 uploadkeeps its resumable upload state inleed-assets.json, with a.lockfile and temporary copies beside it while it writes;leed importreads fromleed-import/and sets converted HTML originals aside as*.html.bak;leed mcp add --cms --scope projectwrites.mcp.json; and images staged for a sandbox move, a design import or a site migration wait insrc/static/images/tmp/until they are uploaded as assets. All of them are yours and local to your machine. .leed-validation.jsonis 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.jsonKeying 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.