Shipping a docs site without a sidebar config file
How DocuGate builds navigation from the folders, headings and index files already in a repository, and the one optional file for what folders cannot say.
By Gusenga Thierry ·
The short answer
A docs site can build its navigation from the repository itself: folders become sidebar groups, each file's first top-level heading becomes its title, and index.md becomes the landing page. Keep one small optional file for what the tree cannot say, such as the order of the top-level entries and the site title, and nothing else.
Why we did not ship a sidebar file
Most documentation generators ask for a navigation file: a list of every page, in order, nested by section. It looks harmless on day one. By month three it is the file nobody updates, and a page that exists in the repository is missing from the site because somebody forgot the second edit.
DocuGate publishes markdown that already lives in a Git repository, usually
written by people who never meant to set up a docs tool. The repository already
has a structure: folders, file names, headings. So we made that structure the
navigation, and kept configuration for the few things the structure genuinely
cannot express. A repository with a docs/ folder and no configuration at all
publishes as a complete site.
Three rules, all in one file
The rules live in src/lib/docs.ts, which both the server and the browser
import, so the title the server derives is the title the reader shows.
A file's path is its URL, and index.md is the folder's address.
/** Turn `guides/deploying.md` into `guides/deploying`, and `index.md` into ''. */
export function pathToSlug(path: string): string {
const withoutExt = path.replace(/\.md$/i, '')
return withoutExt === 'index' ? '' : withoutExt.replace(/\/index$/i, '')
}
The first # heading is the title. If a file has none, the file name is
turned into words.
export function deriveTitle(path: string, content: string): string {
const heading = content.match(/^#\s+(.+)$/m)
if (heading) return heading[1].trim()
return humanise(path.split('/').pop()!.replace(/\.md$/i, ''))
}
A README stands in for a missing index. A repository far more often has a
README.md than an index.md, and without this rule the README sorted
alphabetically among the other pages and the site had no front page. promoteReadme gives the top-level README the empty slug
when nothing else has it. It is the page GitHub already shows first, so it is
almost always the right one.
Ordering when nobody said anything
Folders become groups and files inside them become pages, recursively. Within
each level, buildTree sorts in this order: anything named in the configured
order list, then the index page, then everything else alphabetically by its
label. A folder's label is its name, humanised. A page's label is its title,
not its file name.
That last point surprises people, and it is deliberate. The sidebar shows
titles, so sorting by file name would produce a list that looks unsorted to the
reader. The cost is that zeta.md titled "Alpha" sorts at the top, and that
renaming a heading can move a page.
The override that exists: docugate.json
When the defaults are wrong, there is one optional file at the repository root:
{
"docsDir": "docs",
"sidebar": ["index.md", "quickstart.md", "guides"],
"title": "Acme Handbook",
"api": { "spec": "backend/openapi.yaml" }
}
sidebaris a list of file names (with.md) and folder names. Listed entries come first, in that order; the rest follow by the default rules.titlenames the space.docsDirsays which folder to read. A space created in the dashboard stores its own folder, and that wins, because it is the one the owner can see in settings. The file still decides for repositories read without a space.api.specpoints at an OpenAPI file kept somewhere unusual.
Every field is optional, and the server reads them field by field: anything not
named is dropped, so a block the CLI owns never reaches a reader. A file that is
not valid JSON is treated as absent rather than breaking the space. The
docugate check command warns when sidebar names something that is not a file
or folder in the docs folder, which is almost always a rename it missed.
That is the whole configuration surface for navigation. There is no field to hide a page, to rename a folder, or to nest a page somewhere other than where it sits on disk. If a page should not be published, it should not be in the docs folder.
What it costs to know every title
Building the sidebar needs every page's title, and every title is inside its file. So reading a space means listing the repository tree once and then fetching every markdown file in the docs folder, eight at a time. That is one GitHub request per file. We cap a source at 300 markdown files and a whole space at 600, and a folder over the cap is reported as too large rather than served half-complete, because a sidebar that silently drops pages is worse than one that says why a section is missing. The full read is then cached for public repositories under the commit it came from; see the guide on caching per commit.
We do not read titles from front matter. The file would still have to be fetched to find it, and every author would have to add a block to every file: the configuration file again, spread across the repository.
The trade-off
Convention over configuration is a bet that the repository's structure is close
to the structure readers want. When it is, there is nothing to maintain. When it
is not, the fix is to move files, which shows up in every link and every
git blame. A navigation file would let the site and the repository disagree;
we chose to make them agree, and that choice has edges.
What we would do differently
These are real gaps in the code as it stands, not hypotheticals.
- The title rule does not know about code blocks. The heading match is a
multiline regular expression over the raw file. A page with no
#heading but a shell snippet containing# install itgets the title "install it". Thedocugate checkcommand uses the same expression, so it does not flag it either. The fix is to skip fenced code before matching. - Folder labels cannot be overridden.
api-reference/becomes "Api Reference", and the only way to get "API reference" is to rename the folder. The tree builder already accepts a map of folder labels; today only sections the app generates itself, like the API reference, use it. Exposing it indocugate.jsonwould be a small change. sidebarorders less than the CLI suggests. For a repository merged at the root of a space, which is every single-repository space, the reader applies the list to the top level of the docs folder only. Pages inside subfolders fall back to index first, then alphabetical. The CLI accepts names at any depth. One of the two should change so that what passes the check is what the reader does.- README promotion stops at the top. A
guides/README.mdis an ordinary page called whatever its heading says, not the landing page ofguides/. We would apply the same rule at every level.
Questions people also ask
How does DocuGate pick a page's title?
It uses the first line that starts with a single # and a space. If there is none, it turns the file name into words, so getting-started.md becomes Getting Started.
What becomes the landing page if there is no index.md?
A README.md at the top of the docs folder, the same page GitHub shows first when you open the repository. It only applies at the top level, not inside subfolders.
How do I change the order of the sidebar?
List file and folder names in the sidebar array of an optional docugate.json at the root of the repository. Anything not listed falls in after, with the index first and the rest alphabetically by title.
Can I rename a folder in the sidebar without renaming it on disk?
Not today. A folder's label is its name with dashes and underscores turned into spaces and each word capitalised. Renaming the folder is the only way to change it.
Read next
- How to turn markdown files into a documentation website: The ways to publish a folder of markdown as a docs site: a static site generator, a hosted docs platform, or a host that reads the repository directly.
- What is docs-as-code?: Docs-as-code explained: documentation written in plain text, kept in Git beside the code, reviewed in pull requests and published automatically.
Get started with DocuGate, free.