@zvndev/darkmown 2.3.1-next.2.ga0699da → 2.3.1-next.4.g92bbead

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "2.3.1-next.2.ga0699da",
3
+ "version": "2.3.1-next.4.g92bbead",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/site/_/base.skin CHANGED
@@ -484,6 +484,14 @@ code
484
484
  color $accent-deep
485
485
  padding .12rem .36rem
486
486
  radius 6px
487
+ /* An inline token that wraps (`unsafe-eval` breaks at its hyphen) otherwise
488
+ leaves a half-open pill on each line: padding and rounding on the outer
489
+ edges only. `clone` gives every fragment the full box, so a wrapped token
490
+ reads as one styled run instead of a rendering artifact. Wrapping itself
491
+ is left alone on purpose: some inline tokens on this site run to 70+
492
+ characters, and `nowrap` would overflow the viewport on a phone. */
493
+ box-decoration-break clone
494
+ -webkit-box-decoration-break clone
487
495
 
488
496
  h1 code, h2 code, h3 code
489
497
  font-size .82em
package/site/_/footer.wd CHANGED
@@ -5,7 +5,7 @@ html: true
5
5
  <footer class="sitefooter">
6
6
  <div class="foot-inner">
7
7
  <div>
8
- <strong>Darkmown</strong> — Markdown that runs.
8
+ <strong>Darkmown</strong>. Markdown that runs.
9
9
  <div class="small">Zero JS on static pages · one ~7.7&nbsp;KB runtime when reactive.</div>
10
10
  </div>
11
11
  <nav class="foot-links">
@@ -1,29 +1,34 @@
1
- const copyBtn = document.querySelector("[data-copy-cmd]");
1
+ // Colocated behavior for the landing page, discovered by basename
2
+ // (site/pages/index.wd -> site/pages/index.js). It is the only hand-written
3
+ // JavaScript on this page; everything else is directives.
4
+ //
5
+ // The page authors its install command as a plain ```sh fence so the source
6
+ // stays free of raw HTML. The copy button is added here instead: progressive
7
+ // enhancement, so the command is still readable and selectable with JS off.
2
8
 
3
- if (copyBtn) {
4
- copyBtn.addEventListener("click", async () => {
5
- const cmd = copyBtn.previousElementSibling?.textContent ?? "";
9
+ const installBlock = document.querySelector(".hero pre > code");
10
+
11
+ if (installBlock && navigator.clipboard) {
12
+ const pre = installBlock.parentElement;
13
+ const button = document.createElement("button");
14
+ button.type = "button";
15
+ button.className = "copy-cmd";
16
+ button.textContent = "Copy";
17
+ button.setAttribute("aria-label", "Copy install command");
18
+ pre.classList.add("has-copy");
19
+ pre.append(button);
20
+
21
+ button.addEventListener("click", async () => {
6
22
  try {
7
- await navigator.clipboard.writeText(cmd);
8
- copyBtn.textContent = "Copied";
9
- copyBtn.classList.add("is-copied");
23
+ await navigator.clipboard.writeText(installBlock.textContent.trim());
24
+ button.textContent = "Copied";
25
+ button.classList.add("is-copied");
10
26
  setTimeout(() => {
11
- copyBtn.textContent = "Copy";
12
- copyBtn.classList.remove("is-copied");
27
+ button.textContent = "Copy";
28
+ button.classList.remove("is-copied");
13
29
  }, 1600);
14
30
  } catch {
15
- copyBtn.textContent = "Select + ⌘C";
31
+ button.textContent = "Select + ⌘C";
16
32
  }
17
33
  });
18
34
  }
19
-
20
- const button = document.querySelector("[data-wd-demo]");
21
-
22
- if (button) {
23
- button.addEventListener("click", () => {
24
- button.classList.toggle("is-on");
25
- button.textContent = button.classList.contains("is-on")
26
- ? "Colocated index.js is active"
27
- : "Script is waiting";
28
- });
29
- }
@@ -1,5 +1,9 @@
1
- /* Landing page — layers on /base.wd (tokens, type, components, dark mode).
2
- Only landing-specific layout lives here. */
1
+ /* Landing page. Layers on /base.wd (tokens, type, components, dark mode).
2
+ Only landing-specific layout lives here.
3
+
4
+ The page itself contains no raw HTML: every block below is produced by a
5
+ `::: name` container, a markdown element, or a directive. Class names match
6
+ the container names in site/pages/index.wd. */
3
7
 
4
8
  /* ── Hero ───────────────────────────────────────────────────────── */
5
9
 
@@ -9,173 +13,105 @@
9
13
  margin-bottom 2.5rem
10
14
 
11
15
  .hero h1
12
- max-width 16ch
16
+ max-width 15ch
17
+ font-size clamp(2.4rem, 6vw, 3.6rem)
13
18
 
14
19
  .hero h1 em
15
20
  font-style italic
16
21
  color $accent
17
22
 
18
23
  .hero > p
19
- max-width 64ch
24
+ max-width 60ch
20
25
  font-size 1.18rem
21
26
  color $ink-soft
22
27
  margin 1rem 0 1.75rem
23
28
 
24
- .hero .eyebrow
25
- background $accent-soft
26
- padding .3rem .7rem
27
- radius 999px
28
-
29
- .hero-cta
30
- display flex
31
- flex-wrap wrap
32
- margin-bottom 1.5rem
33
-
34
- .hero-install
35
- display inline-flex
36
- align-items center
37
- gap .55rem
38
- max-width 100%
39
- margin 0 0 2rem
40
- padding .7rem 1rem
29
+ /* The install command is a plain ```sh fence; index.js appends the copy
30
+ button, so the markup stays directive-only and works with JS off. */
31
+ .hero pre
32
+ position relative
33
+ max-width 34rem
34
+ margin 0 0 1.75rem
35
+ padding .85rem 1rem
41
36
  border 1px solid $line
42
37
  radius 10px
43
38
  bg $sunk
44
- font-family ui-monospace, SFMono-Regular, Menlo, monospace
45
- overflow-x auto
46
39
 
47
- .hero-install::before
48
- content "$"
49
- color $accent
50
- font-weight 700
51
- flex-shrink 0
52
-
53
- .hero-install code
54
- font-family inherit
40
+ .hero pre code
55
41
  font-size .95rem
56
42
  color $ink
57
43
  white-space nowrap
58
44
 
59
- .hero-install .copy-cmd
60
- flex-shrink 0
61
- margin-left .35rem
62
- padding .25rem .65rem
45
+ .hero pre.has-copy
46
+ padding-right 5.5rem
47
+
48
+ .hero pre .copy-cmd
49
+ position absolute
50
+ top 50%
51
+ right .6rem
52
+ transform translateY(-50%)
53
+ padding .3rem .7rem
63
54
  border 1px solid $line
64
55
  radius 7px
65
- bg transparent
56
+ bg $panel
66
57
  color $ink-soft
67
58
  font-family inherit
68
59
  font-size .8rem
69
60
  cursor pointer
70
61
 
71
- .hero-install .copy-cmd:hover
62
+ .hero pre .copy-cmd:hover
72
63
  color $ink
73
64
  border-color $accent
74
65
 
75
- .hero-install .copy-cmd.is-copied
66
+ .hero pre .copy-cmd.is-copied
76
67
  color $accent
77
68
  border-color $accent
78
69
 
79
- .hero-stats
80
- display grid
81
- grid-template-columns repeat(4, auto)
82
- gap 1.25rem 2.5rem
83
- list-style none
84
- padding 0
85
- margin 0
86
- border-top 1px solid $line-soft
87
- padding-top 1.5rem
88
-
89
- .hero-stats li
70
+ .hero-cta
90
71
  display flex
91
- flex-direction column
92
-
93
- .hero-stats strong
94
- font-size 1.6rem
95
- color $accent
96
- letter-spacing -.02em
97
-
98
- .hero-stats span
99
- font-size .82rem
100
- color $muted
101
- max-width 18ch
102
-
103
- @media (max-width: 720px)
104
- .hero-stats
105
- grid-template-columns 1fr 1fr
106
-
107
- /* ── vs-Markdoc comparison ──────────────────────────────────────── */
72
+ flex-wrap wrap
73
+ gap .6rem
74
+ margin 0
108
75
 
109
- .versus
110
- display grid
111
- grid-template-columns 1fr 1fr
112
- gap 1.25rem
113
- align-items start
114
- margin 2.5rem 0 1rem
76
+ /* ── Source + the same file running, as one paired module ───────── */
115
77
 
116
- .vs-card
78
+ /* The source block and the live demo are deliberately the same width and sit
79
+ inside one bordered card, so the "this text produced that UI" claim reads as
80
+ a single before/after unit rather than two unrelated page chunks. */
81
+ .pair
82
+ max-width 46rem
83
+ margin 1.25rem 0 2.5rem
84
+ padding 1.25rem
117
85
  border 1px solid $line
118
86
  radius $radius
119
87
  bg $panel
120
- padding 1.35rem
121
- display flex
122
- flex-direction column
123
- min-width 0
124
- box-shadow 0 1px 2px rgba(28,24,20,.04)
125
-
126
- .vs-card.vs-dark
127
- background linear-gradient(180deg, $accent-soft, $panel)
128
- border 1px solid $accent
129
88
 
130
- .vs-head
131
- display flex
132
- align-items baseline
133
- justify-content space-between
134
- gap .75rem
135
- margin-bottom .85rem
136
-
137
- .vs-tag
138
- font-family ui-monospace, SFMono-Regular, Menlo, monospace
139
- font-size .72rem
140
- font-weight 700
89
+ .pairlabel
90
+ margin 0 0 .5rem
91
+ font-size .78rem
141
92
  text-transform uppercase
142
- letter-spacing .08em
143
- padding .25rem .55rem
144
- radius 999px
145
-
146
- .vs-tag-wd
147
- color white
148
- bg $accent
149
-
150
- .vs-tag-md
93
+ letter-spacing .07em
151
94
  color $muted
152
- bg $sunk
153
- border 1px solid $line
154
95
 
155
- .vs-sub
156
- font-family ui-monospace, SFMono-Regular, Menlo, monospace
157
- font-size .82rem
158
- color $muted
96
+ .pairlabel code
97
+ font-size .78rem
98
+ text-transform none
99
+ letter-spacing 0
100
+ color $ink
159
101
 
160
- .vs-card pre
161
- margin .25rem 0 0
162
- font-size .8rem
163
- line-height 1.55
102
+ .pair > pre
103
+ margin 0 0 1.5rem
164
104
 
165
- .vs-live-label
166
- margin 1rem 0 .35rem
167
- font-size .88rem
168
- color $muted
169
- font-style italic
105
+ .pair .pairlabel + .demo
106
+ margin 0
170
107
 
171
- .vs-live
172
- border 1px dashed $accent
108
+ .demo
109
+ border 1px solid $accent
173
110
  radius $radius-sm
174
- background color-mix(in srgb, $accent-soft 50%, $panel)
175
- padding 1rem 1.1rem .85rem
176
- margin .15rem 0 0
111
+ background color-mix(in srgb, $accent-soft 45%, $panel)
112
+ padding 1.15rem 1.25rem 1rem
177
113
 
178
- .vs-live input[data-wd-bind-input]
114
+ .demo input[data-wd-bind-input]
179
115
  width 100%
180
116
  box-sizing border-box
181
117
  font-family inherit
@@ -187,7 +123,7 @@
187
123
  color $ink
188
124
  margin 0 0 .75rem
189
125
 
190
- .vs-live .product
126
+ .demo .product
191
127
  display flex
192
128
  align-items center
193
129
  justify-content space-between
@@ -199,10 +135,10 @@
199
135
  radius 8px
200
136
  font-size .95rem
201
137
 
202
- .vs-live .product p
138
+ .demo .product p
203
139
  margin 0
204
140
 
205
- .vs-live .product button
141
+ .demo .product button
206
142
  border 0
207
143
  bg $accent
208
144
  color white
@@ -213,57 +149,68 @@
213
149
  cursor pointer
214
150
  flex-shrink 0
215
151
 
216
- .vs-live .product button:hover
152
+ .demo .product button:hover
217
153
  bg $accent-deep
218
154
 
219
- .vs-live > p:last-of-type
155
+ .demo > p:last-of-type
220
156
  margin .85rem 0 0
221
157
  font-size .95rem
222
158
 
223
- .vs-needs
224
- margin 1rem 0 0
225
- padding 1rem 1.1rem
226
- border-left 3px solid $warm
227
- bg $warm-soft
228
- radius 8px
159
+ /* ── Live data strip (:fetch stars + visits) ────────────────────── */
229
160
 
230
- .vs-needs strong
231
- color $warm
232
- font-size .92rem
161
+ .livestrip
162
+ display grid
163
+ grid-template-columns repeat(4, 1fr)
164
+ gap 1rem
165
+ margin 1.5rem 0 2rem
166
+ padding 1.35rem 0
167
+ border-top 1px solid $line-soft
168
+ border-bottom 1px solid $line-soft
169
+
170
+ /* Every stat is structurally identical: the value is a bare <p> (either plain
171
+ prose or the body of an `:if` region), the label is its own `.statlabel`
172
+ container. Keeping the label in a container is what stops markdown from
173
+ folding value and label into one paragraph, which is exactly what happened
174
+ when they were two adjacent lines. */
175
+ .stat
176
+ display flex
177
+ flex-direction column
178
+ gap .2rem
179
+ min-width 0
233
180
 
234
- .vs-needs pre
235
- margin .7rem 0 .6rem
236
- font-size .78rem
181
+ .stat > p
182
+ margin 0
183
+ font-size 1.85rem
184
+ line-height 1.05
185
+ color $accent
186
+ letter-spacing -.02em
187
+ font-variant-numeric tabular-nums
237
188
 
238
- .vs-needs-foot
239
- display block
240
- font-size .85rem
241
- color $muted
189
+ /* The `:if` lifecycle regions wrap the value; unwrap them for layout so the
190
+ value sits on the same baseline whether it is static or fetched. */
191
+ .stat > div[data-wd-if] p
192
+ margin 0
193
+ font-size 1.85rem
194
+ line-height 1.05
195
+ color $accent
196
+ letter-spacing -.02em
197
+ font-variant-numeric tabular-nums
242
198
 
243
- .vs-caption
244
- margin auto 0 0
245
- padding-top 1rem
246
- font-style italic
247
- color $ink
248
- font-size .95rem
199
+ .statlabel
200
+ font-size .88rem
201
+ line-height 1.35
202
+ color $ink-soft
203
+ max-width 20ch
249
204
 
250
- .vs-card.vs-dark .vs-caption
251
- color $accent-deep
205
+ .statlabel p
206
+ margin 0
252
207
 
253
208
  @media (max-width: 720px)
254
- .versus
255
- grid-template-columns 1fr
256
- gap 1rem
257
-
258
- /* ── Secondary value props ──────────────────────────────────────── */
259
-
260
- .props
261
- margin 1.5rem 0 0
262
-
263
- .props h2
264
- margin-top 1.5rem
209
+ .livestrip
210
+ grid-template-columns 1fr 1fr
211
+ gap 1.25rem
265
212
 
266
- /* ── Demo-directive widgets (`:try`, feature-card include, index.js) ─ */
213
+ /* ── Demo-directive widgets (`:try`) ────────────────────────────── */
267
214
 
268
215
  .try-card
269
216
  display inline-flex
@@ -286,44 +233,3 @@
286
233
  padding .1rem .5rem
287
234
  radius 999px
288
235
  font-size .8rem
289
-
290
- .feature-card
291
- display grid
292
- gap .35rem
293
- margin 1rem 0
294
- padding 1.1rem 1.25rem
295
- bg $panel
296
- border 1px solid $line
297
- radius $radius
298
-
299
- .feature-card strong
300
- color $warm
301
-
302
- .pulse-button
303
- border 0
304
- bg $ink
305
- color $paper
306
- padding .85rem 1.15rem
307
- radius 999px
308
- font-family inherit
309
- cursor pointer
310
-
311
- .pulse-button.is-on
312
- bg $warm
313
-
314
- .sprint-board
315
- display grid
316
- grid-template-columns repeat(auto-fit, minmax(130px, 1fr))
317
- gap 1rem
318
- margin 1rem 0
319
-
320
- .sprint-board article
321
- bg $panel
322
- border 1px solid $line
323
- padding 1rem
324
- radius $radius
325
-
326
- .sprint-board span
327
- display block
328
- color $accent
329
- font-size .9rem
@@ -1,7 +1,6 @@
1
1
  ---
2
- html: true
3
- title: Darkmown — Markdown that runs, not Markdown that needs React
4
- description: A live store widget — search filter, add-to-cart, running total — written as one Markdown file that runs. The same outcome in Markdoc needs a React component, hydration, and a build step. Compare them side by side.
2
+ title: Darkmown: Markdown that runs
3
+ 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.
5
4
  image: https://darkmown.com/__wd/media/og.png
6
5
  site_url: https://darkmown.com
7
6
  transitions: true
@@ -10,39 +9,43 @@ transitions: true
10
9
  @include /nav.wd
11
10
  @include /base.wd
12
11
 
13
- <main>
12
+ ::: main
14
13
 
15
- <section class="hero">
16
- <span class="eyebrow">Darkmown vs. Markdoc</span>
14
+ ::: hero
17
15
 
18
- # Markdown that *runs.* Not Markdown that needs React.
16
+ # Markdown that *runs.*
19
17
 
20
- 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.
18
+ 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.
21
19
 
22
- <div class="hero-cta">
23
- <a class="btn primary" href="/showcase/">See the showcase</a>
24
- <a class="btn" href="/docs/">Read the docs</a>
25
- </div>
20
+ ```sh
21
+ npx @zvndev/darkmown init my-site
22
+ ```
26
23
 
27
- <div class="hero-install"><code>npx @zvndev/darkmown init my-site</code><button type="button" class="copy-cmd" data-copy-cmd aria-label="Copy install command">Copy</button></div>
24
+ ::: hero-cta
25
+ [Read the docs](/docs/){.btn .primary}
26
+ [Try it in your browser](/playground/){.btn}
27
+ [Star on GitHub](https://github.com/ZVN-DEV/darkmown){.btn}
28
+ :::
28
29
 
29
- <p class="hero-try"><a href="/playground/">or try it in your browser →</a></p>
30
+ :::
30
31
 
31
- <ul class="hero-stats">
32
- <li><strong>0 KB</strong><span>framework JS on static pages</span></li>
33
- <li><strong>~7.7 KB</strong><span>one runtime, CI-enforced &lt; 8 KB</span></li>
34
- <li><strong>1</strong><span>loop and one binding to learn</span></li>
35
- <li><strong>MIT</strong><span>zero runtime dependencies</span></li>
36
- </ul>
37
- </section>
32
+ ## One file. Here is the source.
38
33
 
39
- <section class="versus">
34
+ A store with a live search filter, add-to-cart, and a running total.
40
35
 
41
- ::: div .vs-card .vs-dark
42
- <header class="vs-head"><span class="vs-tag vs-tag-wd">Darkmown</span><span class="vs-sub">store.wd</span></header>
36
+ ::: pair
37
+
38
+ ::: pairlabel
39
+ `store.wd`, the whole file
40
+ :::
43
41
 
44
42
  ```wd
45
- :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}]
43
+ :state products = [
44
+ { "id": 1, "name": "Aurora Lamp", "price": 49 },
45
+ { "id": 2, "name": "Briza Fan", "price": 39 },
46
+ { "id": 3, "name": "Cove Speaker", "price": 89 },
47
+ { "id": 4, "name": "Dune Mug", "price": 12 }
48
+ ]
46
49
  :state cart = []
47
50
  :state q = ""
48
51
  :computed count = cart.length
@@ -51,7 +54,7 @@ The left file is Markdown. It is also a running app — a live search filter, ad
51
54
 
52
55
  @loop products into p where p.name contains q
53
56
  ::: card .product
54
- **{ p.name }** — ${ p.price }
57
+ **{ p.name }** · ${ p.price }
55
58
  :button "Add" -> cart += p
56
59
  :::
57
60
  @endloop
@@ -59,10 +62,17 @@ The left file is Markdown. It is also a running app — a live search filter, ad
59
62
  Cart: { count } item(s)
60
63
  ```
61
64
 
62
- <p class="vs-live-label">↓ and here is that exact file, running</p>
65
+ ::: pairlabel
66
+ that same file, running (type in the box)
67
+ :::
63
68
 
64
- ::: div .vs-live
65
- :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}]
69
+ ::: demo
70
+ :state products = [
71
+ { "id": 1, "name": "Aurora Lamp", "price": 49 },
72
+ { "id": 2, "name": "Briza Fan", "price": 39 },
73
+ { "id": 3, "name": "Cove Speaker", "price": 89 },
74
+ { "id": 4, "name": "Dune Mug", "price": 12 }
75
+ ]
66
76
  :state cart = []
67
77
  :state q = ""
68
78
  :computed count = cart.length
@@ -71,7 +81,7 @@ Cart: { count } item(s)
71
81
 
72
82
  @loop products into p where p.name contains q
73
83
  ::: card .product
74
- **{ p.name }** — ${ p.price }
84
+ **{ p.name }** · ${ p.price }
75
85
  :button "Add" -> cart += p
76
86
  :::
77
87
  @endloop
@@ -79,102 +89,104 @@ Cart: { count } item(s)
79
89
  Cart: **{ count }** item(s)
80
90
  :::
81
91
 
82
- <p class="vs-caption">This is the whole file. It runs.</p>
83
92
  :::
84
93
 
85
- ::: div .vs-card
86
- <header class="vs-head"><span class="vs-tag vs-tag-md">Markdoc</span><span class="vs-sub">store.md</span></header>
87
-
88
- ```text
89
- {% if $products %}
90
- - **Aurora Lamp** — $49
91
- - **Briza Fan** — $39
92
- - **Cove Speaker** — $89
93
- - **Dune Mug** — $12
94
- {% /if %}
95
- ```
94
+ ## This page is built in Darkmown
96
95
 
97
- <p class="vs-live-label">Markdoc has <code>{% if %}</code>, <code>{% partial %}</code>, and <code>$variables</code> — 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:</p>
96
+ 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.
98
97
 
99
- ::: div .vs-needs
100
- <strong>+ store-widget.jsx</strong> — a React component, registered as a custom tag:
98
+ :fetch stats from "/api/stats" timeout=6000
101
99
 
102
- ```jsx
103
- const tags = { store: { render: 'Store' } };
104
- Markdoc.renderers.react(content, React, {
105
- components: { Store } // your filter + cart + total live here
106
- });
107
- ```
100
+ ::: livestrip
108
101
 
109
- <span class="vs-needs-foot">…plus a React + hydration runtime shipped to the browser, and the build step that wires the tag to the component.</span>
102
+ ::: stat
103
+ :if stats_loading
104
+ ·
105
+ :endif
106
+ :if stats_error
107
+ n/a
108
+ :endif
109
+ :if stats
110
+ { stats.version }
111
+ :endif
112
+ ::: statlabel
113
+ latest release, live from npm
110
114
  :::
111
-
112
- <p class="vs-caption">Same outcome needs a React component.</p>
113
115
  :::
114
116
 
115
- </section>
116
-
117
- :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~7.7 KB gzipped runtime loads (CI-enforced under 8 KB), never before. JS optional, CSS optional, always."
118
-
119
- <section class="props">
120
-
121
- ## Three rules, no escape hatch
122
-
123
- - **Zero-JS static.** A page with no state ships no framework JavaScript. `/markdown/` and `/docs/` prove it — open them and check the network tab.
124
- - **One runtime.** Every reactive page shares the same ~7.7 KB gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that scales with your UI.
125
- - **One loop, one binding.** `@loop … into …` is the only loop; `{ name }` is the only interpolation. Nothing else to learn, nothing else to misuse.
126
-
127
- </section>
128
-
129
- ## A whole framework, in text
117
+ ::: stat
118
+ :if stats_loading
119
+ ·
120
+ :endif
121
+ :if stats_error
122
+ n/a
123
+ :endif
124
+ :if stats
125
+ { stats.downloads }
126
+ :endif
127
+ ::: statlabel
128
+ npm installs last month
129
+ :::
130
+ :::
130
131
 
131
- Everything a site needs, expressed as Markdown directives. No components, no config, no JSX, no special files:
132
+ ::: stat
133
+ 7,654 B
134
+ ::: statlabel
135
+ the whole runtime, gzipped
136
+ :::
137
+ :::
132
138
 
133
- - **Routing** — folders are routes. Drop a file in, it's a page. Prefix with `.` or `-` to hide it.
134
- - **State & reactivity** — `:state`, `:button`, `:if`, and keyed `@loop`s patch the DOM directly. No virtual DOM.
135
- - **Server calls** — `:fetch` pulls JSON into state; `:form action=` round-trips to any backend and degrades to a native POST without JS.
136
- - **Composition** — `@include` embeds one Markdown file into another, passing values down Liquid-style.
137
- - **Styling, optional** — colocate a `.skin` file for design tokens, or ship none and let the browser default.
139
+ ::: stat
140
+ 0 KB
141
+ ::: statlabel
142
+ JS on a static page
143
+ :::
144
+ :::
138
145
 
139
- ## What to try
146
+ :::
140
147
 
141
- :try "A whole live app in one .wd file: fetch, loops, nested ifs, a form" href="/app/"
142
- :try "The docs route — a .wd page built from includes" href="/docs/"
143
- :try "A plain .md page where directives stay inert text" href="/markdown/"
144
- :try "Reactive page: state, keyed loops, scoped sections" href="/reactive/"
145
- :try "Data page: fetch, forms, server round-trips, persistence" href="/data/"
146
- :try "A hidden route, to confirm it 404s" href="/-draft/"
148
+ Say hello and the form round-trips to a real serverless function, then renders the reply:
147
149
 
148
- ## Loops that read like prose
150
+ :form action="/api/echo" into reply
151
+ :input note placeholder="What would you build with this?"
152
+ :submit "Send it"
153
+ :endform
149
154
 
150
- `@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:
155
+ :if reply
156
+ The server answered at **{ reply.at }**. That was a `:form action=`. With JavaScript off it degrades to a plain native POST.
157
+ :else if reply_error
158
+ The echo endpoint is only live on the deployed site: { reply_error }
159
+ :endif
151
160
 
152
- ```wd
153
- @loop /features.json into card
154
- @include /feature-card.wd
155
- @endloop
156
- ```
157
-
158
- @loop /features.json into card
159
- @include /feature-card.wd
160
- @endloop
161
+ ## Three rules, no escape hatch
161
162
 
162
- ## Colocated behavior
163
+ - **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.
164
+ - **One runtime.** Every reactive page shares the same 7,654-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`.
165
+ - **One loop, one binding.** `@loop … into …` is the only loop. `{ name }` is the only interpolation. There is no second way to do either.
163
166
 
164
- This button is powered by `site/pages/index.js`, discovered by matching the page basename:
167
+ ## A whole framework, in text
165
168
 
166
- <button class="pulse-button" data-wd-demo>Script is waiting</button>
169
+ - **Routing.** Folders are routes. Drop a file in, it is a page. Prefix it with `.` or `-` to hide it.
170
+ - **State and reactivity.** `:state`, `:button`, `:if`, and keyed `@loop`s patch the DOM directly. No virtual DOM.
171
+ - **Server calls.** `:fetch` pulls JSON into state; `:form action=` round-trips to any backend and degrades to a native POST without JS.
172
+ - **Composition.** `@include` embeds one Markdown file into another, passing values down.
173
+ - **Styling, optional.** Colocate a `.skin` file for design tokens, or ship none and let the browser default.
174
+ - **Errors that teach.** Every compile error carries a stable code, the `file:line`, and a corrective example that is guaranteed to compile.
167
175
 
168
- <span id="deploy"></span>
176
+ ## See it for real
169
177
 
170
- ## Deploy in one click
178
+ :try "A whole live app in one file: fetch, loops, nested ifs, a form" href="/app/"
179
+ :try "The docs, a .wd page built from includes" href="/docs/"
180
+ :try "A plain .md page, where directives stay inert text" href="/markdown/"
171
181
 
172
- `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.
182
+ `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.
173
183
 
174
- <a class="btn primary" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown">Deploy to Vercel</a>
175
- <a class="btn" href="https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown">Deploy static to Cloudflare</a>
176
- <a class="btn" href="https://github.com/ZVN-DEV/darkmown">Star on GitHub</a>
184
+ ::: hero-cta
185
+ [Deploy to Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown){.btn .primary}
186
+ [Read the docs](/docs/){.btn}
187
+ [How it compares to Markdoc](/vs/markdoc/){.btn}
188
+ :::
177
189
 
178
- </main>
190
+ :::
179
191
 
180
192
  @include /footer.wd
@@ -0,0 +1,53 @@
1
+ /* The Darkmown vs. Markdoc comparison, moved off the landing page.
2
+ Layers on /base.wd. Class names match the `::: name .class` containers
3
+ in markdoc.wd. The page contains no raw HTML. */
4
+
5
+ .versus
6
+ display grid
7
+ grid-template-columns 1fr 1fr
8
+ gap 1.25rem
9
+ align-items start
10
+ margin 2.75rem 0 2.5rem
11
+
12
+ .vs-card
13
+ border 1px solid $line
14
+ radius $radius
15
+ bg $panel
16
+ padding 1.35rem
17
+ display flex
18
+ flex-direction column
19
+ min-width 0
20
+ box-shadow 0 1px 2px rgba(28,24,20,.04)
21
+
22
+ .vs-card.vs-dark
23
+ background linear-gradient(180deg, $accent-soft, $panel)
24
+ border 1px solid $accent
25
+
26
+ /* First line of each card is the filename, authored as inline code. */
27
+ .vs-card > p:first-child
28
+ margin 0 0 .85rem
29
+ font-size .82rem
30
+ color $muted
31
+
32
+ .vs-card > p:first-child code
33
+ font-size .82rem
34
+ color $ink
35
+
36
+ .vs-card pre
37
+ margin .25rem 0 .75rem
38
+ padding-right 1rem
39
+ font-size .78rem
40
+ line-height 1.55
41
+ overflow-x auto
42
+
43
+ .vs-card p
44
+ font-size .92rem
45
+
46
+ .vs-card > p:last-child
47
+ margin auto 0 0
48
+ padding-top .5rem
49
+
50
+ @media (max-width: 720px)
51
+ .versus
52
+ grid-template-columns 1fr
53
+ gap 1rem
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Darkmown vs. Markdoc: Markdown that runs, without a React component
3
+ description: Markdoc gives Markdown conditionals, partials and variables, but no loop tag and no interactivity. A filter, a cart, or a total means writing a React component and shipping a hydration runtime. Darkmown does it in the Markdown file. Side by side.
4
+ image: https://darkmown.com/__wd/media/og.png
5
+ site_url: https://darkmown.com
6
+ transitions: true
7
+ ---
8
+
9
+ @include /nav.wd
10
+ @include /base.wd
11
+
12
+ ::: main
13
+
14
+ # Darkmown vs. Markdoc
15
+
16
+ [Markdoc](https://markdoc.dev) is a good tool, and the closest thing to Darkmown in spirit: Markdown with first-party syntax instead of MDX's "Markdown plus JavaScript". If you are choosing between them, the difference is narrow and specific.
17
+
18
+ **Markdoc renders content. Darkmown runs it.** Markdoc has `{% if %}`, `{% partial %}` and `$variables`, so it can vary what it renders at build time. It has no loop tag, and no state, so the moment your page needs a filter, a cart, or a running total, you register a custom tag backed by a React component, ship React and a hydration runtime to the browser, and wire it up in your build. Darkmown expresses those in the Markdown file, and ships one 7,654-byte runtime.
19
+
20
+ ## The same store, both ways
21
+
22
+ ::: versus
23
+
24
+ ::: card .vs-card .vs-dark
25
+ `store.wd` (Darkmown)
26
+
27
+ ```wd
28
+ :state products = [
29
+ { "id": 1, "name": "Aurora Lamp", "price": 49 },
30
+ { "id": 2, "name": "Briza Fan", "price": 39 }
31
+ ]
32
+ :state cart = []
33
+ :state q = ""
34
+ :computed count = cart.length
35
+
36
+ :bind q placeholder="Search products…"
37
+
38
+ @loop products into p where p.name contains q
39
+ ::: card .product
40
+ **{ p.name }** · ${ p.price }
41
+ :button "Add" -> cart += p
42
+ :::
43
+ @endloop
44
+
45
+ Cart: { count } item(s)
46
+ ```
47
+
48
+ That is the whole file. [It is running on the homepage](/): search, add to cart, and a live total.
49
+ :::
50
+
51
+ ::: card .vs-card
52
+ `store.md` (Markdoc)
53
+
54
+ ```text
55
+ {% if $products %}
56
+ - **Aurora Lamp**, $49
57
+ - **Briza Fan**, $39
58
+ {% /if %}
59
+ ```
60
+
61
+ There is 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:
62
+
63
+ ```jsx
64
+ const tags = {
65
+ store: { render: 'Store' }
66
+ };
67
+
68
+ // your filter + cart + total live here
69
+ Markdoc.renderers.react(content, React, {
70
+ components: { Store }
71
+ });
72
+ ```
73
+
74
+ …plus React and a hydration runtime shipped to the browser, and the build step that wires the tag to the component.
75
+ :::
76
+
77
+ :::
78
+
79
+ ## Where each one wins
80
+
81
+ - **Pick Markdoc** if you are already in a React app, want Markdown authoring with a validated custom-tag schema, and are happy for interactivity to live in React components. Its schema validation and editor tooling are genuinely strong, and it is battle-tested at Stripe's scale.
82
+ - **Pick Darkmown** if you want the page itself to be the app (loops, state, fetch, and forms expressed in Markdown), and you want static pages to ship literally zero framework JavaScript.
83
+
84
+ ## Honest differences
85
+
86
+ - **Markdoc is older and more proven.** It runs Stripe's documentation. Darkmown is young; judge it accordingly.
87
+ - **Markdoc has schema validation for custom tags.** Darkmown has no custom tags at all, by design: the directive vocabulary is fixed and closed, which is what lets it validate expressions at compile time and run without `eval`.
88
+ - **Markdoc is renderer-agnostic** (React, HTML, or your own). Darkmown compiles to static HTML plus its own small runtime, and is not a library you embed in another framework's render tree.
89
+ - **Neither ships a component model.** Markdoc composes with `{% partial %}`, Darkmown with `@include`.
90
+
91
+ [Read the docs](/docs/){.btn .primary}
92
+ [Try Darkmown in your browser](/playground/){.btn}
93
+
94
+ :::
95
+
96
+ @include /footer.wd