Importing an OpenAPI Spec

An OpenAPI spec becomes a set of API reference pages in four steps: leed settings pagetype create roots the set at a URL, leed import openapi stages it on disk for you to look at, leed site build renders it, and leed import apply 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. These replace the retired leed site create-page-type, leed site generate and leed site import.

The split between staging and applying is the point of the whole design. Nothing reaches the CMS until a human has built the set and looked at it, and apply enforces that with two guards rather than trusting you to remember. The same flow imports a directory of markdown with leed import markdown, or of HTML with leed import html.

flowchart TD
  A["leed settings pagetype create<br/>--type api --name --slug"] --> B["leed import openapi &lt;spec&gt;<br/>--page-type-id &lt;id&gt; --page-type-path &lt;slug&gt;"]
  B --> C["leed site build<br/>review the rendered pages"]
  C --> S["leed import status<br/>optional pre-flight"]
  S --> D{"Spec file unchanged<br/>since staging?"}
  C --> D
  D -- "no" --> B
  D -- "yes" --> E{"Recorded build newer<br/>than the staging run?"}
  E -- "no" --> C
  E -- "yes" --> F["leed import apply<br/>--page-type-id &lt;id&gt;"]
  F -- "pages failed, or menu<br/>not applied" --> F
  F -- "complete" --> G["Remove the pageTypeList.json entry<br/>Delete local staged files<br/>Delete the manifest"]
  B -. "changed your mind" .-> X["leed import clean"]

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 spec must end with one of .json, .yaml, .yml: <path>.

StepCommandRequiredTalks to the CMS?What it leaves behind
1leed settings pagetype create--type, --name, --slugYes — POST /api/pagetypesA page type in the CMS
2leed import openapithe spec, --page-type-id, and --page-type-path on the first runNo — entirely localStaged pages under src/<page type path>/, a temporary pageTypeList.json entry, and a manifest in your cache directory
3leed site buildnoneNoThe rendered site in raw-content/.build/site, and a recorded build timestamp
4leed import apply--page-type-idYes — needs page:createThe 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 settings pagetype create --type api --name "API 3.7" --slug docs/api/3.7.0

The nested slug 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. Creating an API or documentation page type also creates a search index for it, and the page type can be bound to an existing navigation menu; the import then merges into that bound menu.

--type takes documentation, api or posts. 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 new page type’s pageTypeId, which every later step needs. Under --json it is in data.

Nothing is registered in your checkout: src/_data/pageTypeList.json is written by the CMS when it publishes, and you may not commit it. So the first staging run needs the slug spelled out with --page-type-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 — stage locally

leed import openapi ./openapi.yaml --page-type-id pt_9f2c41 --page-type-path docs/api/3.7.0

This 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.

	Staged 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 import apply --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 staged, from which file, and when. --dry-run reports all of that without writing any of it.

--page-type-path <slug> names where the pages are rooted, and an explicit value always wins. Without it the CLI falls back to the local pageTypeList.json entry — the publish pipeline’s copy, or a previous staging run’s injection — and with neither it stops:

Could not determine the page type slug.
  → Create it with `leed settings pagetype create --type <kind> --name <name> --slug <slug>`, then pass `--page-type-path <slug>` on the first staging run.

--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.

KeyWhat it holds
pageTypeIdThe page type this manifest belongs to
generation.sourceWhere the set came from: kind (openapi, markdown or html), and for OpenAPI the spec’s absolute openapiPath, its SHA-256 specHash and any baseUrl
generation.pageTypePathThe slug the pages are rooted at
generation.generatedAtISO timestamp the build guard compares against
generation.pageTypeListThe pre-injection contents of pageTypeList.json, so the file can be put back exactly
generation.menuIdThe page type’s navigation menu, so apply merges into the menu the CMS already has
entriesPer-page upload status (imported, skipped, failed) and reason
specStagedIdsWhich spec files have already been staged in the CMS
menuAppliedWhether the menu reached the CMS

If XDG_CACHE_HOME is set, the files land directly under it — the leed path segment is dropped. A manifest the CLI cannot parse — including one written by a CLI from before leed import — is treated as “nothing staged”, and you stage again.

Step 3 — build and review

leed site build --serve

This is not a formality. apply refuses to run until a build has happened after the staging run, and the reason for the whole 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, and it satisfies the guard; so does a plain leed site build.

Before applying, leed import status --page-type-id pt_9f2c41 is a cheap pre-flight. It says whether the source is unchanged and a build has been recorded since staging, how many staged pages the CMS already holds, and what the CMS’s own pre-write pass makes of the set — every reason apply would refuse, without writing anything.

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 — apply

leed import apply --page-type-id pt_9f2c41

The CMS first validates the whole set in one pre-write pass — a structural problem is refused with a single 409 before anything is written. Then the pages are uploaded, the staged menu is merged into the page type’s bound navigation menu (items are only ever added, never removed), 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 wrote — new pages, and existing unpublished drafts whose body it replaced. Skipped is pages it already held and did not write. Failed is pages it rejected, and the run stops short of the cleanup when that count is non-zero. Page ids are deterministic — derived from the endpoint and scoped by page type — so re-applying the same spec addresses the same pages, and one spec can be imported into two API page types without collision. A page that already exists as an unpublished draft of this page type has its body replaced by the staged file, so editing a spec and re-importing lands; a published page, one with pending suggestions, or one in another page type or folder is left exactly as it is, and no title, slug, label or folder is ever rewritten. That is what makes re-running an interrupted apply safe.

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

apply does not upload the files staging 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 staging time.

The OpenAPI spec changed or is missing since you staged it.
  → Re-run `leed import openapi` and rebuild before applying.

Moving or renaming the file produces the same message — the manifest holds the absolute path it was staged from. The fix is always the same: stage again, then build again.

A build must have happened after staging

The second guard compares your last recorded successful build against the moment staging 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 apply 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 staged files and the injected pageTypeList.json entry — then tells you where the progress is tracked:

Some pages failed. Re-run 'leed import apply' with the same flags to retry — progress is tracked in:
	/home/you/.cache/leed/production/cmp_4a71/imports/pt_9f2c41.json

Run 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 import apply retries only the staging. To abandon a staged set instead, leed import clean --page-type-id pt_9f2c41 removes exactly what staging wrote and leaves the CMS untouched.

On a fully successful run the CLI cleans up after itself. The temporary src/_data/pageTypeList.json entry is removed, the staged menu entries are pruned from src/_data/menu.json, the local staged files are 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-staging the same page type clears any recorded upload progress, because a fresh staging run 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 every command here, 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.

ESC