---
title: Darkmown: Markdown that runs
description: A .md file is a page. Rename it to .wd and the same file gets loops, state, fetched data, and forms, with no components, no JSX, and no build step. Pages with no state ship zero framework JavaScript.
image: https://darkmown.com/__wd/media/og.png
site_url: https://darkmown.com
md_source: true
transitions: true
schema: WebSite
---

@include /nav.wd
@include /base.wd

::: main

::: hero

# Markdown that *runs.*

A `.md` file is already a page. Rename it to `.wd` and that same file gets loops, state, fetched data, and forms. No components, no JSX, no config. Pages with no state ship **zero** framework JavaScript.

```sh
npx @zvndev/darkmown init my-site
```

::: hero-cta
[Read the docs](/docs/){.btn .primary}
[Try it in your browser](/playground/){.btn}
[Star on GitHub](https://github.com/ZVN-DEV/darkmown){.btn}
:::

:::

## One file. Here is the source.

A store with a live search filter, add-to-cart, and a running total.

::: pair

::: pairlabel
`store.wd`, the whole file
:::

```wd
:state products = [
  { "id": 1, "name": "Aurora Lamp", "price": 49 },
  { "id": 2, "name": "Briza Fan", "price": 39 },
  { "id": 3, "name": "Cove Speaker", "price": 89 },
  { "id": 4, "name": "Dune Mug", "price": 12 }
]
:state cart = []
:state q = ""
:computed count = cart.length

:bind q placeholder="Search products…"

@loop products into p where p.name contains q
::: card .product
**{ p.name }** · ${ p.price }
:button "Add" -> cart += p
:::
@endloop

Cart: { count } item(s)
```

::: pairlabel
that same file, running (type in the box)
:::

::: demo
:state products = [
  { "id": 1, "name": "Aurora Lamp", "price": 49 },
  { "id": 2, "name": "Briza Fan", "price": 39 },
  { "id": 3, "name": "Cove Speaker", "price": 89 },
  { "id": 4, "name": "Dune Mug", "price": 12 }
]
:state cart = []
:state q = ""
:computed count = cart.length

:bind q placeholder="Search products…"

@loop products into p where p.name contains q
::: card .product
**{ p.name }** · ${ p.price }
:button "Add" -> cart += p
:::
@endloop

Cart: **{ count }** item(s)
:::

:::

## This page is built in Darkmown

Not a screenshot, and not a React site describing a Markdown tool. `site/pages/index.wd` is a `.wd` file with no raw HTML in it. The first two numbers below are live: a `:fetch` reading `api/stats.js`, a serverless function in this same repo.

:fetch stats from "/api/stats/" timeout=6000

::: livestrip

::: stat
:if stats_loading
·
:endif
:if stats_error
n/a
:endif
:if stats
{ stats.version }
:endif
::: statlabel
latest release, live from npm
:::
:::

::: stat
:if stats_loading
·
:endif
:if stats_error
n/a
:endif
:if stats
{ stats.downloads }
:endif
::: statlabel
npm installs last month
:::
:::

::: stat
7,654 B
::: statlabel
the whole runtime, gzipped
:::
:::

::: stat
0 KB
::: statlabel
JS on a static page
:::
:::

:::

Say hello and the form round-trips to a real serverless function, then renders the reply:

:form action="/api/echo/" into reply
:input note placeholder="What would you build with this?"
:submit "Send it"
:endform

:if reply
The server answered at **{ reply.at }**. That was a `:form action=`. With JavaScript off it degrades to a plain native POST.
:else if reply_error
The echo endpoint is only live on the deployed site: { reply_error }
:endif

## Three rules, no escape hatch

- **Zero-JS static.** A page with no state ships no framework JavaScript. Open [the docs](/docs/) or [a plain `.md` page](/markdown/) and check the network tab.
- **One runtime.** Every reactive page shares the same 6,584-byte gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that grows with your UI. It runs under a strict CSP with no `unsafe-eval`.
- **One loop, one binding.** `@loop … into …` is the only loop. `{ name }` is the only interpolation. There is no second way to do either.

## A whole framework, in text

- **Routing.** Folders are routes. Drop a file in, it is a page. Prefix it with `.` or `-` to hide it.
- **State and reactivity.** `:state`, `:button`, `:if`, and keyed `@loop`s patch the DOM directly. No virtual DOM.
- **Server calls.** `:fetch` pulls JSON into state; `:form action=` round-trips to any backend and degrades to a native POST without JS.
- **Composition.** `@include` embeds one Markdown file into another, passing values down.
- **Styling, optional.** Colocate a `.skin` file for design tokens, or ship none and let the browser default.
- **Errors that teach.** Every compile error carries a stable code, the `file:line`, and a corrective example that is guaranteed to compile.

## See it for real

:try "A whole live app in one file: fetch, loops, nested ifs, a form" href="/app/"
:try "A filter that lives in the URL, so the view is shareable" href="/url-state/"
:try "A link whose destination follows state as you click" href="/attr-binding/"
:try "The docs, a .wd page built from includes" href="/docs/"
:try "A plain .md page, where directives stay inert text" href="/markdown/"

`npm run build` emits a plain static `dist/` (no server, no special runtime), so it deploys anywhere that serves files. This site is a Darkmown build.

::: hero-cta
[Deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown){.btn .primary}
[Read the docs](/docs/){.btn}
[How it compares to Markdoc](/vs/markdoc/){.btn}
[Benchmarks vs. a real Next.js site](/benchmarks/){.btn}
:::

:::

@include /footer.wd
