Signed-off-by: oppiliappan me@oppi.li
+8
input.css
+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
+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
+

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
static/img/docs_homepage.png
This is a binary file and will not be displayed.
+30
-30
templates/index.html
+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
oppi.li
submitted
#0
1 commit
expand
collapse
blog: docs site post
Signed-off-by: oppiliappan <me@oppi.li>
1/1 failed
expand
collapse
expand 0 comments
pull request successfully merged