2026 Feb 21 – A small, sharp tool
I've been working on a static site generator with Jinja as a templating engine since September mostly to address my annoyance with web conventions that every page is secretly called index.html. We're insulated from this by "pretty URLs" and CMS and continuous deployment that keeps built files out of sight, but I handle HTML directly for hobby purposes on the small web where you simply drag-and-drop files into a web GUI. This is friction as a user on top of build inefficiency: every mkdir that a larger SSG runs under the hood takes more time and uses more blocks.
Essentially, all I wanted was for the Markdown input and HTML output directories to look the same, save for file extensions .md → .html. This website, for example, is written in Markdown and stored
.
|-- blog/
| |-- 2026-02-19-a-small-sharp-tool.md
| `-- index.md
|-- index.md
|-- portfolio.md
`-- reading.md
Pretty URls turn this into
.
|-- blog/
| |-- 2026-02-19-a-small-sharp-tool/
| | `-- index.html
| `-- index.html
|-- index.html
|-- portfolio/
| `-- index.html
`-- reading/
`-- index.html
What I had posted on this hobby site was copied directly from my Markdown notes vault with light YAML, so for a few months I manually deleted any YAML before building with the initial script. I also manually edited a feed.xml and a JSON for non-blog data.
The iteration you see now is called Kamote because that's what I'm eating for breakfast lately. Its biggest improvements are parsing YAML, generating an Atom feed, a simple watch/serve wrapper, and a thin CLI so my commands are
uv run kamote build
uv run kamote watch
benchmarking
I made Git worktrees for the final commit on the 11ty branch and the first commit with Kamote so that the input Markdown is the same, ideally resulting in a hardly any output difference. I ran the time command twice with each.
$ time uv run kamote build
$ time npx @11ty/eleventy
| user | system | cpu | total | |
|---|---|---|---|---|
| Eleventy | 2.75s | 0.23s | 124% | 2.401 |
| Kamote | 1.56s | 0.20s | 94% | 1.857 |
Cold run
| user | system | cpu | total | |
|---|---|---|---|---|
| Eleventy | 2.87s | 0.19s | 118% | 2.597 |
| Kamote | 0.39s | 0.06s | 99% | 0.448 |
Warm run (incremental build)
The time that means the most to humans here is total. For incremental builds, Eleventy takes almost three seconds while Kamote is done in less than one.
The resulting HTML has never been that large, but disk usage:
$ du -sh pages-11ty/docs
4.7M pages-11ty/docs
$ du -sh pages-kamote-initial/sailorfe/_build
4.6M pages-kamote-initial/sailorfe/_build
I ran ncdu for nicer, more informative output and found that the output /blog had a 44KiB difference of 240KiB on Eleventy and 196KiB on Kamote, which is likely due to Eleventy nesting directories for pretty URLs.
future
The earliest version of this hardcoded my site information in the main generation module. My main experimentation is happening over on my hobby site, so I've been working across two Git repositories with different purposes and content, which is wildly inefficient.
The best improvement so far is configuration with site.json, which lets me replicate Eleventy's site data for globals like {{ site.title }}. I don't see myself packaging this anytime soon, but you're welcome to clone this repo and play around in the dev branch. Packaging would take... god, I don't know...
kamote initcommand that createssite.jsonand a filetree skeleton. I think I know how to approach this with pathlib, but...- Probably switching from JSON to
tomlliblike I use for Ephem, or PyYAML.
I also can't figure out pymdownx.tasklist for the life of me.