engineering blog at https://blog.tangled.sh
at master 241 lines 7.2 kB view raw view rendered
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![docs.tangled.org](/static/img/docs_homepage.png) 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/)!