Page Paths and Folder Structure

The folder a page’s file sits in is its URL, and the only thing that sets that folder is the documentation navigation menu. Everything else on this page follows from that one sentence.

This is not a naming convention you are being asked to respect. It is enforced in code, in both directions and after the fact: a client that supplies its own path has it discarded, a page whose stored folder no longer matches its place in the menu is moved to match, and the file in your repository is git-renamed to the new location on the next publish. When the menu and the file disagree, the menu wins — which means the correction you did not make is the one that lands.

The rule is identical on every plan. Nothing on this page is gated, and no upgrade changes any of it.

The formula

A page’s site URL and its file in the repository are the same string with a different wrapper:

URL   /docs/site-repository/page-paths-and-folder-structure/
file  src/docs/site-repository/page-paths-and-folder-structure.md

Read either one and you can write the other. Strip the leading src, add a trailing slash and swap .md for nothing to get the URL; do it backwards to get the file. Folders nest as deep as your menu does — this is a real two-level example from another Leed documentation site:

URL   /docs/build/auth-and-accounts/apple-sign-in/
file  src/docs/build/auth-and-accounts/apple-sign-in.md

The one special case is an empty slug. A page whose slug is blank publishes as index.md at its folder’s root, so it answers on the folder’s own address rather than a child of it: src/docs/index.md serves /docs/.

Where each piece comes from

The path is assembled from exactly three sources, joined in this order. Nothing else contributes a segment.

SegmentSourceSet whereChanging it moves
<pageTypeSlug> — always firstThe page type’s own slugSettings → Page TypesEvery page in the set
<folder>/… — zero or moreslugify() of each ancestor folder’s name in the docs left-nav menuThe documentation menu editorEvery page beneath that folder
<slug> — always lastThe page’s own slug, derived from its title when it is createdThe page’s settings in the editorThat one page

The field that surprises people is the middle row. A menu folder carries both a name and a title; the URL is built from name. title is the hover tooltip and has no effect on any address. Rename a folder’s tooltip and nothing moves; rename its name and every page under it does.

Slugification is the same routine that turns a page title into a slug: lowercased, transliterated, and every run of non-alphanumeric characters collapsed to a single hyphen. Site Repository becomes site-repository; AI and MCP becomes ai-and-mcp.

The shape a path may take

The stored folder path — the middle segments, without the page type slug and without the page’s own slug — is constrained by the database column that holds it:

/^[a-z0-9-]+(\/[a-z0-9-]+)*$/

Lowercase letters, digits and hyphens; segments separated by /; no leading and no trailing slash. The column’s own description spells out both ends of the mapping: “Docs folder ancestry: middle path segments prepended to slug for subfolder URLs, no leading/trailing slash (e.g. ‘configuration/linux’ → /docs/configuration/linux/my-page/). Null/empty = flat /{pageTypeSlug}/{slug}/”.

A folder name that slugifies to nothing at all — punctuation only, say — contributes no segment rather than an empty one.

The menu is the only writer

Three things look like they can set a page’s address. None of them can.

flowchart TD
    PT["Page type slug"] --> J
    MENU["Docs left-nav menu:<br/>each ancestor folder name,<br/>slugified"] --> J
    SLUG["Page slug"] --> J
    J{"joined, in this order"} --> URL["/docs/site-repository/page-paths-and-folder-structure/"]
    URL --> FILE["src/docs/site-repository/<br/>page-paths-and-folder-structure.md"]

    FM["'path' in front matter"] -.-> X1(["No such key exists"])
    CREATE["'path' on POST /api/page"] -.-> X2(["Ignored for a menu-bound docs page type"])
    UPDATE["'path' on PUT /api/page/:pageId"] -.-> X3(["Stripped by the request schema"])

Front matter cannot

There is no path key and no url key in a Leed page file. The front-matter block a publish writes is a fixed set of twenty-one keys, and a page’s address is not among them — it is derived from where the file was written, not declared inside it. The front matter reference lists the complete emitted set, along with the six keys a hand-authored page has to supply.

Adding a path key of your own does not fail; it is simply data. Anything the schema does not recognize is passed through to the template as a custom field, so "path": "guides/setup" in front matter gives your layout a {{ path }} variable and moves nothing.

The create call cannot

POST /api/page accepts a path, and for a documentation page type whose set is bound to a left-nav menu that value is ignored. The code says why in as many words: the menu is the source of truth for the folder path, “so the URL can never contradict the page’s actual menu placement.”

What you do get on create is a placement, not a path. The request takes a menuFolderId — the id of a folder in the docs menu — and the new page’s folder path is derived from that folder’s ancestry. Name a folder that does not exist, or one that is a page rather than a folder, and the page is created at the top level with a warning in the log; a menu problem never fails the creation.

The update call cannot

PUT /api/page/:pageId does not accept path at all. The field is omitted from the request schema deliberately, with a comment naming the failure it prevents: a payload path “would persist to the revision without staging a paths row, desyncing the published file location from the recorded URL.” The address and the file location move together or not at all.

What does — saving the menu

Saving the documentation left-nav menu is the single writer. Every save diffs the tree you submitted against the tree that was stored, recomputes the folder ancestry of every page whose page type binds that menu, and — for each page whose ancestry changed — stages the move.

CandidateWhat it looks like it doesWhat it actually does
path in front matterSets the page’s URLNothing — no such key is emitted or read; your value becomes an ordinary custom field
path on POST /api/pageFiles a new page under a folderIgnored for a menu-bound docs page type; pass menuFolderId instead
path on PUT /api/page/:pageIdMoves an existing pageRejected by the request schema before the handler sees it
Saving the docs left-nav menuReorders the navigationThe only writer. Recomputes every affected page’s folder path and stages the move

Renaming a folder is a bulk URL change

A folder’s name is one segment of every URL beneath it, so renaming a folder re-slugifies that segment and moves every page in it. A category with nine pages stages nine path changes from a single edit — and the menu editor shows you a renamed folder, not nine moved pages.

Two guards exist for exactly this reason, and both reject the whole menu save rather than half-applying it:

  • Path collision. If a recomputed address is already held by a live page, the save is refused with “Cannot move page: the path ‘…’ is already in use by another page”.
  • Page type root. A page may not land exactly on another page type’s root address — that would shadow the whole set’s landing path — and the save is refused with “Cannot move page: the path ‘…’ is the root path of the ‘…’ page type”.

There is also a lock, if you want one. A page type with Navigation Menu Locked checked in Settings → Page Types refuses any menu save that would change one of its pages’ URLs — no moves, no folder renames, and no removals from the nav, since removing a page flips its ancestry to the flat fallback and that is a move too. Presentation-only edits stay editable. A blocked save comes back with “Cannot change navigation: the ‘…’ page type’s navigation is locked. An Administrator or Content Publisher can uncheck Navigation Menu Locked in Settings → Page Types.”

One page, one place

A documentation page may appear in exactly one docs left-nav menu, company-wide, and exactly once inside it.

A menu save that breaks the rule comes back 409 with one of two messages, depending on which way it broke:

Duplicate page in menu: the page is already in this menu, so the add was
rejected — a page may only be referenced once, in a single docs menu

A docs page may only be referenced once, in a single docs menu

When a page genuinely belongs in two places in a reader’s mind, there are two sanctioned answers and neither is a second menu entry. Link to it from the prose of the other page, which is what a pageid: link is for. Or, if the second location is really an old address people still follow, give the page an alias — an alias is a redirect into the one real page, not a second copy of it.

A category is a folder or a page, never both

A docs menu item that points at a page may not have children. Try it and the save is refused before anything is written:

400  A docs page item cannot have children — place pages inside folders instead

The reason is mechanical rather than editorial. Every docs menu save blanks the href of any item that has a submenu, because a folder is never itself a link. So an item that carried both a page reference and children would have its page reference wiped on the way in — the page would vanish from the navigation entirely, and, because its ancestry changed, its live URL would move at the same time. Rejecting the shape outright is the alternative to that happening quietly.

The sanctioned way to give a documentation set a landing page is documentationConfiguration.startingPage, which points at a page by id and is what the breadcrumb trail uses as its home link. A page with an empty slug publishes at the set’s root — src/docs/index.md serving /docs/ — and is the usual choice. It is configured with the rest of the docs settings, described in the documentation configuration reference.

What breaks when the file and the menu disagree

The honest reason to care is not consistency for its own sake. It is that six separate things read the same derivation, and a disagreement does not produce an error — it produces a repair, later, that you did not ask for.

Bulk import is the realistic way to hit this. The import endpoint honors an explicit path on each page, and it applies the generated menu with a plain update that runs no recompute — so an imported mismatch is created deliberately and stays invisible until the next ordinary menu save.

There is no confirmation prompt and no summary. A successful menu save shows a single Menu Saved toast whether it moved nothing or moved fifty pages, so treat any change to the shape of the tree as a URL change by default.

Everything a path change ripples into
What changesAutomatic or manualWhat to check afterwards
The page’s URLAutomaticThe new address resolves and the menu link points at it
The file in your repositoryAutomatic — committed as a git move, so history follows the filegit log --follow on the new path still shows the old commits
A redirect from the old URLAutomatic — the old address is relegated to an alias when the move publishes, and aliases are emitted as 301s in _redirectsThe old URL redirects rather than 404s
The aliases array in the page’s front matterAutomatic — the alias is written into the published fileThe old path appears in aliases on the committed .md
sitemap.xmlAutomatic — the sitemap is generated from the built pagesThe old address is gone and the new one is present
BreadcrumbsAutomatic — built from the menu tree, not from the pathThe trail matches the new folder
Previous / next linksAutomatic — the pager walks the same menu treeThe page’s neighbors are the ones you intended
Leed analytics continuityAutomatic — Know keys page events on pageId, not on the URL, so a move does not split a page’s historyNothing
External analytics and Search ConsoleManual — those key on the URL and will treat the new address as a new pageSubmit the updated sitemap; expect a re-crawl window
(company_id, path) uniquenessAutomatic — enforced by the database, and checked before the menu save is acceptedNothing; a collision refuses the save outright

Two of those are worth pulling out. The redirect is real and automatic, so a considered move is not a broken-link event — but it only lands when the moved page publishes, which for a Write-role author is their next publish rather than the menu save itself. And internal links never need the redirect at all: a link written as pageid: resolves through the page’s id at build time, so it follows a move on its own. That is the whole argument for writing internal links as pageid: rather than as paths.

If you are authoring files directly

Working repository-first — writing .md files in src/ and letting Leed read them — is supported, and the invariant is the thing to hold onto:

pageTypeSlug + folderPath + slug  ===  the path derived from the file's location

Where the repository is read as the source of truth, that equation is asserted rather than assumed. A docs file that does not live under its page type’s slug root is a hard error — “does not live under its page type’s slug root” — not a warning that lets the import continue with a guess.

The checklist that keeps it true:

  1. The page type’s slug is the first directory. A file for the documentation set goes under src/docs/, where docs is the set’s own slug from Settings → Page Types. Change the page type’s slug and every file in the set has to move with it.
  2. One directory per menu folder, in the menu’s order of nesting. src/docs/site-repository/ exists because the menu has a folder named Site Repository at the top level of the docs menu, and for no other reason.
  3. The file is named for the slug. page-paths-and-folder-structure.md, lowercase, hyphenated, no other punctuation.
  4. index.md is the folder’s own address, and belongs to the page whose slug is empty.
  5. Add the page to the menu in the same change. A file with no menu entry has no folder ancestry to derive, so the flat fallback is what it gets — and the first menu save that adds it will move it.

The counterpart to this page is the menu itself: building it, ordering it and nesting it is the subject of Left Navigation Menu, which is where the menu editor and its screens live. The first segment of every path here comes from the page type, whose slug is set when you configure the page type. And the mechanics of the file itself — when it is written, what the commit looks like, what else Leed puts beside it — are covered in What Leed Writes Into Your Repo.

ESC