Docs
Darkmown is a Markdown-native framework for mostly-static sites with tiny reactive islands. Pages are plain files: .md stays pure CommonMark, and .wd ("whateverdown") is the same Markdown plus first-party directives.
Relative include: this hidden block lives beside the docs page as -relative-note.wd, so it can be included without becoming a route.
Install
npx @zvndev/darkmown init my-site
cd my-site
npm install
npm run dev
Or add Darkmown to an existing project with npm install -D @zvndev/darkmown. The package is scoped; the command it installs is plain darkmown.
The two formats
.mdis strict CommonMark. Directives, includes, and{ bindings }stay plain text, and the build prints a hint if it spots.wdsyntax in a.mdfile..wdis Markdown plus directives. Renaming a file from.mdto.wdis the upgrade path — nothing else changes.
Routing rules
site/pages/index.wdbecomes/.site/pages/docs/index.wdbecomes/docs/..secret.wd,-draft.wd, and hidden folders do not become pages.site/_/is the include shelf, never a route.
Includes
@include /nav.wdresolves fromsite/_.@include ./-relative-note.wdresolves beside the current page.@include /card.wd with title="Hello" count=3passes values into the include.with title={ feature.title }passes a value already in scope — including whole objects.- Includes inside a loop inherit the loop value automatically.
Loops
@loop <things> into <thing> is the only loop. The body is normal Markdown (or more directives) and repeats once per row:
@loop /features.json into card
@include /feature-card.wd
@endloop
The source decides how the loop behaves:
- A JSON file path (
/features.json,./data.json) unrolls at build time — pure static HTML. - An in-scope value (an include argument or an outer loop value) also unrolls at build time.
- A
:statelist compiles to a reactive region the runtime patches by key.
Loops nest, and { card.title } style dotted paths reach into each row.
Interpolation
One syntax everywhere: { name } or { name.path }.
- If the name is a static value in scope (include argument, loop value), it is resolved at build time.
- If the name is declared
:state, it becomes a live binding. - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
Sections
::: section #id .class opens a container and ::: closes it. Sections scope state: a :state declared inside a section belongs to that section, so two sections can both declare count without colliding. Bindings and buttons resolve to the nearest scope.
Reactive directives
Reactive pages opt into /__wd/runtime.js (~2 KB gzipped, under a CI-enforced 5 KB budget). Static pages do not.
:state count = 0
The count is { count }.
:button "Increment" -> count++
:if count
Count has changed.
:else
Count is still zero.
:endif
:state todos = [{"id": 1, "title": "Route pages"}]
@loop todos into todo
- { todo.title }
@endloop
:button "Add" -> todos += {"id": 2, "title": "Live compile"}
Directive actions are intentionally narrow and compile-time checked. Arbitrary JavaScript belongs in a colocated .js file.
Data, forms, and persistence
:fetch name from "url"declares state and fills it from JSON over the network. Shelf.jsonfiles are served at/__wd/data/.:form into namecaptures submits straight into state — no backend.:form action="/url"emits a plain native form with zero JS instead.:input field placeholder="…" requiredand:submit "Label"build the form body.:state cart = [] persistsurvives reloads via localStorage.:if item.pathworks inside reactive loops for per-row branches, and conditionals nest — an inner:ifresolves after the outer branch, staying reactive.- Reactive pages expose
window.wd(get,set,state,render) so colocated.jscan do anything directives can't. Section state is addressed assectionId:name.
See it all live on the Data & Forms page.
Colocation
- A matching
.skinfile attaches CSS to the page (indentation-based, compiles to real CSS). - A matching
.jsfile attaches page behavior. - Both work for included fragments too, by basename.
Spec status
The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. :fetch, :form (including server round-trips), :computed, and persist are all shipped and live — try them on the Data & Forms page. Still on the roadmap: a first-party server runtime (site/api/), HTML-fragment swaps, and nested :if over loop items. See docs/spec-alignment.md in the package for the full audit.