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.mdRead 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.mdThe 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.
| Segment | Source | Set where | Changing it moves |
|---|---|---|---|
<pageTypeSlug> — always first | The page type’s own slug | Settings → Page Types | Every page in the set |
<folder>/… — zero or more | slugify() of each ancestor folder’s name in the docs left-nav menu | The documentation menu editor | Every page beneath that folder |
<slug> — always last | The page’s own slug, derived from its title when it is created | The page’s settings in the editor | That 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.
| Candidate | What it looks like it does | What it actually does |
|---|---|---|
path in front matter | Sets the page’s URL | Nothing — no such key is emitted or read; your value becomes an ordinary custom field |
path on POST /api/page | Files a new page under a folder | Ignored for a menu-bound docs page type; pass menuFolderId instead |
path on PUT /api/page/:pageId | Moves an existing page | Rejected by the request schema before the handler sees it |
| Saving the docs left-nav menu | Reorders the navigation | The 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 menuWhen 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 insteadThe 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 changes | Automatic or manual | What to check afterwards |
|---|---|---|
| The page’s URL | Automatic | The new address resolves and the menu link points at it |
| The file in your repository | Automatic — committed as a git move, so history follows the file | git log --follow on the new path still shows the old commits |
| A redirect from the old URL | Automatic — the old address is relegated to an alias when the move publishes, and aliases are emitted as 301s in _redirects | The old URL redirects rather than 404s |
The aliases array in the page’s front matter | Automatic — the alias is written into the published file | The old path appears in aliases on the committed .md |
sitemap.xml | Automatic — the sitemap is generated from the built pages | The old address is gone and the new one is present |
| Breadcrumbs | Automatic — built from the menu tree, not from the path | The trail matches the new folder |
| Previous / next links | Automatic — the pager walks the same menu tree | The page’s neighbors are the ones you intended |
| Leed analytics continuity | Automatic — Know keys page events on pageId, not on the URL, so a move does not split a page’s history | Nothing |
| External analytics and Search Console | Manual — those key on the URL and will treat the new address as a new page | Submit the updated sitemap; expect a re-crawl window |
(company_id, path) uniqueness | Automatic — enforced by the database, and checked before the menu save is accepted | Nothing; 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 locationWhere 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:
- The page type’s slug is the first directory. A file for the documentation set goes under
src/docs/, wheredocsis 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. - One directory per menu folder, in the menu’s order of nesting.
src/docs/site-repository/exists because the menu has a folder namedSite Repositoryat the top level of the docs menu, and for no other reason. - The file is named for the slug.
page-paths-and-folder-structure.md, lowercase, hyphenated, no other punctuation. index.mdis the folder’s own address, and belongs to the page whose slug is empty.- 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.