By now your project has a solid foundation. pyproject.toml tells every tool what to do. pre-commit makes sure those tools actually run. Code gets formatted, linted, type-checked, and tested automatically, every single commit.
But there’s a question none of that answers.
A new collaborator joins six months from now. They clone the repo. They run the setup commands. Everything installs fine. Then they open the project and ask: “Okay, but what does this code actually do? Where do I even start reading?”
A README can answer the basics — how to install, how to run tests. But a README that tries to explain the paper you’re replicating, every experiment, every module’s API, and the current state of progress turns into one impossibly long wall of text that nobody actually reads top to bottom.
What you need isn’t a longer README. It’s a documentation site — something with pages, navigation, search, and structure, the kind of thing you’d expect from any real software library.
That’s what MkDocs gives you.
The idea is surprisingly simple
Imagine writing a book versus writing one giant sticky note. A sticky note crams everything into a single surface — you squint, you scroll, you lose your place. A book has chapters. A table of contents. You open to the section you actually need.
MkDocs takes a folder of plain markdown files — the same .md format you already know from READMEs — and turns them into that book. Each file becomes a page. A navigation menu lets readers jump between them. Search lets them find a specific term across the whole project. And because it’s just markdown, writing documentation feels exactly like writing any other .md file — no new syntax to learn.
Where the configuration lives
mkdocs.ymlJust like pyproject.toml is the control room for your code tools, mkdocs.yml is the control room for your documentation site. It sits in the root of your repo, right next to pyproject.toml.
site_name: Market Making Paper Replication
theme:
name: material
nav:
- Home: index.md
- Paper Overview: paper_overview.md
- Experiments: experiments.md
- Progress: progress.md
- API Reference: api/site_name is exactly what it sounds like — the title shown at the top of your site. theme tells MkDocs which visual style to use; material refers to the Material for MkDocs theme, which is the polished, searchable, mobile-friendly look most modern documentation sites use (think Python’s own popular libraries — many of them look like this). nav is your table of contents: each entry maps a label readers see in the sidebar to an actual markdown file sitting in your docs/ folder.
Notice the structure mirrors a book, not a single document. “Home” is your cover page. “Paper Overview” explains what’s being replicated. “Experiments” documents what was actually run. “Progress” tracks where things currently stand. Each one is its own file, its own page, easy to find.
Where the actual content lives
docs/
index.md
paper_overview.md
experiments.md
progress.md
api/This is the folder mkdocs.yml points into. Every markdown file you write here becomes a page on the site. index.md is special — it’s the homepage, the first thing someone sees when they open the docs.
Think of mkdocs.yml as the spine of the book, and docs/ as the pages bound inside it. The spine decides the order and labels. The folder holds the actual writing.
The part that writes itself: API docs
Writing prose pages like “Paper Overview” or “Experiments” takes real thought — nobody can automate explaining why you made a modeling choice. But there’s a category of documentation that’s pure, tedious bookkeeping: listing every function in your src/ folder, what arguments it takes, what it returns, what its docstring says.
That’s exactly the kind of work that should never be done by hand.
plugins:
- mkdocstrings:
handlers:
python:
paths: [src]This plugin, mkdocstrings, reads your actual Python source code — the docstrings you already wrote, the type hints already in your functions — and generates documentation pages from them automatically. Write a docstring once, in the code, where it belongs. The API reference page updates itself.
Think of it like a building directory that updates automatically every time someone moves offices, instead of someone manually repainting a sign on the wall every time staff changes.
Handling notebooks inside the docs
Remember from the last post: your notebooks/ folder has outputs stripped before every commit, and reports/ holds frozen HTML snapshots of finished results. MkDocs can actually render notebooks directly as documentation pages too, using a plugin:
plugins:
- mkdocs-jupyterThis lets you point a nav entry directly at a notebook file, and MkDocs renders it inline as a page — code, markdown cells, and (if you point it at a notebook that still has outputs, like ones in a designated “final” state) the rendered charts and tables too. Useful if you want a specific analysis to live as part of the official documentation site, rather than just sitting in reports/ as a standalone file.
Previewing before you publish
mkdocs serveThis starts a local server on your machine and shows you exactly what the site will look like, live, as you edit. Change a markdown file, save it, and the page refreshes in your browser automatically. It’s the same idea as a live preview in any writing tool — you see the result before anyone else does.
Publishing it
mkdocs buildThis command takes every markdown file, runs it through the theme and navigation defined in mkdocs.yml, and outputs a complete, ready-to-host website — plain HTML, CSS, and JavaScript — into a site/ folder.
In your repo’s case, this build step doesn’t even need to be run manually. Looking back at the repository layout from the README:
.github/
workflows/
docs.yml # Deploy docs on merge to mainA GitHub Actions workflow handles it automatically. Every time someone merges a PR into main, GitHub runs mkdocs build for you and publishes the result to GitHub Pages — the 📖 Full documentation → link sitting right at the top of your README. Nobody has to remember to “update the docs site.” Merging the code is updating the docs site.
Yes, this is exactly the file the blog already references but doesn’t show — worth adding so the “publishing” section isn’t hand-wavy. Here’s where and how to slot it in.
Where it goes
Right after the “Publishing it” section, before “How it all connects.” That section currently says “a GitHub Actions workflow handles it automatically” without showing what that actually looks like — this fills that gap.
What to add
What the deploy workflow actually does
Here’s the full docs.yml referenced earlier:
name: Deploy Docs
on:
push:
branches: [main]
permissions:
contents: write
jobs:
deploy:
name: Build & Deploy MkDocs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --extra docs
- name: Build & deploy to GitHub Pages
run: uv run mkdocs gh-deploy --forceWalking through it the same way we walked through .pre-commit-config.yaml earlier: on: push: branches: [main] is the trigger — this workflow only fires when something is pushed directly to main, which in practice means whenever a PR is merged. It doesn’t run on every commit to a feature branch, only the ones that actually land.
permissions: contents: write is a small but easy-to-miss detail. By default, GitHub Actions workflows get read-only access to your repo. Deploying docs means writing a new branch (the one GitHub Pages serves from), so this line explicitly grants that permission. Skip it, and the deploy step fails with a permissions error that’s confusing to debug the first time you see it.
Inside steps, the workflow does four things in order. actions/checkout@v4 pulls your repository’s code onto the runner — the fetch-depth: 0 option fetches the full git history rather than just the latest commit, which gh-deploy needs internally to manage the Pages branch correctly. astral-sh/setup-uv@v5 installs uv on the runner, the same tool you’ve been using locally. uv sync --extra docs installs exactly the docs dependency group from pyproject.toml — mkdocs, mkdocs-material, mkdocstrings, nothing more, since this job has no need for black or pytest.
And the last line is the one doing the actual work: mkdocs gh-deploy --force. This single command builds your site (the same thing mkdocs build does) and then pushes the result straight to a special gh-pages branch, which is what GitHub Pages actually serves to the public. The --force flag overwrites whatever was there before, which is exactly what you want — each deploy should fully replace the last one, not merge with it.
Put together: merge a PR → GitHub spins up a clean machine → installs only what’s needed for docs → builds the site → pushes it live. No one runs a command by hand. No one forgets a step.
How it all connects
Step back and look at the full picture this series has built.
pyproject.toml defined every tool your project needs, in one shared file. pre-commit made sure those tools run automatically, before bad code ever reaches a commit. And now MkDocs takes everything that’s true about the project — the code’s own docstrings, the explanations you write by hand, even finished notebook analyses — and turns it into a single site anyone can browse, without needing to dig through folders or guess where things live.
A new collaborator’s experience six months from now looks completely different because of this. They clone the repo. uv sync installs exactly what’s needed. Every commit they make is automatically checked for quality. And if they’re confused about how anything works, the docs link in the README takes them straight to an organized, searchable site — built from the same source of truth as the code itself.
Final thoughts
None of these three pieces — pyproject.toml, pre-commit, MkDocs — are complicated on their own. What makes them powerful is that they all point back to the same idea: write the truth once, in one place, and let tooling carry it everywhere else it needs to go automatically.
That’s the difference between a project that’s pleasant to join and one where every new collaborator spends their first week just figuring out how things work.