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

Routing rules

Includes

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:

Loops nest, and { card.title } style dotted paths reach into each row.

Filtering with where

Add where <predicate> to filter a loop. Conditions join with and / or:

@loop /products.json into p where p.featured == true and p.price < 80
- { p.name }
@endloop

Operators are == != < <= > >= and contains (case-insensitive substring). Operands are item fields, declared :state, numbers, or "strings" — a validated whitelist, never arbitrary code.

The predicate decides reactivity exactly like the source does: read only the row and the filter runs at build time (zero JS); read a :state value and the loop re-filters live. Pair it with :bind for a search box in pure Markdown:

:state products = [{"id":1,"name":"Aurora Lamp"},{"id":2,"name":"Briza Fan"}]
:state q = ""

:bind q placeholder="Search"

@loop products into p where p.name contains q
- { p.name }
@endloop

:bind <state> renders an <input> wired two-way to a :state value — type and the list filters as you go.

Editable lists with per-row actions

A :button inside a reactive loop can act on its own row — the basis for carts and to-do lists:

@loop products into product
:button "Add to cart" -> cart += product
:::
@loop cart into line
:button "Remove" -> cart remove line
:::

cart += product appends a copy of the current row to another :state list; cart remove line removes the current row from the list being looped. Both are checked against the enclosing loop at compile time — no JavaScript.

Interpolation

One syntax everywhere: { name } or { name.path }.

Frontmatter

Frontmatter sits between --- fences. Values are strings, plus inline arrays:

---
title: Customers
tags: [sales, revenue, "q1, q2"]
---

{ meta.title } prints a scalar and { meta.tags } joins an array with , . You can loop a frontmatter array at build time — @loop meta.tags into tag — and the page stays static. Arrays are inline flow only ([a, b]); quoted items keep their commas.

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

See it all live on the Data & Forms page.

Colocation

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.