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 itTwo 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 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 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.
| 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
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.
| Task | Runs | When you would use it |
|---|---|---|
| Run Webserver | leed site build -d --serve | The everyday loop: debug build, watch, serve on port 8080 |
| 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 |
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 key | What it tracks | What happens when it drifts |
|---|---|---|
huskyVersion | The CLI version that wrote .husky/ | The hooks are re-installed and the key is updated |
vscodeVersion | The CLI version that wrote the workspace and .vscode/tasks.json | Both are rewritten and the key is updated |
aiConfiguration.claude.skillsVersion | The CLI version that wrote .claude/skills/ | The recorded skills are removed, the current set is copied in, the key is updated |
aiConfiguration.claude.installedSkills | The skill directory names the last install produced | Rewritten 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.jsonThree 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. .claude/,.vscode/and.husky/are tooling, not content. They are installed per machine. A teammate who clones the repository gets them by runningleed site init, not by pulling..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 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.