forked from
tangled.org/site
engineering blog at https://blog.tangled.sh
1---
2atroot: true
3template:
4slug: docs
5title: we rolled our own documentation site
6subtitle: you don't need mintlify
7date: 2026-01-12
8authors:
9 - name: Akshay
10 email: akshay@tangled.org
11 handle: oppi.li
12draft: false
13---
14
15We recently organized our documentation and put it up on
16https://docs.tangled.org, using just pandoc. For several
17reasons, using pandoc to roll your own static sites is more
18than sufficient for small projects.
19
20
21
22## requirements
23
24- Lives in [our
25 monorepo](https://tangled.org/tangled.org/core).
26- No JS: a collection of pages containing just text
27 should not require JS to view!
28- Searchability: in practice, documentation engines that
29 come bundled with a search-engine have always been lack
30 lustre. I tend to Ctrl+F or use an actual search engine in
31 most scenarios.
32- Low complexity: building, testing, deploying should be
33 easy.
34- Easy to style
35
36## evaluating the ecosystem
37
38I took the time to evaluate several documentation engine
39solutions:
40
41- [Mintlify](https://www.mintlify.com/): It is quite obvious
42 from their homepage that mintlify is performing an AI
43 pivot for the sake of doing so.
44- [Docusaurus](https://docusaurus.io/): The generated
45 documentation site is quite nice, but the value of pages
46 being served as a full-blown React SPA is questionable.
47- [MkDocs](https://www.mkdocs.org/): Works great with JS
48 disabled, however the table of contents needs to be
49 maintained via `mkdocs.yml`, which can be quite tedious.
50- [MdBook](https://rust-lang.github.io/mdBook/index.html):
51 As above, you need a `SUMMARY.md` file to control the
52 table-of-contents.
53
54MkDocs and MdBook are still on my radar however, in case we
55need a bigger feature set.
56
57## using pandoc
58
59[pandoc](https://pandoc.org/) is a wonderfully customizable
60markup converter. It provides a "chunkedhtml" output format,
61which is perfect for generating documentation sites. Without
62any customization,
63[this](https://pandoc.org/demo/example33/) is the generated
64output, for this [markdown file
65input](https://pandoc.org/demo/MANUAL.txt).
66
67- You get an autogenerated TOC based on the document layout
68- Each section is turned into a page of its own
69
70Massaging pandoc to work for us was quite straightforward:
71
72- I first combined all our individual markdown files into
73 [one big
74 `DOCS.md`](https://tangled.org/tangled.org/core/blob/master/docs/DOCS.md)
75 file.
76- Modified the [default
77 template](https://github.com/jgm/pandoc-templates/blob/master/default.chunkedhtml)
78 to put the TOC on every page, to form a "sidebar", see
79 [`docs/template.html`](https://tangled.org/tangled.org/core/blob/master/docs/template.html)
80- Inserted tailwind `prose` classes where necessary, such
81 that markdown content is rendered the same way between
82 `tangled.org` and `docs.tangled.org`
83
84Generating the docs is done with one pandoc command:
85
86```bash
87pandoc docs/DOCS.md \
88 -o out/ \
89 -t chunkedhtml \
90 --variable toc \
91 --toc-depth=2 \
92 --css=docs/stylesheet.css \
93 --chunk-template="%i.html" \
94 --highlight-style=docs/highlight.theme \
95 --template=docs/template.html
96```
97
98## avoiding javascript
99
100The "sidebar" style table-of-contents needs to be collapsed
101on mobile displays. Most of the engines I evaluated seem to
102require JS to collapse and expand the sidebar, with MkDocs
103being the outlier, it uses a checkbox with the
104[`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:checked)
105pseudo-class trick to avoid JS.
106
107The other ways to do this are:
108
109- Use `<details` and `<summary>`: this is definitely a
110 "hack", clicking outside the sidebar does not collapse it.
111 Using Ctrl+F or "Find in page" still works through the
112 details tag though.
113- Use the new `popover` API: this seems like the perfect fit
114 for a "sidebar" component.
115
116The bar at the top includes a button to trigger the popover:
117
118```html
119<button popovertarget="toc-popover">Table of Contents</button>
120```
121
122And a `fixed` position div includes the TOC itself:
123
124```html
125<div id="toc-popover" popover class="fixed top-0">
126 <ul>
127 Quick Start
128 <li>...</li>
129 <li>...</li>
130 <li>...</li>
131 </ul>
132</div>
133```
134
135The TOC is scrollable independently and can be collapsed by
136clicking anywhere on the screen outside the sidebar.
137Searching for content in the page via "Find in page" does
138not show any results that are present in the popover
139however. The collapsible TOC is only available on smaller
140viewports, the TOC is not hidden on larger viewports.
141
142## search
143
144There is no native search on the site for now. Taking
145inspiration from [https://htmx.org](https://htmx.org)'s search bar, our search
146bar also simply redirects to Google:
147
148```html
149<form action="https://google.com/search">
150 <input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]">
151 ...
152</form>
153```
154
155I mentioned earlier that Ctrl+F has typically worked better
156for me than, say, the search engine provided by Docusaurus.
157To that end, the same docs have been exported to a ["single
158page" format](https://docs.tangled.org/single-page.html), by
159just removing the `chunkedhtml` related options:
160
161```diff
162 pandoc docs/DOCS.md \
163 -o out/ \
164- -t chunkedhtml \
165 --variable toc \
166 --toc-depth=2 \
167 --css=docs/stylesheet.css \
168- --chunk-template="%i.html" \
169 --highlight-style=docs/highlight.theme \
170 --template=docs/template.html
171```
172
173With all the content on a single page, it is trivial to
174search through the entire site with the browser. If the docs
175do outgrow this, I will consider other options!
176
177## building and deploying
178
179We use [nix](https://nixos.org) and
180[colmena](https://colmena.cli.rs/) to build and deploy all
181Tangled services. A nix derivation to [build the
182documentation](https://tangled.org/tangled.org/core/blob/master/nix/pkgs/docs.nix)
183site is written very easily with the `runCommandLocal`
184helper:
185
186```nix
187runCommandLocal "docs" {} ''
188 .
189 .
190 .
191 ${pandoc}/bin/pandoc ${src}/docs/DOCS.md ...
192 .
193 .
194 .
195''
196```
197
198The NixOS machine is configured to serve the site [via
199nginx](https://tangled.org/tangled.org/infra/blob/master/hosts/nixery/services/nginx.nix#L7):
200
201```nix
202services.nginx = {
203 enable = true;
204 virtualHosts = {
205 "docs.tangled.org" = {
206 root = "${tangled-pkgs.docs}";
207 locations."/" = {
208 tryFiles = "$uri $uri/ =404";
209 index = "index.html";
210 };
211 };
212 };
213};
214```
215
216And deployed using `colmena`:
217
218```bash
219nix run nixpkgs#colmena -- apply
220```
221
222To update the site, I first run:
223
224```bash
225nix flake update tangled
226```
227
228Which bumps the `tangled` flake input, and thus
229`tangled-pkgs.docs`. The above `colmena` invocation applies
230the changes to the machine serving the site.
231
232## notes
233
234Going homegrown has made it a lot easier to style the
235documentation site to match the main site. Unfortunately
236there are still a few discrepancies between pandoc's
237markdown rendering and
238[goldmark's](https://pkg.go.dev/github.com/yuin/goldmark/)
239markdown rendering (which is what we use in Tangled). We may
240yet roll our own SSG,
241[TigerStyle](https://tigerbeetle.com/blog/2025-02-27-why-we-designed-tigerbeetles-docs-from-scratch/)!