An OpenAPI spec becomes a set of API reference pages in four commands: leed site create-page-type roots the set at a URL, leed site generate writes it to disk for you to look at, leed site build renders it, and leed site import hands the finished set to the CMS. You get one page per endpoint, a navigation menu for them, and a page type whose path carries the API version — so 3.7.0 and 4.0.0 can sit side by side and neither overwrites the other.
The split between generate and import is the point of the whole design. Nothing reaches the CMS until a human has built the set and looked at it, and the import command enforces that with two guards rather than trusting you to remember.
flowchart TD
A["leed site create-page-type<br/>--name --path --type"] --> B["leed site generate<br/>--openapi <spec> --page-type-id <id>"]
B --> C["leed site build<br/>review the rendered pages"]
C --> D{"Spec file unchanged<br/>since generate?"}
D -- "no" --> B
D -- "yes" --> E{"Recorded build newer<br/>than the generate?"}
E -- "no" --> C
E -- "yes" --> F["leed site import<br/>--page-type-id <id>"]
F -- "pages failed, or menu<br/>not applied" --> F
F -- "complete" --> G["Restore pageTypeList.json<br/>Delete local preview files<br/>Delete the manifest"]
Before you start
You need three things: an initialized site on this machine (see leed site init if you have not run the installer here), a signed-in session for the environment your leed.config.json names, and a spec file. Run every command from the site folder — the one holding leed.config.json — or from raw-content/.
The spec file must end in .json, .yaml or .yml. Anything else is rejected before the file is opened, with --openapi file must end with one of .json, .yaml, .yml: <path>.
| Step | Command | Required flags | Talks to the CMS? | What it leaves behind |
|---|---|---|---|---|
| 1 | leed site create-page-type | --name, --path | Yes — POST /api/pagetypes, needs pagetype:write | A page type in the CMS, and an entry in src/_data/pageTypeList.json |
| 2 | leed site generate | --openapi, --page-type-id | No — entirely local | Preview pages under src/<page type path>/, a temporary pageTypeList.json entry, and a manifest in your cache directory |
| 3 | leed site build | none | No | The rendered site in raw-content/.build/site, and a recorded build timestamp |
| 4 | leed site import | --page-type-id | Yes — needs page:create | The pages and menu in the CMS; on full success, nothing local at all |
Step 1 — create the page type
A page type is what roots the set at a URL and tells the CMS these are API pages rather than ordinary documentation.
leed site create-page-type --name "API 3.7" --path /docs/api/3.7.0/ --type apiLeading and trailing slashes are trimmed, so /docs/api/3.7.0/ is stored as docs/api/3.7.0. The nested path is what makes versioning work: put the version in the path, create a new page type for the next release, and the two sets never collide. Nothing in the code enforces that convention — it is a recommendation, and a good one.
--type accepts documentation, api and posts, and defaults to api. Use api for a spec import. documentation also works end to end. posts does not — the CMS bulk-import endpoint accepts documentation and API page types only, and answers a posts page type with 400 Bulk import is only supported for documentation/api page types, which you would not discover until step 4.
The command prints the identifier every later step needs:
Created page type: pt_9f2c41
name: API 3.7
slug: docs/api/3.7.0
pageTypeId: pt_9f2c41
Registered "pt_9f2c41" locally in:
/home/you/leed/example.com/raw-content/src/_data/pageTypeList.json
Next:
1. leed site generate --openapi <spec> --page-type-id pt_9f2c41
2. leed site build (review the output)
3. leed site import --page-type-id pt_9f2c41That local registration in src/_data/pageTypeList.json is what makes the rest of the workflow fat-finger-proof: generate reads the page type’s real slug from it instead of asking you to retype the path.
What an API page type changes about a page — its fields, its layout, the menu bound to it — is covered in page types and configuring a page type.
Step 2 — generate locally
leed site generate --openapi ./openapi.yaml --page-type-id pt_9f2c41This is a purely local command. It reads the spec, builds one page per endpoint, and writes them into your repository under src/<page type path>/ — src/docs/api/3.7.0/ for the example above. Nothing is uploaded, and no session is required.
Generated 42 page(s) for page type "pt_9f2c41".
Local files written under: /home/you/leed/example.com/raw-content/src/docs/api/3.7.0
Next steps:
1. Run `leed site build` and review the output.
2. Run `leed site import --page-type-id pt_9f2c41` to upload.Three things are written besides the pages themselves. A temporary entry is injected into src/_data/pageTypeList.json so the local build knows how to render the set. Generated menu entries are added to src/_data/menu.json. And a resumable manifest is written to your cache directory, recording what was generated, from which file, and when.
Two flags are optional and usually stay that way. --page-type-path <slug> names the path by hand — but the slug registered by create-page-type always wins, and passing a different value prints a warning saying so before the registered slug is used anyway. It is only load-bearing when no local registration exists, which is the one case that fails outright:
Could not determine the page type slug.
→ Run `leed site create-page-type` first (it registers the page type locally), or pass --page-type-path.--base-url <url> sets the base for links in the generated pages; it defaults to https://<your configured domain>/.
What the manifest file holds
The manifest lives at ~/.cache/leed/<environment>/<companyId>/imports/<pageTypeId>.json, deliberately outside the git tree so a stray git add cannot commit it, and keyed by company because one machine can be signed in to more than one.
| Key | What it holds |
|---|---|
pageTypeId | The page type this manifest belongs to |
generation.openapiPath | Absolute path of the spec that was generated from |
generation.specHash | SHA-256 of that file’s bytes at generation time |
generation.pageTypePath | The slug the pages are rooted at |
generation.baseUrl | --base-url, when one was given |
generation.generatedAt | ISO timestamp the build guard compares against |
generation.pageTypeList | The pre-injection contents of pageTypeList.json, so the file can be put back exactly |
entries | Per-page upload status (imported, skipped, failed) and reason |
specStagedIds | Which spec files have already been staged in the CMS |
menuApplied | Whether the generated menu reached the CMS |
If XDG_CACHE_HOME is set, the files land directly under it — the leed path segment is dropped. Note also that a manifest the CLI cannot parse is treated as “start over” rather than as an error, which is safe because the import endpoint only ever creates.
Step 3 — build and review
leed site build --serveThis is not a formality. The import command refuses to run until a build has happened after the generate, and the reason for the whole generate/import split is that somebody looks at real rendered output before it becomes CMS content. --serve puts it on http://localhost:8080 so you can click through it; a plain leed site build satisfies the guard equally.
Each generated endpoint page carries a request-snippet tab strip bound to the reserved api-languages tab group — cURL, TypeScript, Python, Go. The group is emitted by the generator, not written by you, and the reader’s choice of language follows them across every endpoint page in the set. Leave it alone: the group’s roster is repaired automatically under Settings → General, and the page editor filters it out of the Tab Group menu because it is reserved.
Step 4 — import
leed site import --page-type-id pt_9f2c41The pages are uploaded one at a time, the generated menu is applied to the page type’s left navigation, and finally the raw spec files are staged so they can be committed to the site repository when the set is next published.
Uploading 42 page(s) to https://app.leed.ai...
Import summary: 40 imported, 2 skipped, 0 failed (of 42).
Menu applied.
Staging OpenAPI spec files at https://app.leed.ai...
OpenAPI spec files staged — they will be committed to the repo when this page set is published.
Local preview files cleaned up — the CMS is now authoritative.Read the summary line as three separate counts. Imported is pages the CMS accepted as new. Skipped is pages it already held: the endpoint is create-only, so a page that already exists is acknowledged with Page already exists and never overwritten — which is what makes re-running an interrupted import safe, and also means you cannot use import to push an edit to a page the CMS already has. Failed is pages it rejected, and the run stops short of the cleanup when that count is non-zero.
Menu applied. and Menu not yet applied. are the two forms of the line beneath it. The second means the pages landed but the navigation did not, which is treated as an incomplete run.
The two guards, and why they exist
Both guards fire before a single page is uploaded, and both point backwards at a specific earlier step rather than telling you to start over.
The spec must not have changed
import does not upload files that generate left on disk. It re-derives the payloads from the spec file, which means an edited spec would silently upload something you never reviewed. So it hashes the file and compares it with the SHA-256 recorded at generation time.
The OpenAPI spec changed or is missing since you generated.
→ Re-run `leed site generate` and rebuild before importing.Moving or renaming the file produces the same message — the manifest holds the absolute path it was generated from. The fix is always the same: generate again, then build again.
A build must have happened after the generate
The second guard compares your last recorded successful build against the moment generate ran, and its message names which of the two cases you are in and prints both timestamps:
Build and review the generated set before importing. The last recorded build was
2026-09-02T09:14:03.118Z, but this set was generated later (2026-09-02T09:41:55.002Z).
→ Run `leed site build` (add --serve to review it in a browser), then re-run this command.
Note: --debug builds are not recorded, since their output skips minification.If no build has ever been recorded for the site, the first sentence reads “No successful build has been recorded for this site yet.” instead.
Re-running and recovering
A partial import is a resumable state, not a mess. When pages fail, or the menu does not apply, the command exits non-zero and leaves everything in place — the manifest, the generated preview files and the injected pageTypeList.json entry — then tells you where the progress is tracked:
Some pages failed. Re-run 'leed site import' with the same flags to retry — progress is tracked in:
/home/you/.cache/leed/production/cmp_4a71/imports/pt_9f2c41.jsonRun the same command again. Pages already acknowledged as imported or skipped are not re-sent, so a resume picks up where it stopped rather than starting over.
The spec-staging step at the end is the same: if it fails, the page import has already succeeded and is safe to leave alone, and re-running leed site import retries only the staging.
On a fully successful run the CLI cleans up after itself. src/_data/pageTypeList.json is restored from the backup captured at generation time, the generated menu entries are pruned from src/_data/menu.json, the local preview directory is deleted, and the manifest is removed. From that moment the CMS is authoritative and there is nothing local left to resume.
That cleanup matters more than it looks. src/_data/ is one of the folders leed site validate refuses to let you change, and this workflow writes into it twice — so the CLI takes responsibility for putting both files back rather than leaving you to explain them to the validator.
Re-running generate for the same page type clears any recorded upload progress, because a fresh generation invalidates it. It deliberately keeps the original pre-first-inject pageTypeList.json backup, so the eventual restore still puts the file back the way you found it rather than back to an already-injected state.
Publishing what you imported
Imported pages are ordinary CMS pages in draft. They do not appear on your site until they are published, and the navigation menu the import applied is a draft menu that needs publishing too. Both happen in the CMS — see publishing a documentation set for the order to do them in, and API reference pages for what the finished set looks like to a reader.
Flag-by-flag detail for all four commands, including their --json payloads, is on OpenAPI commands; the complete leed tree is indexed at CLI command reference. The manifest sits in your cache directory alongside the validation tracking file, both of which are cataloged in leed.config.json, files and environment.