code.pablomurad.com

mdssg

Single-file static site generator for a folder of linked markdown notes

git clone https://code.pablomurad.com/mdssg.git

Recent commits

  1. 33eb9ff
    feat: fix
    
  2. 85d16d1
    feat: ship retro theme and global graph
    
  3. fe9c03c
    first commit
    

Full log →

Readme

mdssg

A static site generator for a folder of linked markdown notes. One file, no dependencies, Linux and Windows.

python tools/build_pyz.py          # produces one small mdssg.pyz
python mdssg.pyz init my-site
python mdssg.pyz serve -C my-site

Requires Python 3.11 or newer. That is the whole list.

This project is finished, and will rarely be updated. It does what I built it to do, and I would rather it stay small than grow. Do not wait on me for a fix or a feature — fork it and change it. Rename it, strip it down, take the parser or the graph code and build something else out of it, ship it commercially. The MIT licence already allows all of that and I am saying it out loud so nobody has to wonder. Issues and pull requests may sit unanswered; that is not rudeness, it is the stated deal.

ci python dependencies licence

Why another one

The generators that understand [[wikilinks]] are Node.js runtimes with a heavy install. The ones that are small treat each markdown file as an isolated document with no graph at all. mdssg takes the empty quadrant: graph-aware, but tiny and dependency-free.

It builds the graph, not just the pages.

Wikilinks resolve by name. [[Kant]] finds the note wherever it lives. Folders organise your writing and never appear in a URL, so moving a file breaks nothing.

A link to an unwritten note is not a dead link. It becomes a seed page: a real page at a real URL, assembled from every sentence on the site that mentioned that name. The site is closed under linking — an internal dead link cannot be produced. And because seed URLs and note URLs come from the same name, writing the note later lands on the URL the seed already had.

Backlinks quote the sentence they came from, not just the title.

Related notes come from the shape of the graph — bibliographic coupling and co-citation over your own links. No tags to maintain, no model to run, and mdssg doctor --explain-related X will tell you why.

The graph is visible. Every build includes a compact, clickable /graph/ map of the written notes, seed pages and the links between them. It is an SVG, not a JavaScript application. Large sites keep the drawing bounded to 180 nodes and 2,400 edges while the grouped text directory still lists every page.

Search stays static. /search/ loads a small JSON index generated with the site and searches it with about 2 KB of dependency-free JavaScript. Nothing is sent to a service. Each note also carries a compact graph of its immediate neighbours.

Documentation

The documentation is the example site, and it is written with mdssg.

Read it as markdown right here in example/content/, or build it and read it as a site:

python -m mdssg build -C example
python -m mdssg serve -C example

Commands

mdssg init [dir]              scaffold a site
mdssg build                   render into public/
mdssg build --clean           discard previous output and cache first
mdssg build --index-only      precompute the graph for CGI, render nothing
mdssg serve                   preview, rebuilding on change
mdssg new "A Title" [--post]  create a note
mdssg doctor                  seeds, likely typos, orphans
mdssg doctor --explain-related X

What is in the box

Its own markdown parser, its own template engine, global and local link graphs, static full-text search, a themed 404, an incremental build, a dev server with live reload, RSS, canonical URLs, a sitemap, and a CGI handler for hosting that predates all of this. The dependency list is empty.

No plugin system, no shortcodes, no taxonomy engine, no asset pipeline.

Development

python -m unittest discover -s tests -t . -v
python tools/build_pyz.py && python tools/check_pyz.py
python tools/benchmark.py --notes 1500 --max-seconds 20

CI runs the suite on Linux, macOS and Windows across Python 3.11 through 3.14, and runs it again against the packaged .pyz — reading the bundled theme works in a checkout and fails inside a zip archive unless it is done properly, so that last job is not ceremony.

Two tests carry more weight than the rest:

  • closed under linking — after any build, every internal href in the output resolves to a file that exists in the output
  • clean equals incremental — a clean build and an incremental build, after edits of every shape, must produce byte-identical trees
  • large-site budget — CI builds 1,500 linked notes from scratch in at most 20 seconds and then requires an unchanged rebuild to write zero files

Status

Finished, and rarely updated. See the note at the top: fork it rather than waiting on me.

If you do build something on top of it, I would genuinely like to see it, but you owe me nothing beyond keeping the copyright line the licence asks for.

Licence

MIT. Copyright © 2026 Pablo Murad <pablomurad@pm.me>

You are free to use, modify, redistribute and sell this, with or without changes, for any purpose, without asking.