Four commands under leed site make up the OpenAPI import workflow: create-page-type roots a page set at a URL, generate writes it to disk, import uploads it, and list recovers a page type id you mislaid. This page is the flag-by-flag reference for all four. The order to run them in, the guards between them and what to do when one refuses are on importing an OpenAPI spec; read that first if this is your first import.
All four resolve a target environment and a session before doing anything, so all four must be run from your site folder (the one holding leed.config.json) or from raw-content/. Only generate works without a session.
| Command | HTTP call | Local file it writes |
|---|---|---|
leed site create-page-type | POST /api/pagetypes | src/_data/pageTypeList.json |
leed site generate | none — entirely local | src/<page type path>/, src/_data/pageTypeList.json, src/_data/menu.json, the import manifest |
leed site import | POST /api/page/import/docs, then POST /api/page/import/docs/spec | the manifest (updated, then deleted on full success) |
leed site list | GET /api/pagetypes | none |
leed site create-page-type
Creates a page type in the CMS and registers it in your repository so the later commands can find it by id alone.
leed site create-page-type --name "API 3.7" --path /docs/api/3.7.0/ --type api| Flag | Type | Required | Default | Valid values | What it does |
|---|---|---|---|---|---|
--name <name> | string | yes | — | any | Display name the CMS stores and shows in its page-type list |
--path <path> | string | yes | — | one or more /-separated segments | The path the page type is rooted at. Leading and trailing slashes are trimmed, so /docs/api/3.7.0/ is stored as docs/api/3.7.0. Nested segments are preserved, which is what makes a versioned set possible |
--type <type> | string | no | api | documentation, api, posts | The kind of page type to create |
An empty --path — --path / or --path " " — is refused before the request is sent, with --path must contain a non-empty slug (got "<value>").
The command prints the identifier every later step needs, the file it registered the page type in, and the three commands that follow:
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 entry carries the page type’s slug and the server-assigned navigation menu id. generate reads both from it, which is why you never have to retype the path and why the generated menu lands on the menu the CMS already has rather than a new one.
--type posts is accepted here and creates a page type, but the workflow cannot be completed with it — the CMS bulk-import endpoint answers a posts page type with 400 Bulk import is only supported for documentation/api page types at step four. What each kind changes about a page is on page types and configuring a page type.
leed site generate
Reads a spec and writes a reviewable page set into your repository. Nothing is uploaded and no session is required.
leed site generate --openapi ./openapi.yaml --page-type-id pt_9f2c41| Flag | Type | Required | Default | Valid values | What it does |
|---|---|---|---|---|---|
--openapi <path> | path | yes | — | a readable file ending .json, .yaml or .yml | The spec to generate from |
--page-type-id <id> | string | yes | — | a pageTypeId from create-page-type or list | Which page type the set belongs to |
--page-type-path <slug> | string | no | the registered page type’s slug | a /-separated slug | Names the path by hand. Only load-bearing when no local registration exists |
--base-url <url> | url | no | https://<your configured domain>/ | any absolute URL | Base for links in the generated pages. A trailing / is added if you omit one |
The extension check runs in the command’s pre-action, before the engine opens anything: a spec named api.txt fails with --openapi file must end with one of .json, .yaml, .yml: <path>, and a missing file or a directory fail with their own messages in the same place.
Everything the command writes happens after every guard has passed. In order: the spec is processed, the page set is composed in memory, every generated spec file is measured against the size ceiling, the set is checked for internal integrity, and only then are the preview files written, the pageTypeList.json entry injected and the manifest saved.
Re-running generate for the same page type clears any recorded upload progress (entries, specStagedIds and menuApplied are reset) because a fresh generation invalidates it, but it deliberately keeps the original pre-first-inject pageTypeList.json backup. Without that, a second generate would capture the already-injected file as the “original” and the eventual restore would be a no-op.
Each endpoint page the generator writes carries a request-snippet tab strip bound to the reserved api-languages tab group — cURL, TypeScript, Python, Go. It is emitted by the generator from documentationConfiguration.openAPI.snippetLanguages, repaired automatically, and filtered out of the page editor’s Tab Group menu. Read it, do not edit it; API reference pages shows what it looks like to a reader.
leed site import
Uploads a set that generate already produced and a build has already rendered.
leed site import --page-type-id pt_9f2c41| Flag | Type | Required | Default | Valid values | What it does |
|---|---|---|---|---|---|
--page-type-id <id> | string | yes | — | the id the set was generated for | Selects the manifest, and so the set, to upload |
There is no --openapi here and no --base-url. Both were recorded at generation time and are re-read from the manifest; the CLI accepts unknown options silently, so passing --base-url to import changes nothing at all rather than erroring.
Two guards run before the first request, in this order:
- The spec must be unchanged.
importre-derives its payloads from the spec file on disk rather than reading whatgeneratewrote, so a spec that moved or changed would upload something nobody reviewed. Its SHA-256 must still match the digest recorded at generation. It fails with “The OpenAPI spec changed or is missing since you generated.” - A recorded build must be newer than the generate. The message names which of the two cases you are in and prints both timestamps, and its hint says outright that
--debugbuilds are not recorded — the recording rule onleed site buildis the reason a reader who always builds with-dis told to build forever.
Only after both pass does the command revalidate your session and upload. It reports three counts and a menu line:
Uploading 42 page(s) to https://app.leed.ai...
Import summary: 40 imported, 2 skipped, 0 failed (of 42).
Menu applied.skipped is not a failure: the import endpoint is create-only, so a page the CMS already holds is acknowledged and never overwritten. That is what makes a resume safe, and it also means import cannot push an edit to an existing page.
Anything less than complete leaves the run resumable and exits non-zero. If pages failed, the message names the manifest to resume from; if only the menu is outstanding, the message is Menu not yet applied. The manifest, the preview files and the injected pageTypeList.json entry all stay in place, and re-running the same command picks up where it stopped.
On a fully successful run the CLI restores pageTypeList.json from the generation-time backup, prunes the generated entries from src/_data/menu.json, deletes the local preview directory and deletes the manifest. From that point the CMS is authoritative and there is nothing local left to resume.
leed site list
Lists the page types the CMS holds, filtered to one kind.
leed site list --type api| Flag | Type | Required | Default | Valid values | What it does |
|---|---|---|---|---|---|
--type <type> | string | no | api | documentation, api, posts | Which kind of page type to list |
The filter is applied on your machine, not by the server: the command fetches every page type and keeps the ones whose type matches. Page types stored before the kind was recorded have no type at all and therefore match nothing — if a page type you know exists is missing from every filter, that is why.
pageTypeId name slug
u05nxr Documentation docs
pt_9f2c41 API 3.7 docs/api/3.7.0An empty result is a success, not a failure: No page types of type "api" found.
Where the manifest lives
generate and import share one file per page type:
${XDG_CACHE_HOME:-~/.cache/leed}/<environment>/<companyId>/imports/<pageTypeId>.jsonIt sits in your cache directory rather than in the repository on purpose — raw-content/ is a git working tree, and a stray git add there would commit your import state. Keying by companyId matters because one machine can hold sessions for more than one company, and keying by environment keeps a staging import from resuming into production.
The file is deleted on a fully successful import. A manifest the CLI cannot parse is treated as “start over” rather than as an error, which is safe precisely because the import endpoint only ever creates. Note the path caveat: when XDG_CACHE_HOME is set the leed segment is dropped, so the files land directly beneath it. Every path the CLI reads or writes is cataloged on leed.config.json, files and environment.
JSON output
All four commands accept --json and answer with one envelope on stdout, as site.create-page-type, site.generate, site.import and site.list. Their data objects, generated from leed schema --json at CLI 4.106.0:
{
"site.create-page-type": { "pageTypeId": "…", "name": "…", "slug": "…", "type": "api", "pageTypeListPath": "…" },
"site.generate": { "pageTypeId": "…", "pageTypePath": "…", "pageCount": 42, "localPath": "…", "specPath": "…", "manifestPath": "…" },
"site.import": { "pageTypeId": "…", "cmsUrl": "…", "pageCount": 42, "imported": 40, "skipped": 2, "failed": 0, "menuApplied": true, "specsStaged": true, "cleanedUp": true },
"site.list": { "type": "api", "pageTypes": [{ "pageTypeId": "…", "name": "…", "slug": "…", "type": "api" }] }
}Failures here carry not_a_site_repo, not_authenticated, auth_expired, permission_denied, not_found, validation_failed, upload_failed or network_error. None of the four is destructive, so none of them ever refuses for confirmation. The envelope itself, and what to do with each code, are on machine-readable output (--json).
Every leed command and every global option is indexed at CLI command reference. All four commands here revalidate your session before their first request, which is why one of them can drop into a login mid-run — CLI authentication explains what that revalidation is doing. When one of them stops with a message you do not recognize, it is in the catalog on CLI troubleshooting and exit codes.