Writing Documentation in Markdown
Documentation that people actually use: the README, project docs in a docs folder, structure and headings, links between pages, changelogs, and docs-as-code with review and checks.
Start with the reader
Markdown makes documentation easy to write, which is exactly why so much of it is hard to use: a page grows by accretion until nobody can find the step they need. Before you write, decide who reads the page and what they are trying to do. A new developer setting up the portal, an on-call engineer at 03:00 and a manager who wants to know what changed in the last release need different pages.
A useful way to sort documentation is the Diátaxis framework: tutorials (learning), how-to guides and runbooks (doing a task), reference (looking something up) and explanation (understanding why). Each kind has its own shape. A how-to guide is a numbered list of steps; reference is often a table; explanation is prose. Mixing them on one page is the most common reason a page feels long and confusing.
The README
The README is the first file people open, and GitHub and GitLab render it on the repository's home page. Keep it short and practical; it is a front door, not the whole house. A good README for Kestrel Energy's customer portal answers five questions:
# Kestrel customer portal
Web portal where Kestrel Energy customers view meter readings,
invoices and outages.
## Run it locally
1. Install Node.js (the current LTS release).
2. Install the dependencies and start the development server:
```bash
npm ci
npm run dev
```
3. Open http://localhost:5173 in your browser.
## Documentation
- [Architecture](docs/architecture.md)
- [Runbooks](docs/runbooks/index.md)
- [Changelog](CHANGELOG.md)
## Help and ownership
Owned by the portal team (tech lead: Ravi Kumar).
Questions: the *#portal-dev* channel.
<h1>Kestrel customer portal</h1> <p>Web portal where Kestrel Energy customers view meter readings, invoices and outages.</p> <h2>Run it locally</h2> <ol> <li> <p>Install Node.js (the current LTS release).</p> </li> <li> <p>Install the dependencies and start the development server:</p> <pre><code class="language-bash">npm ci npm run dev </code></pre> </li> <li> <p>Open http://localhost:5173 in your browser.</p> </li> </ol> <h2>Documentation</h2> <ul> <li><a href="docs/architecture.md">Architecture</a></li> <li><a href="docs/runbooks/index.md">Runbooks</a></li> <li><a href="CHANGELOG.md">Changelog</a></li> </ul> <h2>Help and ownership</h2> <p>Owned by the portal team (tech lead: Ravi Kumar). Questions: the <em>#portal-dev</em> channel.</p>
Notice what makes it work:
- One sentence says what the project is, before anything else.
-
The steps are a numbered list, and the commands sit in a fenced
code block inside step 2: indented by three spaces so
they belong to the list item. The
bashafter the fence gives syntax highlighting. The blank lines around the code block make the list "loose", so each item is wrapped in a<p>; that only adds a little space. -
The commands have no
$prompt in front of them, so people can copy them without editing. - Details live in other files, linked from a short list, instead of making the README longer.
A docs folder
Anything longer than a screen or two belongs in separate files,
usually in a docs/ folder next to the code. One topic
per file keeps pages short, makes each one easy to link to, and
keeps pull requests small. Use lowercase file names with hyphens:
they become part of addresses, and they sort and type predictably.
kestrel-portal/
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── docs/
├── index.md
├── architecture.md
├── images/
│ └── login-flow.png
└── runbooks/
├── index.md
├── portal-503.md
└── certificate-renewal.md
An index.md in each folder gives readers a starting
point and is what most documentation site generators show for the
folder's address. Images go in a folder near the pages that use
them, so moving a section moves its pictures too.
Structure and headings
Headings are the skeleton of a page. Readers scan them, screen reader users jump between them, GitHub builds an outline from them, and each heading gets an anchor you can link to (see Markdown on GitHub and GitLab). A few rules make them work:
-
One top-level heading (
#) per file: the page title. -
Do not skip levels: a
###belongs under a##. Skipped levels confuse screen readers and outlines. - Make headings say something: "Restart the portal" beats "Step 4"; "Certificate expired" beats "Problem 2".
- Use real headings, not a bold line that looks like one. Bold text is invisible to outlines, anchors and assistive technology.
A table of contents
A table of contents (TOC) is a list of links to the sections of a document, usually near the top. GitHub already shows an outline button for every rendered Markdown file, and documentation sites build a TOC from the headings, so a hand-written TOC is only worth it in a long single file that is read in many places. It is a list of links to heading anchors:
## Contents
- [Symptoms](#symptoms)
- [Diagnosis](#diagnosis)
- [Fix: restart the login service](#fix-restart-the-login-service)
- [Rollback](#rollback)
<h2>Contents</h2> <ul> <li><a href="#symptoms">Symptoms</a></li> <li><a href="#diagnosis">Diagnosis</a></li> <li><a href="#fix-restart-the-login-service">Fix: restart the login service</a></li> <li><a href="#rollback">Rollback</a></li> </ul>
The fragments (#symptoms) only work where the renderer
gives headings matching ids. GitHub, GitLab and
documentation sites do; plain markdown-it does not, as you can see
in the HTML above, where the links point to anchors that do not
exist yet. A hand-written TOC also goes stale the moment someone
renames a heading, which is why
link checks matter.
Links between pages
A relative link points to
another file by its path from the current file, such as
runbooks/portal-503.md or
../architecture.md, instead of by a full web address.
It is the Markdown use of a
relative URL. You can add a fragment to land on a heading in that file.
If the portal returns HTTP 503, follow
[the 503 runbook](runbooks/portal-503.md).
For the expired-certificate case, see
[Certificate expired](runbooks/certificate-renewal.md#certificate-expired).

<p>If the portal returns HTTP 503, follow <a href="runbooks/portal-503.md">the 503 runbook</a>. For the expired-certificate case, see <a href="runbooks/certificate-renewal.md#certificate-expired">Certificate expired</a>.</p> <p><img src="images/login-flow.png" alt="Login flow from browser to identity provider"></p>
Why relative and not
https://github.com/kestrel/portal/blob/main/docs/…?
Because relative links keep working wherever the files go. GitHub
resolves them against the branch you are looking at, so a reviewer
reading a pull request sees the new version of the linked page. They
also work in a fork, in a local clone with a live preview, and on a
documentation site, where generators such as MkDocs turn links to
.md files into links to the generated pages.
Changelogs
A changelog is a file, by
convention CHANGELOG.md, that lists the notable changes
of each released version, newest first, written for the people who
use the software. It is not a copy of the commit log: commits say
how something was done, a changelog says what changed for the
reader. The widely used Keep a Changelog convention
(version 2.0.0, June 2026) groups changes under six headings: Added,
Changed, Deprecated, Removed, Fixed and Security, with ISO dates and
an Unreleased section at the top.
# Changelog
## [Unreleased]
### Added
- Outage map shows the expected restore time.
## [2.4.0] - 2026-09-22
### Fixed
- Invoices for customers with two meters showed the
wrong total.
### Security
- Session cookies now expire after 30 minutes of inactivity.
<h1>Changelog</h1> <h2>[Unreleased]</h2> <h3>Added</h3> <ul> <li>Outage map shows the expected restore time.</li> </ul> <h2>[2.4.0] - 2026-09-22</h2> <h3>Fixed</h3> <ul> <li>Invoices for customers with two meters showed the wrong total.</li> </ul> <h3>Security</h3> <ul> <li>Session cookies now expire after 30 minutes of inactivity.</li> </ul>
Version numbers usually follow semantic versioning, and teams that write structured commit messages can generate a first draft of each section automatically (see changelogs and automated releases). Read the draft before publishing: a human summary for readers is the point of the file.
Contributing guides
A contributing guide,
usually CONTRIBUTING.md, tells people how to propose a
change to a project: how to set up a development environment, how to
name branches, what a good pull request contains, which checks must
pass and who reviews. GitHub treats it as one of the
community health files
and links to it when someone opens an issue or pull request.
Inside a company it is just as useful. When Emeka Obi, new in the network team, wants to fix a step in a runbook, the contributing guide tells him to change the Markdown in a branch, open a pull request and ask the runbook's owner for review, instead of mailing a correction that gets lost.
From files to a documentation site
A documentation site is a website generated from a folder of documentation files, with navigation, search and a consistent layout. The source stays plain Markdown in the repository; a static site generator such as MkDocs, Hugo, Docusaurus or Jekyll builds the HTML (see static site generators). Most of them read the page order from a configuration file and page details from front matter.
Whether you need one depends on the readers. For a team that works in the repository anyway, rendered Markdown on GitHub or GitLab is enough. For people outside the team, such as the service desk or customers, a site with search is much friendlier, and it can be published automatically on every merge.
Docs-as-code in practice
Docs-as-code means handling documentation with the same tools and habits as code. Markdown is what makes that possible: it is plain text, so Git can show a readable diff of every change, and reviewers can comment on a single line.
In practice that means:
- Same pull request. A change that alters behavior updates the README, runbook or changelog in the same pull request, so the reviewer can ask "where is the doc update?".
- Review for the reader. Ask a reviewer who matches the audience: Mei Chen of the service desk is the right person to check whether a service desk procedure actually makes sense.
- Automated checks. A Markdown linter, a link checker and sometimes a prose linter run on every pull request (see Linting and Consistent Style).
- One sentence per line (optional). Starting each sentence on a new line does not change the rendered paragraph, because a single newline is only a soft break, but it makes diffs and review comments much more precise.
The last point in action: these two sources render the same way.
Stop the login service. Wait until the health check fails. Then start it again.
<p>Stop the login service. Wait until the health check fails. Then start it again.</p>
Stop the login service.
Wait until the health check fails.
Then start it again.
<p>Stop the login service. Wait until the health check fails. Then start it again.</p>
The browser shows both as one paragraph. In the second version a change to one sentence changes one line in the diff.