engineering blog at https://blog.tangled.sh

blog: docs site post #5

merged opened by oppi.li targeting master from op/tupssoolqvus
Labels

None yet.

Participants 1
AT URI
at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3mc7q22ebo622
+279 -30
Diff #0
+8
input.css
··· 85 85 } 86 86 } 87 87 88 + img { 89 + @apply border border-gray-200 rounded dark:border-gray-700; 90 + } 91 + 92 + img.icon { 93 + @apply border-0 dark:brightness-100 dark:opacity-100; 94 + } 95 + 88 96 a { 89 97 @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 90 98 }
+241
pages/blog/docs.md
··· 1 + --- 2 + atroot: true 3 + template: 4 + slug: docs 5 + title: why we rolled our own documentation site 6 + subtitle: you don't need mintlify 7 + date: 2026-01-06 8 + authors: 9 + - name: Akshay 10 + email: akshay@tangled.org 11 + handle: oppi.li 12 + draft: true 13 + --- 14 + 15 + We recently organized our documentation and put it up on 16 + https://docs.tangled.org, using just pandoc. For several 17 + reasons, using pandoc to roll your own static sites is more 18 + than 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 + 38 + I took the time to evaluate several documentation engine 39 + solutions: 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 + 54 + MkDocs and MdBook are still on my radar however, in case we 55 + need a bigger feature set. 56 + 57 + ## using pandoc 58 + 59 + [pandoc](https://pandoc.org/) is a wonderfully customizable 60 + markup converter. It provides a "chunkedhtml" output format, 61 + which is perfect for generating documentation sites. Without 62 + any customization, 63 + [this](https://pandoc.org/demo/example33/) is the generated 64 + output, for this [markdown file 65 + input](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 + 70 + Massaging 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 + 84 + Generating the docs is done with one pandoc command: 85 + 86 + ```bash 87 + pandoc 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 + 100 + The "sidebar" style table-of-contents needs to be collapsed 101 + on mobile displays. Most of the engines I evaluated seem to 102 + require JS to collapse and expand the sidebar, with MkDocs 103 + being the outlier, it uses a checkbox with the 104 + [`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:checked) 105 + pseudo-class trick to avoid JS. 106 + 107 + The 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 + 116 + The 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 + 122 + And 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 + 135 + The TOC is scrollable independently and can be collapsed by 136 + clicking anywhere on the screen outside the sidebar. 137 + Searching for content in the page via "Find in page" does 138 + not show any results that are present in the popover 139 + however. The collapsible TOC is only available on smaller 140 + viewports, the TOC is not hidden on larger viewports. 141 + 142 + ## search 143 + 144 + There is native search on the site for now. Taking 145 + inspiration from https://htmx.org's search bar, our search 146 + bar also simply redirects to Google: 147 + 148 + ``` 149 + <form action="https://google.com/search"> 150 + <input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]"> 151 + ... 152 + </form> 153 + ``` 154 + 155 + I mentioned earlier that Ctrl+F has typically worked better 156 + for me than, say, the search engine provided by Docusaurus. 157 + To that end, the same docs have been exported to a ["single 158 + page" format](https://docs.tangled.org/single-page.html), by 159 + just 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 + 173 + With all the content on a single page, it is trivial to 174 + search through the entire site with the browser. If the docs 175 + do outgrow this, I will consider other options! 176 + 177 + ## building and deploying 178 + 179 + We use [nix](https://nixos.org) and 180 + [colmena](https://colmena.cli.rs/) to build and deploy all 181 + Tangled services. A nix derivation to [build the 182 + documentation](https://tangled.org/tangled.org/core/blob/master/nix/pkgs/docs.nix) 183 + site is written very easily with the `runCommandLocal` 184 + helper: 185 + 186 + ```nix 187 + runCommandLocal "docs" {} '' 188 + . 189 + . 190 + . 191 + ${pandoc}/bin/pandoc ${src}/docs/DOCS.md ... 192 + . 193 + . 194 + . 195 + '' 196 + ``` 197 + 198 + The nixos machine is configured to serve the site [via 199 + nginx](https://tangled.org/tangled.org/infra/blob/master/hosts/nixery/services/nginx.nix#L7): 200 + 201 + ```nix 202 + services.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 + 216 + And deployed using `colmena`: 217 + 218 + ```bash 219 + nix run nixpkgs#colmena -- apply 220 + ``` 221 + 222 + To update the site, I first run: 223 + 224 + ```bash 225 + nix flake update tangled 226 + ``` 227 + 228 + Which bumps the `tangled` flake input, and thus 229 + `tangled-pkgs.docs`. The above `colmena` invocation applies 230 + the changes to the machine serving the site. 231 + 232 + ## notes 233 + 234 + Going homegrown has made it a lot easier to style the 235 + documentation site to match the main site. Unfortunately 236 + there are still a few discrepancies between pandoc's 237 + markdown rendering and 238 + [goldmark's](https://pkg.go.dev/github.com/yuin/goldmark/) 239 + markdown rendering (which is what we use in Tangled). We may 240 + yet roll our own SSG, 241 + [TigerStyle](https://tigerbeetle.com/blog/2025-02-27-why-we-designed-tigerbeetles-docs-from-scratch/)!
static/img/docs_homepage.png

This is a binary file and will not be displayed.

+30 -30
templates/index.html
··· 10 10 {{ .Meta.title }} 11 11 </title> 12 12 13 - <body class="bg-slate-100 dark:bg-gray-900 flex flex-col min-h-screen"> 14 - {{ template "partials/nav.html" }} 15 - <div class="prose dark:prose-invert mx-auto px-1 pt-4 flex-grow flex flex-col container"> 16 - <main> 17 - <header class="px-6"> 18 - <h1 class="mb-0">{{ index .Meta "title" }}</h1> 19 - <h2 class="font-light mt-1 mb-0 text-lg">{{ index .Meta "subtitle" }}</h2> 20 - </header> 13 + <body class="bg-slate-100 dark:bg-gray-900 flex flex-col items-center min-h-screen"> 14 + {{ template "partials/nav.html" }} 15 + <div class="px-1 pt-4 flex-grow flex flex-col w-full max-w-[75ch]"> 16 + <main> 17 + <header class="px-6"> 18 + <h1 class="mb-0 text-2xl font-bold text-black dark:text-white">{{ index .Meta "title" }}</h1> 19 + <h2 class="font-light text-gray-600 dark:text-gray-400 mt-1 mb-0 text-lg">{{ index .Meta "subtitle" }}</h2> 20 + </header> 21 21 22 - {{ .Body }} 22 + {{ .Body }} 23 23 24 - <section class="py-4"> 25 - <ul class="px-0"> 24 + <section class="py-4"> 25 + <ul class="px-0 space-y-4"> 26 26 {{ $posts := .Extra.blog }} 27 27 {{ range $posts }} 28 - <li class="mt-5 bg-white dark:bg-gray-800 py-4 px-6 rounded drop-shadow-sm list-none"> 29 - {{ $dateStr := .Meta.date }} 30 - {{ $date := parsedate $dateStr }} 31 - <div class="post-date py-1 mb-0 text-sm">{{ $date.Format "02 Jan, 2006" }}</div> 32 - <div> 33 - <a class="title mb-0 text-xl no-underline font-bold" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 34 - {{ if .Meta.draft }} 35 - <span class="text-red-500">[draft]</span> 36 - {{ end }} 37 - <p class="italic mt-1 mb-0">{{ .Meta.subtitle }}</p> 38 - </div> 39 - </li> 28 + <li class="bg-white dark:bg-gray-800 py-4 px-6 rounded drop-shadow-sm list-none"> 29 + {{ $dateStr := .Meta.date }} 30 + {{ $date := parsedate $dateStr }} 31 + <div class="post-date py-1 mb-0 text-sm text-gray-600 dark:text-gray-400">{{ $date.Format "02 Jan, 2006" }}</div> 32 + <div> 33 + <a class="title mb-0 text-xl no-underline font-bold" href="/{{ .Meta.slug }}.html">{{ .Meta.title }}</a> 34 + {{ if .Meta.draft }} 35 + <span class="text-red-500">[draft]</span> 36 + {{ end }} 37 + <p class="italic mt-1 mb-0 text-gray-600 dark:text-gray-400">{{ .Meta.subtitle }}</p> 38 + </div> 39 + </li> 40 40 {{ end }} 41 41 </ul> 42 - </section> 43 - </main> 44 - </div> 45 - <footer class="w-full"> 46 - {{ template "partials/footer.html" }} 47 - </footer> 48 - </body> 42 + </section> 43 + </main> 44 + </div> 45 + <footer class="w-full"> 46 + {{ template "partials/footer.html" }} 47 + </footer> 48 + </body> 49 49 50 50 </html>

History

1 round 0 comments
sign up or login to add to the discussion
oppi.li submitted #0
1 commit
expand
blog: docs site post
1/1 failed
expand
expand 0 comments
pull request successfully merged