Darkmown vs. Markdoc

Markdown that runs. Not Markdown that needs React.

The left file is Markdown. It is also a running app — a live search filter, add-to-cart, and a running total. The right file is the closest Markdoc can get on its own: a static list. To make it do what the left one already does, you add a React component, hydration, and a build step.

See a whole app in one file Read the docs

Darkmownstore.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)

↓ and here is that exact file, running

Aurora Lamp — $49

Briza Fan — $39

Cove Speaker — $89

Dune Mug — $12

Cart: 0 item(s)

This is the whole file. It runs.

Markdocstore.md
{% if $products %}
- **Aurora Lamp** — $49
- **Briza Fan** — $39
- **Cove Speaker** — $89
- **Dune Mug** — $12
{% /if %}

Markdoc has {% if %}, {% partial %}, and $variables — but no loop tag, so each row is written by hand and the list is static. To filter, add to a cart, or total it, you reach for code:

+ store-widget.jsx — a React component, registered as a custom tag:

const tags = { store: { render: 'Store' } };
Markdoc.renderers.react(content, React, {
  components: { Store } // your filter + cart + total live here
});

…plus a React + hydration runtime shipped to the browser, and the build step that wires the tag to the component.

Same outcome needs a React component.

Three rules, no escape hatch

A whole framework, in text

Everything a site needs, expressed as Markdown directives. No components, no config, no JSX, no special files:

What to try

TryA whole live app in one .wd file: fetch, loops, nested ifs, a form TryThe docs route — a .wd page built from includes TryA plain .md page where directives stay inert text TryReactive page: state, keyed loops, scoped sections TryData page: fetch, forms, server round-trips, persistence TryA hidden route, to confirm it 404s

Loops that read like prose

@loop is the only loop. Point it at a JSON file, an in-scope value, or a :state list, and the body repeats once per row. Includes inside the loop inherit the loop value:

@loop /features.json into card
@include /feature-card.wd
@endloop

Folder routing without config

Pages come from site/pages, and hidden work is just dot or minus prefixed.

One loop, @loop

Loop a JSON file, an in-scope value, or a :state list — includes inside inherit the loop value.

Colocated behavior

A matching .skin or .js file beside the page is picked up automatically.

Colocated behavior

This button is powered by site/pages/index.js, discovered by matching the page basename:

Deploy in one click

npm run build emits a plain static dist/ — no server, no special runtime — so it deploys anywhere that serves files. This very site is a Darkmown build. The demo form posts to Darkmown's dev/Vercel echo endpoint; on static-only hosts, point action= at your own backend or remove that demo.

Deploy to Vercel Deploy static to Cloudflare Star on GitHub