@zvndev/darkmown 2.0.0 → 2.0.1

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.
Files changed (35) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +5 -3
  4. package/docs/spec-alignment.md +2 -1
  5. package/package.json +3 -2
  6. package/site/_/footer.wd +1 -1
  7. package/site/pages/app.wd +1 -1
  8. package/site/pages/blog/zero-js-by-default.md +1 -1
  9. package/site/pages/docs/index.wd +15 -1
  10. package/site/pages/index.js +19 -0
  11. package/site/pages/index.skin +46 -1
  12. package/site/pages/index.wd +5 -3
  13. package/site/pages/showcase/index.wd +1 -1
  14. package/src/compiler/context.js +11 -2
  15. package/src/compiler/includes.js +9 -3
  16. package/src/compiler/loops.js +34 -4
  17. package/src/compiler/page.js +29 -2
  18. package/src/compiler/structure.js +26 -4
  19. package/src/templates/blog/site/_/nav.wd +6 -9
  20. package/src/templates/blog/site/pages/index.skin +7 -4
  21. package/src/templates/blog/site/pages/index.wd +2 -3
  22. package/src/templates/dashboard/site/_/nav.wd +6 -9
  23. package/src/templates/dashboard/site/pages/index.skin +7 -4
  24. package/src/templates/dashboard/site/pages/index.wd +2 -3
  25. package/src/templates/landing/site/_/nav.wd +6 -9
  26. package/src/templates/landing/site/pages/index.skin +13 -4
  27. package/src/templates/landing/site/pages/index.wd +22 -9
  28. package/src/templates/starter/site/_/nav.wd +7 -10
  29. package/src/templates/starter/site/pages/about.md +7 -18
  30. package/src/templates/starter/site/pages/about.skin +0 -25
  31. package/src/templates/starter/site/pages/index.skin +10 -6
  32. package/src/templates/starter/site/pages/index.wd +2 -3
  33. package/src/templates/store/site/_/nav.wd +6 -9
  34. package/src/templates/store/site/pages/index.skin +7 -4
  35. package/src/templates/store/site/pages/index.wd +2 -3
package/AGENTS.md CHANGED
@@ -7,7 +7,7 @@ Darkmown is a Markdown-native web framework. You build sites by writing `.md` an
7
7
  - `.md` files are plain CommonMark. Directives stay inert text.
8
8
  - `.wd` files are the same Markdown **plus directives** (state, loops, conditionals, includes, forms).
9
9
  - To make a `.md` page interactive, **rename it to `.wd`**. ⚠️ Renaming means the old `.md` is **gone** — never leave both `index.md` and `index.wd`, that is a fatal `Duplicate route` build error. Upgrade = delete the `.md`, create the `.wd`.
10
- - Static pages ship **zero** JavaScript; a page loads the ~7.4 KB gzipped runtime (CI-enforced under 8 KB) only if it declares reactive behavior.
10
+ - Static pages ship **zero** JavaScript; a page loads the ~7.5 KB gzipped runtime (CI-enforced under 8 KB) only if it declares reactive behavior.
11
11
 
12
12
  ### ⚠️ Raw HTML is escaped by default (since 2.0.0)
13
13
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 2.0.1 — 2026-07-11
6
+
7
+ A tightening pass from a post-2.0 product review: a silent reactive-loop failure becomes a loud compile error, scaffolds stop shipping the raw-HTML footgun, and every scaffolded page gets real landmarks back. All compile-time — the runtime is byte-identical (7518 B gzipped).
8
+
9
+ - **A third reactive `@loop` level is now a compile error.** The runtime supports one level of reactive nesting; a third reactive level previously compiled clean and painted the inner region empty at runtime. It now fails the build with `file:line`, the offending opener quoted, and a corrective suggestion — including when the third level arrives through an `@include` (nesting depth threads across the include boundary). Static (build-unrolled) nesting is unaffected at any depth.
10
+ - **`::: nav` and `::: main` containers emit real landmark elements.** A container named `nav` or `main` now compiles to `<nav class="nav">` / `<main class="main">` instead of a `<div>` (the name stays as a class, so existing `.nav`/`.main` skin selectors match unchanged; every other name still emits a `<div>`). With that, the shell reuses the container's `<main>` for its `id="main"` skip-link target instead of wrapping the whole body — nav chrome sits outside `<main>` and "Skip to content" actually skips it.
11
+ - **Scaffold templates no longer set `html: true`.** 2.0.0 made escaped markdown the default and called raw-HTML mode the #1 footgun — while every template scaffolded with the flag on. All five templates now express their nav/layout/CTAs with container directives and `{.class}` inline attributes instead of raw HTML (zero `html: true` anywhere in a fresh scaffold), and with the landmark containers above they keep proper `<nav>`/`<main>` structure. The starter's `about.md` becomes pure CommonMark with a "Back home" link — a `.md` page can't include the shared nav, and the page now says so instead of duplicating raw markup.
12
+ - **`file:line` on the remaining compile errors.** Malformed `@loop` headers and include-resolution failures (unresolvable path, sandbox escape) reported only the file; they now report `file:line` like every other directive error, and the unresolvable-include message gained a hint ("Looked in site/pages and site/_ — check the path and leading slash").
13
+ - **Size prose is drift-guarded.** Docs and site copy quoting the runtime size (now "~7.5 KB" everywhere current-tense) are asserted against `.size-snapshot.json` by a new test, so the number in the prose can never silently diverge from the shipped bytes again.
14
+ - **darkmown.com hero: install one-liner with a copy button.** The landing page now puts `npx @zvndev/darkmown init my-site` (click-to-copy) directly under the CTAs, and the site docs gained a CLI reference section covering all six commands.
15
+ - **Internal: a build-throughput benchmark.** `npm run bench:build` generates a synthetic site (collections, includes, static loops, highlighted code) and reports pages/sec; it refuses to touch a `site/` directory it didn't generate itself.
16
+
5
17
  ## 2.0.0 — 2026-07-11
6
18
 
7
19
  The raw-HTML markdown default flips to escaped (`html: false` — the breaking change that makes this a major), alongside security hardening and a DX pass from a product review: incremental dev rebuilds, true error line numbers, a docs table of contents, and styled templates. Nearly all build-time and dev-server; the one runtime change — deterministic UTC date pipes — nudges the runtime to 7518 B gzipped, still well under the 8 KB budget.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **[darkmown.com](https://darkmown.com)** · Markdown that runs.
9
9
 
10
- Darkmown is a Markdown-native web framework. Two formats, one rule: `.md` stays plain CommonMark forever, and renaming a file to `.wd` ("whateverdown") is what unlocks directives — includes, loops, state, conditionals, and sections. Static pages ship **zero** framework JavaScript; reactive pages share one runtime around ~7.4 KB gzipped, CI-enforced under 8 KB.
10
+ Darkmown is a Markdown-native web framework. Two formats, one rule: `.md` stays plain CommonMark forever, and renaming a file to `.wd` ("whateverdown") is what unlocks directives — includes, loops, state, conditionals, and sections. Static pages ship **zero** framework JavaScript; reactive pages share one runtime around ~7.5 KB gzipped, CI-enforced under 8 KB.
11
11
 
12
12
  ## Showcase
13
13
 
@@ -65,7 +65,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
65
65
  - Files or folders starting with `.`, `-`, or `_` are hidden from routing.
66
66
  - `site/_` is the include shelf for `@include /name.wd`.
67
67
  - Matching `page.skin` and `page.js` colocate styling and behavior by basename.
68
- - Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~7.4 KB gzipped, CI-enforced under 8 KB).
68
+ - Static pages ship zero Darkmown runtime. Reactive pages share `/__wd/runtime.js` (currently ~7.5 KB gzipped, CI-enforced under 8 KB).
69
69
  - Shelf `.json` files are published at `/__wd/data/` so `:fetch` works on any static host.
70
70
 
71
71
  ## Interpolation
@@ -353,7 +353,7 @@ That is a full add-to-cart / remove-line flow — and a to-do list with delete
353
353
  - Interpolation inside the inner body resolves the inner item first, then the outer item — `{ line.qty }` and `{ order.ref }` are both in scope.
354
354
  - Build-time loops over JSON/frontmatter already nest freely (the "loops nest" behavior above); what's new is **reactive-inside-reactive** over a `:state`/`:store`/`:fetch` row field.
355
355
 
356
- > **Honest caveat:** nesting is **one level only** — an inner reactive loop may not itself contain a third reactive loop. Build-time loop nesting is unaffected by this limit.
356
+ > **Honest caveat:** nesting is **one level only** — an inner reactive loop that itself contains a third reactive loop is a **compile error** with a corrective message, not a silent runtime failure. Build-time loop nesting is unaffected by this limit.
357
357
  >
358
358
  > **Per-row actions inside an inner loop:** `cart += member` (append-row) works from an inner loop — it carries the inner item into a top-level `:state`/`:store` list. But a per-row **`remove`** needs a top-level list as its source, and an inner loop's source is a path off the outer row (`team.members`), so deleting an inner row in place is **not** supported — the compiler rejects it with a corrective message. Carry the row into a top-level list and remove it there.
359
359
 
@@ -440,6 +440,8 @@ Cart has { count } items.
440
440
 
441
441
  State declared inside a section is scoped to it — two sections can both own a `count`. Bindings and actions resolve to the nearest scope.
442
442
 
443
+ A container named `nav` or `main` emits the real landmark element (`<nav class="nav">`, `<main class="main">`) instead of a `<div>`, so scaffolded pages keep proper landmarks — a skip link skips a `::: nav`, and `::: main` becomes the page's `<main id="main">`. Any other name stays a `<div>` with that class.
444
+
443
445
  ### Reactive classes — `.class when <predicate>`
444
446
 
445
447
  A container class can be toggled by a predicate. Static `.class` tokens are unchanged; add `when <predicate>` to make one reactive:
@@ -6,10 +6,11 @@ This audit compares the current Darkmown implementation with the original MARROW
6
6
 
7
7
  - **Markdown-first authoring:** `.md` is strict CommonMark rendered by a real parser (markdown-it); `.wd` is the same Markdown plus first-party directives. Renaming the file is the upgrade path — extensions genuinely gate functionality.
8
8
  - **Content tree over components:** routing, includes, and repeated fragments are file/document based instead of React-style components.
9
- - **Tiered output:** static routes emit HTML/CSS and no Darkmown runtime; reactive routes opt into `/__wd/runtime.js` (~7.4 KB gzipped, CI-enforced below the 8 KB target).
9
+ - **Tiered output:** static routes emit HTML/CSS and no Darkmown runtime; reactive routes opt into `/__wd/runtime.js` (~7.5 KB gzipped, CI-enforced below the 8 KB target).
10
10
  - **No virtual DOM:** state changes patch direct DOM bindings, conditional regions, and keyed loop regions.
11
11
  - **Keyed list patching:** reactive loops reconcile by item key (`id`/`key`/value) instead of re-rendering the region.
12
12
  - **Sections with scoped state:** `::: section #id .class` containers scope `:state`; bindings and actions resolve through the nearest scope chain.
13
+ - **Semantic landmark containers:** `::: nav` / `::: main` emit the real `<nav>`/`<main>` element (keeping the name as a class for styling); other container names stay `<div>`s. Keeps skip links and landmarks intact without raw HTML.
13
14
  - **`.skin` language:** indentation-based token styling compiles to CSS.
14
15
  - **Folder router:** `site/pages` maps to routes, and `.`, `-`, `_` prefixes hide pages.
15
16
  - **Colocation:** matching `.skin` and `.js` files attach to the page by basename, for includes too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,7 +35,8 @@
35
35
  "prepare": "husky || true",
36
36
  "prepublish:check": "npm run typecheck && npm run test:cov && npm run build && npm run runtime:size && npm audit --audit-level=high && npm run smoke",
37
37
  "prepublishOnly": "npm run prepublish:check",
38
- "og": "node scripts/og-image.mjs"
38
+ "og": "node scripts/og-image.mjs",
39
+ "bench:build": "node scripts/bench-build.mjs"
39
40
  },
40
41
  "lint-staged": {
41
42
  "*.{js,json}": "biome check --no-errors-on-unmatched --files-ignore-unknown=true"
package/site/_/footer.wd CHANGED
@@ -6,7 +6,7 @@ html: true
6
6
  <div class="foot-inner">
7
7
  <div>
8
8
  <strong>Darkmown</strong> — Markdown that runs.
9
- <div class="small">Zero JS on static pages · one ~7.4&nbsp;KB runtime when reactive.</div>
9
+ <div class="small">Zero JS on static pages · one ~7.5&nbsp;KB runtime when reactive.</div>
10
10
  </div>
11
11
  <nav class="foot-links">
12
12
  <a href="/docs/">Docs</a>
package/site/pages/app.wd CHANGED
@@ -20,7 +20,7 @@ This whole section is plain Markdown. The list below is unrolled **at build time
20
20
  - **{ feature.title }** — { feature.body }
21
21
  @endloop
22
22
 
23
- <aside class="boundary">If you deleted everything below this line, this page would ship <strong>zero</strong> bytes of JavaScript. Everything above is static HTML. Below is where the one ~7.4&nbsp;KB gzipped runtime loads (CI-enforced under 8 KB) — and only because the document asks for it.</aside>
23
+ <aside class="boundary">If you deleted everything below this line, this page would ship <strong>zero</strong> bytes of JavaScript. Everything above is static HTML. Below is where the one ~7.5&nbsp;KB gzipped runtime loads (CI-enforced under 8 KB) — and only because the document asks for it.</aside>
24
24
 
25
25
  ## Fetched data, looped, with nested conditions
26
26
 
@@ -15,7 +15,7 @@ none of them pay for it.
15
15
 
16
16
  That is the whole bargain. A `.md` file is plain CommonMark. Rename it to `.wd`
17
17
  and you unlock directives — state, loops, conditionals — but you only ship the
18
- ~7.4 KB runtime on the pages that actually use them. A listing built from a
18
+ ~7.5 KB runtime on the pages that actually use them. A listing built from a
19
19
  collection loop resolves entirely at build time, so it stays static HTML.
20
20
 
21
21
  ## The extension is the feature gate
@@ -47,6 +47,7 @@ Darkmown is a Markdown-native framework for mostly-static sites with tiny reacti
47
47
  - [Syntax highlighting](#syntax-highlighting)
48
48
  - [Trust boundary](#trust-boundary)
49
49
  - [Colocation](#colocation)
50
+ - [CLI reference](#cli-reference)
50
51
  - [Spec status](#spec-status)
51
52
  </nav>
52
53
 
@@ -308,9 +309,11 @@ You don't size images by hand. Every `<img>` the compiler emits gets its intrins
308
309
 
309
310
  `::: 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.
310
311
 
312
+ A container named `nav` or `main` emits the real landmark element (`<nav class="nav">`, `<main class="main">`) so pages keep proper landmarks without raw HTML. Any other name stays a `<div>` with that class.
313
+
311
314
  ## Reactive directives
312
315
 
313
- Reactive pages opt into `/__wd/runtime.js` (~7.4 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
316
+ Reactive pages opt into `/__wd/runtime.js` (~7.5 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
314
317
 
315
318
  ```wd
316
319
  :state count = 0
@@ -599,6 +602,17 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) i
599
602
  - Both work for included fragments too, by basename.
600
603
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
601
604
 
605
+ ## CLI reference
606
+
607
+ The `darkmown` command (installed by `npx @zvndev/darkmown`) is the whole tool — every command is one word:
608
+
609
+ - `darkmown init [dir] [--template <name>]` — scaffold a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`). Existing files are never overwritten.
610
+ - `darkmown dev` — the live compiler: browser reload, an in-browser error overlay, incremental rebuilds, and a local runner for `api/*` functions.
611
+ - `darkmown build [--target cloudflare] [--drafts]` — write 100% static output to `dist/` (plus `sitemap.xml` / `rss.xml` / `robots.txt`); `--drafts` includes `draft: true` pages for a staging deploy.
612
+ - `darkmown serve` — preview the built `dist/` locally (static only; use `dev` to exercise `api/*`).
613
+ - `darkmown deploy <vercel|cloudflare> [--prod]` — build (target-aware) and deploy through the platform CLI, printing your URL; an unauthenticated CLI surfaces the login to run first.
614
+ - `darkmown version` / `darkmown help` — print the installed version, or the full command summary and directive syntax.
615
+
602
616
  ## Spec status
603
617
 
604
618
  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. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: HTML-fragment `swap` semantics and cart server sync. (The serverless `api/` model already shipped in 1.2.0 — see [Backends & deploy](#backends--deploy) above; a `.wd` backend DSL remains a deliberate non-goal.) See `docs/spec-alignment.md` in the package for the full audit.
@@ -1,3 +1,22 @@
1
+ const copyBtn = document.querySelector("[data-copy-cmd]");
2
+
3
+ if (copyBtn) {
4
+ copyBtn.addEventListener("click", async () => {
5
+ const cmd = copyBtn.previousElementSibling?.textContent ?? "";
6
+ try {
7
+ await navigator.clipboard.writeText(cmd);
8
+ copyBtn.textContent = "Copied";
9
+ copyBtn.classList.add("is-copied");
10
+ setTimeout(() => {
11
+ copyBtn.textContent = "Copy";
12
+ copyBtn.classList.remove("is-copied");
13
+ }, 1600);
14
+ } catch {
15
+ copyBtn.textContent = "Select + ⌘C";
16
+ }
17
+ });
18
+ }
19
+
1
20
  const button = document.querySelector("[data-wd-demo]");
2
21
 
3
22
  if (button) {
@@ -29,7 +29,52 @@
29
29
  .hero-cta
30
30
  display flex
31
31
  flex-wrap wrap
32
- margin-bottom 2rem
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
41
+ border 1px solid $line
42
+ radius 10px
43
+ bg $sunk
44
+ font-family ui-monospace, SFMono-Regular, Menlo, monospace
45
+ overflow-x auto
46
+
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
55
+ font-size .95rem
56
+ color $ink
57
+ white-space nowrap
58
+
59
+ .hero-install .copy-cmd
60
+ flex-shrink 0
61
+ margin-left .35rem
62
+ padding .25rem .65rem
63
+ border 1px solid $line
64
+ radius 7px
65
+ bg transparent
66
+ color $ink-soft
67
+ font-family inherit
68
+ font-size .8rem
69
+ cursor pointer
70
+
71
+ .hero-install .copy-cmd:hover
72
+ color $ink
73
+ border-color $accent
74
+
75
+ .hero-install .copy-cmd.is-copied
76
+ color $accent
77
+ border-color $accent
33
78
 
34
79
  .hero-stats
35
80
  display grid
@@ -24,9 +24,11 @@ The left file is Markdown. It is also a running app — a live search filter, ad
24
24
  <a class="btn" href="/docs/">Read the docs</a>
25
25
  </div>
26
26
 
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>
28
+
27
29
  <ul class="hero-stats">
28
30
  <li><strong>0 KB</strong><span>framework JS on static pages</span></li>
29
- <li><strong>~7.4 KB</strong><span>one runtime, CI-enforced &lt; 8 KB</span></li>
31
+ <li><strong>~7.5 KB</strong><span>one runtime, CI-enforced &lt; 8 KB</span></li>
30
32
  <li><strong>1</strong><span>loop and one binding to learn</span></li>
31
33
  <li><strong>MIT</strong><span>zero runtime dependencies</span></li>
32
34
  </ul>
@@ -110,14 +112,14 @@ Markdoc.renderers.react(content, React, {
110
112
 
111
113
  </section>
112
114
 
113
- :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~7.4 KB gzipped runtime loads (CI-enforced under 8 KB), never before. JS optional, CSS optional, always."
115
+ :note "Static pages ship zero JavaScript. The moment a page asks for reactivity — like this very homepage — one ~7.5 KB gzipped runtime loads (CI-enforced under 8 KB), never before. JS optional, CSS optional, always."
114
116
 
115
117
  <section class="props">
116
118
 
117
119
  ## Three rules, no escape hatch
118
120
 
119
121
  - **Zero-JS static.** A page with no state ships no framework JavaScript. `/markdown/` and `/docs/` prove it — open them and check the network tab.
120
- - **One runtime.** Every reactive page shares the same ~7.4 KB gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that scales with your UI.
122
+ - **One runtime.** Every reactive page shares the same ~7.5 KB gzipped runtime, CI-enforced under 8 KB. No per-component bundles, no hydration tax that scales with your UI.
121
123
  - **One loop, one binding.** `@loop … into …` is the only loop; `{ name }` is the only interpolation. Nothing else to learn, nothing else to misuse.
122
124
 
123
125
  </section>
@@ -96,7 +96,7 @@ Darkmown's whole pitch is weight. A static page ships **zero** framework JavaScr
96
96
 
97
97
  ::: card .sc-scale
98
98
  - **Static Darkmown page** — `0 KB` framework JS. Forever.
99
- - **Reactive Darkmown page** — one shared `~7.4 KB` gzipped runtime, no per-component bundles.
99
+ - **Reactive Darkmown page** — one shared `~7.5 KB` gzipped runtime, no per-component bundles.
100
100
  - **A typical SPA** — ships a framework runtime in the **~15–60 KB** gzipped range *before* your app code, on every page (Alpine ~15 KB, Vue ~18 KB runtime, React 19 ~59 KB).
101
101
  :::
102
102
 
@@ -102,15 +102,24 @@
102
102
  * @property {string[]} sections Active section-id scope chain.
103
103
  * @property {string | null} loopItem Name of the current reactive loop item, if any.
104
104
  * @property {string} [loopKey] State key of the list being looped, if any.
105
+ * @property {number} [reactiveDepth] Number of enclosing REACTIVE `@loop` levels
106
+ * (0 at top level). The runtime reconciles at most two nested `data-wd-loop`
107
+ * levels — a third paints empty — so a third reactive level is rejected at
108
+ * compile time. Static (build-unrolled) loops flatten to markup and don't add
109
+ * a level, so they carry the count through without incrementing it.
110
+ * @property {{ at: string, line: string }} [loopOpener] The current `@loop`
111
+ * opener's `file:line` + raw text, set when descending into a loop body so the
112
+ * reactive-nesting-depth error can point at the offending opener.
105
113
  * @property {boolean} [loopMeta] Inside a loop body, so `$index`/`$first`/… are valid.
106
114
  * @property {import("markdown-it").default} [md] Markdown-it instance selected for this file.
107
115
  * @property {(lines: string[], ctx: Ctx) => string} compileBody The body
108
116
  * dispatcher, threaded through the context so block handlers (`:if`, `:::`,
109
117
  * `:form`, `@loop`, …) recurse into nested bodies without importing the
110
118
  * dispatcher back — the module graph stays an import-cycle-free DAG.
111
- * @property {(file: string, context: Paths, stack: string[], scope: Scope, comp: Compilation, sections: string[], loopItem: string | null) => { meta: Meta, html: string }} compileFile
119
+ * @property {(file: string, context: Paths, stack: string[], scope: Scope, comp: Compilation, sections: string[], loopItem: string | null, reactiveDepth?: number, loopOpener?: { at: string, line: string } | null) => { meta: Meta, html: string }} compileFile
112
120
  * The per-file compile, threaded for the same reason so `@include` can
113
- * compile its target file.
121
+ * compile its target file. `reactiveDepth`/`loopOpener` carry the enclosing
122
+ * reactive-loop nesting across the include boundary so the depth guard holds.
114
123
  */
115
124
 
116
125
  /**
@@ -158,9 +158,13 @@ export function collectColocatedAssets(file, context, assets) {
158
158
  * @param {string} fromFile File requesting the include.
159
159
  * @param {Paths} context
160
160
  * @param {boolean} [allowAny] Allow non-page extensions (e.g. JSON for `@loop`).
161
+ * @param {string} [loc] Source location (`file:line`) for the error messages, so
162
+ * an unresolved/out-of-sandbox include reports the directive's line. Callers
163
+ * with a line index pass `at(ctx, index)`; it defaults to `fromFile` (the bare
164
+ * path) for any caller without one, preserving the file-only message.
161
165
  * @returns {string}
162
166
  */
163
- export function resolveInclude(spec, fromFile, context, allowAny = false) {
167
+ export function resolveInclude(spec, fromFile, context, allowAny = false, loc = fromFile) {
164
168
  const clean = stripQuotes(spec);
165
169
  const candidates = [];
166
170
  if (clean.startsWith("/")) {
@@ -172,13 +176,15 @@ export function resolveInclude(spec, fromFile, context, allowAny = false) {
172
176
  for (const candidate of candidates) {
173
177
  const resolved = path.resolve(candidate);
174
178
  if (!isAllowedInclude(resolved, context)) {
175
- throw new Error(`Include "${spec}" from ${fromFile} resolves outside site/pages or site/_`);
179
+ throw new Error(`Include "${spec}" from ${loc} resolves outside site/pages or site/_`);
176
180
  }
177
181
  if (!fs.existsSync(resolved)) continue;
178
182
  if (!allowAny && !pageIncludeExtensions.includes(path.extname(resolved))) continue;
179
183
  return resolved;
180
184
  }
181
- throw new Error(`Could not resolve include "${spec}" from ${fromFile}`);
185
+ throw new Error(
186
+ `Could not resolve include "${spec}" from ${loc}. Looked in site/pages and site/_ — check the path and leading slash.`
187
+ );
182
188
  }
183
189
 
184
190
  /**
@@ -219,7 +219,7 @@ function numArgAttr(arg) {
219
219
  */
220
220
  export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart = 0) {
221
221
  const match = line.match(/^@loop\s+(.+?)\s+into\s+([A-Za-z_$][\w$]*)(\s+.+?)?\s*$/);
222
- if (!match) throw new Error(`Malformed @loop in ${ctx.file}: ${line}. ${LOOP_USAGE}`);
222
+ if (!match) throw new Error(`Malformed @loop in ${at(ctx, index)}: ${line}. ${LOOP_USAGE}`);
223
223
  const source = stripQuotes(match[1].trim());
224
224
  const itemName = match[2];
225
225
  const clauses = match[3]
@@ -273,8 +273,13 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
273
273
  paginate: clauses.paginate
274
274
  };
275
275
  // The loop body starts on the line after the opener; nested errors in it (and
276
- // in the empty branch, via `opts.emptyStart`) report the true file line.
277
- const bodyCtx = nestedCtx(ctx, index + 1);
276
+ // in the empty branch, via `opts.emptyStart`) report the true file line. The
277
+ // opener's location rides on the body ctx so a reactive loop nested inside this
278
+ // one can point the depth error back at its own opener line.
279
+ const bodyCtx = {
280
+ ...nestedCtx(ctx, index + 1),
281
+ loopOpener: { at: at(ctx, index), line: line.trim() }
282
+ };
278
283
 
279
284
  if (
280
285
  source.startsWith("/") ||
@@ -283,7 +288,7 @@ export function handleLoop(line, bodyLines, emptyLines, ctx, index, emptyStart =
283
288
  source.endsWith(".json")
284
289
  ) {
285
290
  if (opts.paginate) throw new Error(paginateOnlyCollections(ctx));
286
- const dataFile = resolveInclude(source, ctx.file, ctx.context, true);
291
+ const dataFile = resolveInclude(source, ctx.file, ctx.context, true, at(ctx, index));
287
292
  ctx.comp.deps.add(dataFile);
288
293
  const rows = JSON.parse(fs.readFileSync(dataFile, "utf8"));
289
294
  if (!Array.isArray(rows)) throw new Error(`@loop data must be a JSON array: ${dataFile}`);
@@ -372,6 +377,27 @@ function paginateOnlyCollections(ctx) {
372
377
  return `@loop paginate requires a collection source (a site/pages/<name>/ subdirectory) in ${ctx.file}. ${LOOP_USAGE}`;
373
378
  }
374
379
 
380
+ /**
381
+ * Reject a reactive `@loop` that would open a THIRD reactive nesting level. The
382
+ * runtime reconciles at most two nested `data-wd-loop` levels (an outer loop and
383
+ * one inner loop); a third level's `data-wd-loop-out` paints empty. This guards
384
+ * every reactive-region entry point ({@link reactiveLoop}, {@link itemRelativeLoop}),
385
+ * so a state-key, item-relative, or state-filtered source is all covered. Static
386
+ * (build-unrolled) loops carry the depth through without incrementing it, so an
387
+ * interleaved static level neither triggers nor masks the limit.
388
+ * `ctx.reactiveDepth` is the number of reactive loops already enclosing this one.
389
+ * @param {Ctx} ctx
390
+ * @returns {void}
391
+ */
392
+ function assertReactiveDepth(ctx) {
393
+ if ((ctx.reactiveDepth ?? 0) < 2) return;
394
+ const opener = ctx.loopOpener ?? { at: ctx.file, line: "" };
395
+ throw new Error(
396
+ `Reactive @loop nesting is limited to one inner level in ${opener.at}: "${opener.line}". ` +
397
+ `Unroll the outer data at build time (JSON/frontmatter source) or move the innermost list into build-time data.`
398
+ );
399
+ }
400
+
375
401
  /**
376
402
  * Loop over a build-time collection's entry rows. With no `paginate`, this is a
377
403
  * plain static unroll (filter/sort/reverse/offset/limit at build time, zero JS).
@@ -540,6 +566,7 @@ function staticUnroll(rows, itemName, bodyLines, ctx, empty = null, emptyStart =
540
566
  * @returns {string}
541
567
  */
542
568
  function reactiveLoop(key, itemName, bodyLines, ctx, opts = null) {
569
+ assertReactiveDepth(ctx);
543
570
  ctx.comp.assets.runtime = true;
544
571
  /** @type {Ctx} */
545
572
  const templateCtx = {
@@ -547,6 +574,7 @@ function reactiveLoop(key, itemName, bodyLines, ctx, opts = null) {
547
574
  loopItem: itemName,
548
575
  loopKey: key ?? undefined,
549
576
  loopMeta: true,
577
+ reactiveDepth: (ctx.reactiveDepth ?? 0) + 1,
550
578
  scope: createScope(ctx.scope)
551
579
  };
552
580
  const templateHtml = ctx.compileBody(bodyLines, templateCtx).trim();
@@ -689,6 +717,7 @@ function loopClauseAttrs(c) {
689
717
  * @returns {string}
690
718
  */
691
719
  function itemRelativeLoop(path, itemName, bodyLines, ctx, opts) {
720
+ assertReactiveDepth(ctx);
692
721
  /** @type {Ctx} */
693
722
  // loopKey: undefined — an item-relative loop's source is a path off the outer
694
723
  // row, not a top-level state key, so a per-row `remove` inside it has no valid
@@ -698,6 +727,7 @@ function itemRelativeLoop(path, itemName, bodyLines, ctx, opts) {
698
727
  loopItem: itemName,
699
728
  loopKey: undefined,
700
729
  loopMeta: true,
730
+ reactiveDepth: (ctx.reactiveDepth ?? 0) + 1,
701
731
  scope: createScope(ctx.scope)
702
732
  };
703
733
  const templateHtml = ctx.compileBody(bodyLines, templateCtx).trim();
@@ -293,7 +293,17 @@ function measureImage(src, paths) {
293
293
  export function compileDocument(file, context, stack = [], vars = {}, collections = new Map()) {
294
294
  const comp = createCompilation();
295
295
  comp.collections = collections;
296
- const result = compileFile(file, context, stack, createScope(null, vars), comp, [], null);
296
+ const result = compileFile(
297
+ file,
298
+ context,
299
+ stack,
300
+ createScope(null, vars),
301
+ comp,
302
+ [],
303
+ null,
304
+ 0,
305
+ null
306
+ );
297
307
  return {
298
308
  meta: result.meta,
299
309
  html: result.html,
@@ -314,9 +324,24 @@ export function compileDocument(file, context, stack = [], vars = {}, collection
314
324
  * @param {Compilation} comp
315
325
  * @param {string[]} sections
316
326
  * @param {string | null} loopItem
327
+ * @param {number} [reactiveDepth] Enclosing REACTIVE `@loop` levels, threaded in
328
+ * from the including page so an include's body can't bypass the two-level
329
+ * reactive-nesting limit at the `@include` boundary.
330
+ * @param {{ at: string, line: string } | null} [loopOpener] The enclosing `@loop`
331
+ * opener, threaded in so a depth error inside the include names the right opener.
317
332
  * @returns {{ meta: Meta, html: string }}
318
333
  */
319
- export function compileFile(file, context, stack, scope, comp, sections, loopItem) {
334
+ export function compileFile(
335
+ file,
336
+ context,
337
+ stack,
338
+ scope,
339
+ comp,
340
+ sections,
341
+ loopItem,
342
+ reactiveDepth = 0,
343
+ loopOpener = null
344
+ ) {
320
345
  const real = fs.realpathSync(file);
321
346
  if (stack.includes(real)) {
322
347
  throw new Error(
@@ -351,6 +376,8 @@ export function compileFile(file, context, stack, scope, comp, sections, loopIte
351
376
  comp,
352
377
  sections,
353
378
  loopItem,
379
+ reactiveDepth,
380
+ ...(loopOpener ? { loopOpener } : {}),
354
381
  md: selectMd(meta),
355
382
  compileBody,
356
383
  compileFile
@@ -27,6 +27,14 @@ import { compileWhen, evalPredicate } from "./predicates.js";
27
27
  * @typedef {import("./context.js").Ctx} Ctx
28
28
  */
29
29
 
30
+ // Container names that emit their own semantic landmark element instead of a
31
+ // `<div>`, so scaffolds get real `<nav>`/`<main>` landmarks (accessibility: the
32
+ // skip link targets a genuine main-content region and navigation chrome sits in
33
+ // its own landmark). The name is still added as a class, so existing `.nav`/
34
+ // `.main` skin selectors keep working. Fixed compiler-owned constants — never
35
+ // author-supplied — so no tag-injection surface; classes/ids still escape.
36
+ const SEMANTIC_CONTAINER_TAGS = new Set(["nav", "main"]);
37
+
30
38
  /**
31
39
  * @param {string} line
32
40
  * @param {Ctx} ctx
@@ -36,9 +44,13 @@ import { compileWhen, evalPredicate } from "./predicates.js";
36
44
  export function handleInclude(line, ctx, index) {
37
45
  const match = line.match(/^@include\s+(\S+)(?:\s+with\s+(.+))?$/);
38
46
  if (!match) throw new Error(`Malformed @include in ${at(ctx, index)}: ${line}`);
39
- const target = resolveInclude(match[1], ctx.file, ctx.context);
47
+ const target = resolveInclude(match[1], ctx.file, ctx.context, false, at(ctx, index));
40
48
  const args = parseIncludeArgs(match[2] || "", ctx);
41
49
  const childScope = createScope(ctx.scope, args);
50
+ // Thread the reactive-nesting depth (and the enclosing loop opener, so a depth
51
+ // error names the right opener) into the include, exactly as `loopItem` is —
52
+ // otherwise a reactive @loop inside the include would start counting from zero
53
+ // and could open a THIRD nested `data-wd-loop` level that silently paints empty.
42
54
  const child = ctx.compileFile(
43
55
  target,
44
56
  ctx.context,
@@ -46,7 +58,9 @@ export function handleInclude(line, ctx, index) {
46
58
  childScope,
47
59
  ctx.comp,
48
60
  ctx.sections,
49
- ctx.loopItem
61
+ ctx.loopItem,
62
+ ctx.reactiveDepth ?? 0,
63
+ ctx.loopOpener ?? null
50
64
  );
51
65
  // An include with its OWN scoped colocated skin stamps just its returned
52
66
  // subtree — so a scoped `.card` in the include can't collide with a `.card`
@@ -102,14 +116,22 @@ export function handleContainer(header, bodyLines, ctx, index) {
102
116
  const stateClasses = [];
103
117
  let id = "";
104
118
  // Leading tag/name token (anything not starting with . or #). "section" keeps
105
- // the <section> tag; any other name becomes a <div> and also a class.
119
+ // the <section> tag; a whitelisted semantic landmark (`nav`/`main`) emits that
120
+ // real element AND keeps the name as a class hook (so `.nav`/`.main` skins
121
+ // still cascade); any other name becomes a <div> and also a class. The tag is
122
+ // always one of these fixed constants — never author text — so it can't inject.
106
123
  const lead = rest.match(/^([^\s.#]\S*)/);
107
124
  let nameToken = "section";
108
125
  if (lead) {
109
126
  nameToken = lead[1];
110
127
  rest = rest.slice(lead[0].length).trim();
111
128
  }
112
- if (nameToken !== "section") {
129
+ if (nameToken === "section") {
130
+ // default tag, no class
131
+ } else if (SEMANTIC_CONTAINER_TAGS.has(nameToken)) {
132
+ tag = nameToken;
133
+ extraClass.push(nameToken);
134
+ } else {
113
135
  tag = "div";
114
136
  extraClass.push(nameToken);
115
137
  }
@@ -1,10 +1,7 @@
1
- ---
2
- html: true
3
- ---
1
+ ::: nav
2
+ **My Blog**
4
3
 
5
- <nav>
6
- <strong>My Blog</strong>
7
- <span class="links">
8
- <a href="/">Posts</a>
9
- </span>
10
- </nav>
4
+ ::: links
5
+ [Posts](/)
6
+ :::
7
+ :::
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.6
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 720px
32
32
  margin 0 auto
33
33
  padding 2.5rem 1.5rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -40,10 +40,13 @@ nav
40
40
  margin 0 auto
41
41
  padding 1.25rem 1.5rem
42
42
 
43
- nav strong
43
+ .nav p
44
+ margin 0
45
+
46
+ .nav strong
44
47
  font-size 1.1rem
45
48
 
46
- nav a
49
+ .nav a
47
50
  color $accent
48
51
  text-decoration none
49
52
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- html: true
3
2
  title: My Blog
4
3
  description: A markdown-native blog built with Darkmown.
5
4
  site_url: https://example.com
@@ -8,7 +7,7 @@ transitions: true
8
7
 
9
8
  @include /nav.wd
10
9
 
11
- <main>
10
+ ::: main
12
11
 
13
12
  # My Blog
14
13
 
@@ -28,4 +27,4 @@ config, zero JavaScript; each post's frontmatter is the data.
28
27
  No posts yet — add a `.md` file to `site/pages/posts/`.
29
28
  @endloop
30
29
 
31
- </main>
30
+ :::
@@ -1,10 +1,7 @@
1
- ---
2
- html: true
3
- ---
1
+ ::: nav
2
+ **Dashboard**
4
3
 
5
- <nav>
6
- <strong>Dashboard</strong>
7
- <span class="links">
8
- <a href="/">Overview</a>
9
- </span>
10
- </nav>
4
+ ::: links
5
+ [Overview](/)
6
+ :::
7
+ :::
@@ -18,12 +18,12 @@ page
18
18
  line-height 1.55
19
19
  -webkit-font-smoothing antialiased
20
20
 
21
- main
21
+ .main
22
22
  max-width 880px
23
23
  margin 0 auto
24
24
  padding 2rem 1.5rem
25
25
 
26
- nav
26
+ .nav
27
27
  display flex
28
28
  align-items center
29
29
  justify-content space-between
@@ -31,10 +31,13 @@ nav
31
31
  margin 0 auto
32
32
  padding 1.25rem 1.5rem
33
33
 
34
- nav strong
34
+ .nav p
35
+ margin 0
36
+
37
+ .nav strong
35
38
  font-size 1.1rem
36
39
 
37
- nav a
40
+ .nav a
38
41
  color $accent
39
42
  text-decoration none
40
43
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- html: true
3
2
  title: Dashboard — built with Darkmown
4
3
  description: A live dashboard — stat cards fetched from a serverless function, on an otherwise static page.
5
4
  transitions: true
@@ -7,7 +6,7 @@ transitions: true
7
6
 
8
7
  @include /nav.wd
9
8
 
10
- <main>
9
+ ::: main
11
10
 
12
11
  ::: section #dash
13
12
 
@@ -73,4 +72,4 @@ The numbers come from `api/metrics.js` — a serverless function that runs in
73
72
 
74
73
  :::
75
74
 
76
- </main>
75
+ :::
@@ -1,10 +1,7 @@
1
- ---
2
- html: true
3
- ---
1
+ ::: nav
2
+ **Acme**
4
3
 
5
- <nav>
6
- <strong>Acme</strong>
7
- <span class="links">
8
- <a href="#start">Get started</a>
9
- </span>
10
- </nav>
4
+ ::: links
5
+ [Get started](#start)
6
+ :::
7
+ :::
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.6
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 900px
32
32
  margin 0 auto
33
33
  padding 1rem 1.5rem 4rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -40,10 +40,13 @@ nav
40
40
  margin 0 auto
41
41
  padding 1.25rem 1.5rem
42
42
 
43
- nav strong
43
+ .nav p
44
+ margin 0
45
+
46
+ .nav strong
44
47
  font-size 1.15rem
45
48
 
46
- nav a
49
+ .nav a
47
50
  color $accent
48
51
  text-decoration none
49
52
 
@@ -60,6 +63,9 @@ nav a
60
63
  color $muted
61
64
  margin 0 0 1.5rem
62
65
 
66
+ .lede p
67
+ margin 0
68
+
63
69
  .cta
64
70
  display inline-flex
65
71
  align-items center
@@ -186,6 +192,9 @@ input:focus-visible, textarea:focus-visible, select:focus-visible
186
192
  color $muted
187
193
  font-size .95rem
188
194
 
195
+ .quote .who p
196
+ margin 0
197
+
189
198
  .wd-carousel-prev
190
199
  position absolute
191
200
  left -.5rem
@@ -1,5 +1,4 @@
1
1
  ---
2
- html: true
3
2
  title: Acme — ship faster
4
3
  description: A marketing one-pager built with Darkmown. Zero framework JS on the static parts, with a testimonials carousel.
5
4
  transitions: true
@@ -7,12 +6,16 @@ transitions: true
7
6
 
8
7
  @include /nav.wd
9
8
 
10
- <main>
9
+ ::: main
11
10
 
12
11
  ::: hero
13
12
  # Ship faster with *Acme*
14
- <p class="lede">The all-in-one toolkit your team actually enjoys using.</p>
15
- <a class="cta" href="#start">Get started</a>
13
+
14
+ ::: lede
15
+ The all-in-one toolkit your team actually enjoys using.
16
+ :::
17
+
18
+ [Get started](#start){.cta}
16
19
  :::
17
20
 
18
21
  ::: features
@@ -35,21 +38,31 @@ One command to Vercel or Cloudflare — your API comes along for free.
35
38
  :carousel
36
39
  ::: quote
37
40
  > "Acme cut our launch time in half."
38
- <p class="who">— Riley, Head of Product</p>
41
+
42
+ ::: who
43
+ — Riley, Head of Product
44
+ :::
39
45
  :::
40
46
  ::: quote
41
47
  > "The simplest stack we've ever shipped on."
42
- <p class="who">— Sam, CTO</p>
48
+
49
+ ::: who
50
+ — Sam, CTO
51
+ :::
43
52
  :::
44
53
  ::: quote
45
54
  > "Our marketing pages load instantly now."
46
- <p class="who">— Jordan, Growth</p>
55
+
56
+ ::: who
57
+ — Jordan, Growth
58
+ :::
47
59
  :::
48
60
  :endcarousel
49
61
 
50
62
  ::: cta-band #start
51
63
  ## Ready to build?
52
- <a class="cta" href="/">Start now</a>
64
+
65
+ [Start now](/){.cta}
53
66
  :::
54
67
 
55
- </main>
68
+ :::
@@ -1,11 +1,8 @@
1
- ---
2
- html: true
3
- ---
1
+ ::: nav
2
+ **My site**
4
3
 
5
- <nav>
6
- <strong>My site</strong>
7
- <span class="links">
8
- <a href="/">Home</a>
9
- <a href="/about/">About</a>
10
- </span>
11
- </nav>
4
+ ::: links
5
+ [Home](/)
6
+ [About](/about/)
7
+ :::
8
+ :::
@@ -1,27 +1,16 @@
1
1
  ---
2
2
  title: About
3
- html: true
4
3
  ---
5
4
 
6
- <nav>
7
- <strong>My site</strong>
8
- <span class="links">
9
- <a href="/">Home</a>
10
- <a href="/about/">About</a>
11
- </span>
12
- </nav>
13
-
14
- <main>
15
-
16
5
  # About
17
6
 
18
- This page is plain Markdown (`.md`) — strict CommonMark, zero framework JavaScript.
19
- Rename it to `.wd` when you want directives like state, loops, or forms — a `.wd`
20
- page can `@include /nav.wd` instead of repeating the markup above.
7
+ This page is plain Markdown (`.md`) — strict CommonMark, zero framework
8
+ JavaScript, and no raw HTML (Darkmown escapes it by default, so untrusted
9
+ content can't inject markup). That's why there's no shared nav bar here: a
10
+ `.md` page can't `@include` or use directives.
21
11
 
22
- The colocated `about.skin` styles this page; it attaches automatically by
23
- basename, `.md` or `.wd` alike.
12
+ Rename it to `.wd` when you want that — state, loops, forms, and an
13
+ `@include /nav.wd` for the shared nav. The colocated `about.skin` styles this
14
+ page either way; it attaches automatically by basename, `.md` or `.wd` alike.
24
15
 
25
16
  [Back home](/)
26
-
27
- </main>
@@ -32,31 +32,6 @@ main
32
32
  margin 0 auto
33
33
  padding 3rem 1.5rem
34
34
 
35
- nav
36
- display flex
37
- align-items center
38
- justify-content space-between
39
- gap 1rem
40
- max-width 760px
41
- margin 0 auto
42
- padding 1.25rem 1.5rem
43
-
44
- nav strong
45
- font-size 1.1rem
46
-
47
- nav .links
48
- display flex
49
- gap 1.25rem
50
-
51
- nav a
52
- color $ink
53
- text-decoration none
54
- opacity .8
55
-
56
- nav a:hover
57
- opacity 1
58
- color $accent
59
-
60
35
  a
61
36
  color $accent
62
37
 
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.6
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 760px
32
32
  margin 0 auto
33
33
  padding 3rem 1.5rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -41,19 +41,23 @@ nav
41
41
  margin 0 auto
42
42
  padding 1.25rem 1.5rem
43
43
 
44
- nav strong
44
+ .nav p
45
+ margin 0
46
+
47
+ .nav strong
45
48
  font-size 1.1rem
46
49
 
47
- nav .links
50
+ .nav .links p
48
51
  display flex
49
52
  gap 1.25rem
53
+ margin 0
50
54
 
51
- nav a
55
+ .nav a
52
56
  color $ink
53
57
  text-decoration none
54
58
  opacity .8
55
59
 
56
- nav a:hover
60
+ .nav a:hover
57
61
  opacity 1
58
62
  color $accent
59
63
 
@@ -1,11 +1,10 @@
1
1
  ---
2
- html: true
3
2
  title: My Darkmown site
4
3
  ---
5
4
 
6
5
  @include /nav.wd
7
6
 
8
- <main>
7
+ ::: main
9
8
 
10
9
  # My Darkmown site
11
10
 
@@ -25,4 +24,4 @@ Count: { count }
25
24
 
26
25
  :button "Add" -> todos += "Add a loop"
27
26
 
28
- </main>
27
+ :::
@@ -1,10 +1,7 @@
1
- ---
2
- html: true
3
- ---
1
+ ::: nav
2
+ **Shop**
4
3
 
5
- <nav>
6
- <strong>Shop</strong>
7
- <span class="links">
8
- <a href="/">Catalog</a>
9
- </span>
10
- </nav>
4
+ ::: links
5
+ [Catalog](/)
6
+ :::
7
+ :::
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.55
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 860px
32
32
  margin 0 auto
33
33
  padding 2rem 1.5rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -40,10 +40,13 @@ nav
40
40
  margin 0 auto
41
41
  padding 1.25rem 1.5rem
42
42
 
43
- nav strong
43
+ .nav p
44
+ margin 0
45
+
46
+ .nav strong
44
47
  font-size 1.1rem
45
48
 
46
- nav a
49
+ .nav a
47
50
  color $accent
48
51
  text-decoration none
49
52
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- html: true
3
2
  title: Shop — built with Darkmown
4
3
  description: A storefront in Markdown — live search, a shared cart, and a checkout that posts to a serverless function.
5
4
  transitions: true
@@ -7,7 +6,7 @@ transitions: true
7
6
 
8
7
  @include /nav.wd
9
8
 
10
- <main>
9
+ ::: main
11
10
 
12
11
  ::: section #shop
13
12
 
@@ -81,4 +80,4 @@ to { receipt.received.email }. Order { receipt.orderId }.
81
80
 
82
81
  :::
83
82
 
84
- </main>
83
+ :::