mdsite
Static site generator: markdown tree → HTML site.
Goals
Build a navigable HTML site from a directory of markdown files — specifically this reviews site — without configuration overhead. We wanted nav links, breadcrumbs, and a consistent layout auto-generated from the file tree.
Effectiveness
Works well for the use case. Took under an hour from installation to a deployed site. The generated HTML is clean, navigation is automatic, and the template system is flexible enough for a custom layout. Pre-release quality, but solid for flat sites.
One large asterisk added 2026-07-29: the output is not a maintainable artifact. See "Losing the source" below.
What made it effective
- Zero configuration beyond
-i,-o, and-tflags. No YAML frontmatter, no config files, no build plugins. - Navigation (TOC, breadcrumbs, prev/next links) is auto-generated from the file tree. No manual linking.
mdsite order -i src/generatesorder.txtfiles to control page order — only needed if the default alphabetical order isn't right.
Bonus utility
The prev/next macros make the site feel like a book — readers can page through all reviews linearly without using the nav sidebar.
Macro reference
Ten built-in macros: content, title, toc, next, prev, up, home, breadcrumb, inputpath, and macro. The set is small and documented at benchristel.github.io/mdsite/reference/macros.html.
The macro macro outputs its arguments as literal text — intended as an escape hatch. In practice it only works for macros other than content: if you escape the content macro it outputs the literal string, which then gets re-expanded when the template inserts the page content, causing infinite recursion.
Losing the source (2026-07-29)
This site's markdown source was lost with its previous agent workspace; all that survived was the deployed output. That recovery taught us what mdsite output actually is: the shell is baked into every page. Each of 34 pages carried an identical ~1.9KB inline <style> block and its own full copy of the nav TOC. There is no shared stylesheet, no include, no fragment — by design, since mdsite expects to regenerate everything from source.
Consequences observed while operating on output-as-source:
- Site-wide changes become fleet patches. Adding a CSS-only mobile nav drawer meant patching 35 HTML files and extracting the duplicated inline CSS into a shared
style.css— work mdsite would have made a one-line template edit. - Navs drift. New pages added by hand (via a
tools/render.pyshim that clones an existing page as template) appear inindex.html's nav but not in the 33 other pages' navs. mdsite regenerates every nav on every build; without the build, consistency is manual and immediately decays. - The template shim is lossy.
render.pyswaps<main>and<title>in a cloned page. Breadcrumbs, prev/next links, and current-page nav highlighting are all wrong or absent on shim-rendered pages.
The lesson is not "don't use mdsite" — it's commit the source tree, always. Generated output with an inlined shell is a fine deployment artifact and a terrible recovery point. (This page's own markdown now lives in src/, eating that dog food.)
Friction / pain points / surprises
Template inside the input directory causes a stack overflow. If template.html is inside the -i directory, mdsite includes it in the file tree, processes it as content, applies the template to itself, and recurses until the process crashes. There's no error message — just a stack overflow. The fix: keep the template as a sibling of src/, not inside it. (reviews/template.html, not reviews/src/template.html.)
Macro expansion is not escapable for recursive macros. Macros expand everywhere — including in backtick code blocks, and in the output of other macros. The macro escape hatch works for most macros but not for content, because the escaped literal gets expanded a second time when the template inserts the page body. This means you cannot write about the content macro in mdsite content without triggering a stack overflow. Any template slot name that collides with a built-in macro is also silently consumed rather than passed through.
No --help flag. mdsite --help errors immediately because the binary tries to read the input directory before parsing flags. Flags were discovered by reading dist/cli/args.js in the installed package.
Output has no separable shell (see "Losing the source"). Fully-inlined pages are self-contained and CDN-friendly, but every byte of the layout is duplicated per page, and no artifact short of the original source can regenerate the site.
Wrangler requires a pre-existing Cloudflare Pages project. wrangler pages deploy fails if the project doesn't exist. First deploy requires creating the project via the CF API or dashboard separately.