@zvndev/darkmown 2.7.1-next.14.g3f67070 → 2.9.0
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/AGENTS.md +78 -2
- package/CHANGELOG.md +24 -0
- package/README.md +82 -4
- package/docs/cli.md +19 -2
- package/docs/spec-alignment.md +18 -1
- package/package.json +8 -2
- package/site/_/footer.wd +4 -3
- package/site/_/nav.wd +3 -3
- package/site/pages/404.wd +2 -0
- package/site/pages/data.wd +1 -1
- package/site/pages/docs/index.wd +48 -2
- package/site/pages/index.js +17 -0
- package/src/api-runner.js +27 -6
- package/src/builder.js +83 -11
- package/src/catalog.js +21 -2
- package/src/cli.js +188 -2
- package/src/compiler/body.js +30 -1
- package/src/compiler/context.js +6 -0
- package/src/compiler/frontmatter.js +66 -0
- package/src/compiler/markdown.js +282 -2
- package/src/compiler/media.js +104 -9
- package/src/compiler/page.js +250 -12
- package/src/compiler.js +13 -3
- package/src/errors.js +23 -2
- package/src/feeds.js +14 -0
- package/src/image-variant-resize.js +71 -0
- package/src/image-variants.js +294 -0
- package/src/skin.js +135 -0
- package/src/statics.js +126 -12
- package/src/tools/validate.js +7 -2
- package/types/api-runner.d.ts +13 -2
- package/types/builder.d.ts +9 -0
- package/types/compiler/context.d.ts +13 -0
- package/types/compiler/frontmatter.d.ts +19 -0
- package/types/compiler/media.d.ts +20 -4
- package/types/compiler/page.d.ts +17 -3
- package/types/compiler.d.ts +5 -2
- package/types/feeds.d.ts +11 -0
- package/types/image-variants.d.ts +131 -0
- package/types/skin.d.ts +5 -0
- package/types/statics.d.ts +40 -1
- package/types/tools/validate.d.ts +9 -1
package/AGENTS.md
CHANGED
|
@@ -41,9 +41,10 @@ package.json
|
|
|
41
41
|
- Files/folders starting with `.`, `-`, or `_` are **hidden** from routing (drafts, partials).
|
|
42
42
|
- A `page.skin` next to `page.wd` attaches styling; a `page.js` attaches behavior. Matched by basename.
|
|
43
43
|
- **Colocated assets:** any other file under `site/pages/` (not a `.md`/`.wd` route, not 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`. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
|
|
44
|
+
- **Responsive image variants:** with sharp installed (`npm i -D sharp`, an optional dependency; builds work without it), the build scans `site/pages` for raster photos (jpg/jpeg/png/webp) over 700px wide and writes resized `name@640.ext` / `name@1280.ext` variants into `dist`, wiring a `srcset` onto the Markdown image with the original kept as `src`. Only widths below the intrinsic size are generated (never upscale), variants keep the source format, and EXIF orientation is applied. **Author-provided `name@<width>.ext` exports win per width**: your file is copied verbatim and measured from its own header (a `@640` file holding 600px emits `600w`), and generation fills only the uncovered widths; other sizes are hand exports on the probe list (64, 96, 128, …, 3840). Skipped silently: images ≤700px, `@width` files, hidden/symlinked paths, svg/gif/avif, and shelf images (the generator covers page-colocated images only). Without sharp the build behaves as before plus one `npm i -D sharp` hint per build; a corrupt image or a sharp failure on one file is a hint naming the file, never a build failure. Resizes cache in `node_modules/.cache/darkmown-images/` (content hash + width + format), so rebuilds and dev reloads do zero sharp work for unchanged images. Never wired: bound srcs (``), remote/`data:`/query-hash srcs, page-relative or extensionless srcs, and raw-HTML `<img>` tags; `sizes` is never emitted (raw `<img srcset sizes>` on an `html: true` page is the way). `.md` pages get the width/height/lazy hardening but no srcset. The first image on the page stays eager with `fetchpriority="high"`, the rest lazy-load, and author-set attributes always win.
|
|
44
45
|
- Shelf `.json` files publish to `/__wd/data/<name>`; every other shelf file publishes to `/__wd/media/<path>`.
|
|
45
46
|
|
|
46
|
-
Commands: `darkmown dev`, `darkmown build [--drafts]`, `darkmown serve`, `darkmown deploy <vercel|cloudflare>`, `darkmown catalog [--llms|--llms-full]`.
|
|
47
|
+
Commands: `darkmown dev`, `darkmown build [--drafts]`, `darkmown validate` (compile-check without writing `dist`), `darkmown serve` (previews `dist`, runs `api/` functions like dev, and compresses text responses), `darkmown deploy <vercel|cloudflare>`, `darkmown catalog [--llms|--llms-full]`.
|
|
47
48
|
|
|
48
49
|
## Frontmatter: the complete key set
|
|
49
50
|
|
|
@@ -58,6 +59,8 @@ lang: en
|
|
|
58
59
|
html: true
|
|
59
60
|
transitions: true
|
|
60
61
|
draft: true
|
|
62
|
+
noindex: true
|
|
63
|
+
icon: /icon.svg
|
|
61
64
|
date: 2026-09-04
|
|
62
65
|
updated: 2026-09-05
|
|
63
66
|
excerpt: A short summary for the feed.
|
|
@@ -72,6 +75,9 @@ rss_limit: 20
|
|
|
72
75
|
- `title`, `description`, `image` drive `<title>`, the meta description, and the Open Graph / Twitter card. `lang` sets `<html lang>` (default `en`).
|
|
73
76
|
- `html: true` allows raw HTML in that file's body. `transitions: true` opts into view transitions plus link prerendering.
|
|
74
77
|
- `draft: true` keeps the page out of `dist`, `routes.json`, the sitemap, and the feed. `darkmown dev` still serves it; `darkmown build --drafts` includes it.
|
|
78
|
+
- `noindex: true` builds the page but removes it from discovery: a robots `noindex,nofollow` meta, and exclusion from `sitemap.xml`, `rss.xml`, and the generated `llms.txt`/`llms-full.txt`. On a collection entry with a `_schema.wd`, declare it as `noindex: boolean?`.
|
|
79
|
+
- `icon: /path` sets the favicon verbatim (a `type` is added for `.svg`/`.png`/`.ico`). With no `icon:` key, the first of `site/pages/icon.svg`, `icon.png`, `favicon.ico` wins; with neither, the built-in mark is the fallback.
|
|
80
|
+
- `skins: [/base.skin]` (an inline array, same parsing as `tags:`) loads shared `.skin` files for that page only. Paths resolve like `@include` paths; the file must exist, end in `.skin`, and stay inside `site/pages`/`site/_`, or the build fails with `[WD017]`. One scoped skin per page: a shared `scoped` skin scopes to the whole page body.
|
|
75
81
|
- `date` marks the page a post (RSS item, sitemap `lastmod`, `og:type=article`). `updated` and `excerpt` refine that. `author` feeds an article `schema:`.
|
|
76
82
|
- `schema:` emits JSON-LD. One value, or a list. The closed set: `Article`, `BlogPosting`, `TechArticle`, `WebSite`, `Organization`. `organization:` and `logo:` support the `Organization` type. Anything else is a compile error.
|
|
77
83
|
- **Home page only:** `site_url: https://example.com` (no trailing slash) turns on `sitemap.xml`, `rss.xml`, and canonical URLs. `ai_crawlers: allow` (default) or `deny` flips every AI-crawler group in `robots.txt`. `rss_limit: 50` sets how many newest posts `rss.xml` carries (default 20).
|
|
@@ -95,6 +101,8 @@ Values resolve **inside a link or image destination** too, and every brace in th
|
|
|
95
101
|
|
|
96
102
|
Values in URL position are percent-encoded, and the runtime refuses a `javascript:`, `data:`, or `vbscript:` result by applying an empty attribute. Values resolve in **raw HTML** too (attributes and html blocks, HTML-escaped, `html: true` only), but raw HTML is a build-time paint, **not** a binding: `<a href="{ url }">` never updates, and the compiler warns. Fenced code and inline code spans are never rewritten.
|
|
97
103
|
|
|
104
|
+
**Build-time bindings** fold the build's date at compile time, anywhere a binding can appear: `{ $build.year }` (`2026`), `{ $build.date }` (`2026-10-13`), `{ $build.datetime }` (full ISO timestamp). They always fold, so a page using only `$build` bindings stays zero-JS; `© { $build.year }` in a footer include is the standard use. An unknown `$build.*` name stays literal text, and `.md` files are never folded.
|
|
105
|
+
|
|
98
106
|
### Format pipes: `{ value | name:arg }`
|
|
99
107
|
|
|
100
108
|
Shape a value for display. Pipes chain left to right and take literal arguments. They fold at build time for static values and re-apply on every render for `:state`/`:store` bindings, same syntax either way.
|
|
@@ -113,6 +121,8 @@ The whitelist, and nothing else: `money[:currency[:locale]]`, `number[:decimals]
|
|
|
113
121
|
|
|
114
122
|
A pipe only shapes a value the page can **resolve**. `{ 89 | money }` is not a name in scope, so it stays literal text, braces and all.
|
|
115
123
|
|
|
124
|
+
Pipes also fold inside **raw HTML** on an `html: true` page (attribute values and html blocks): a static value (a collection row, a JSON row, `meta`) formats at build time with full pipe semantics, so a static `@loop` with pipes in raw HTML stays zero-JS. A reactive value there can never fold, so it is painted once (or stays literal) with a build hint; before 2.8 it was emitted as literal text with no diagnostic.
|
|
125
|
+
|
|
116
126
|
## Directives — the complete set (nothing else exists)
|
|
117
127
|
|
|
118
128
|
Twenty-five directives. `@include`, `@loop`, `:::`, `:if`, `:state`, `:store`, `:computed`, `:fetch`, `:effect`, `:every`, `:theme`, `:button`, `:form`, `:input`, `:textarea`, `:select`, `:checkbox`, `:radio`, `:submit`, `:bind`, `:slider`, `:video`, `:audio`, `:embed`, `:carousel`. Block forms close with `@endloop`, `:endif`, `:endform`, `:endcarousel`, or a bare `:::`. Run `darkmown catalog` for the same list as JSON.
|
|
@@ -448,7 +458,7 @@ Volume: { volume }
|
|
|
448
458
|
:audio /track.mp3 controls
|
|
449
459
|
:embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny"
|
|
450
460
|
```
|
|
451
|
-
All three are **compile-time only** and emit no `data-wd-*`, so a media page stays zero-JS. `:video`/`:audio` take whitelisted flags (`controls`, `autoplay`, `loop`, `muted`, `playsinline`) and attributes (`poster`, `width`, `height`, `preload`); `autoplay` implies `muted`. `:embed` rewrites a YouTube/Vimeo URL to its no-cookie/player form inside a lazy, responsive 16/9 box; any other `http(s)` URL becomes a generic lazy iframe.
|
|
461
|
+
All three are **compile-time only** and emit no `data-wd-*`, so a media page stays zero-JS. `:video`/`:audio` take whitelisted flags (`controls`, `autoplay`, `loop`, `muted`, `playsinline`) and attributes (`poster`, `width`, `height`, `preload`); `autoplay` implies `muted`. `:embed` rewrites a YouTube/Vimeo URL to its no-cookie/player form inside a lazy, responsive 16/9 box; any other `http(s)` URL becomes a generic lazy iframe. `:embed` also takes `aspect=4:3` (any `<w>:<h>` ratio replaces 16/9), `height=400px` (pins the height and drops the ratio box; units `px`/`rem`/`em`/`vh`/`vw`/`%`, wins over `aspect=`), and `allow="payment clipboard-write"` (REPLACES the default iframe permissions; the whitelist is `payment`, `autoplay`, `encrypted-media`, `fullscreen`, `picture-in-picture`, `clipboard-write`, `camera`, `microphone`, `geolocation`, and `allow=""` strips all). A bad token is `[WD706]`, a bad ratio/length `[WD707]`.
|
|
452
462
|
|
|
453
463
|
### Carousel: `:carousel`
|
|
454
464
|
```wd
|
|
@@ -586,6 +596,10 @@ Always include: a tokens block, a real type scale, generous spacing, a `max-widt
|
|
|
586
596
|
|
|
587
597
|
**Syntax highlighting is free.** A fenced code block tagged with a language is highlighted at build time (no client JS), and its token classes read `$code-*` tokens from your skin, so highlighted code follows dark mode with no extra wiring.
|
|
588
598
|
|
|
599
|
+
**`@font-face` and `@keyframes` are skin at-rules.** Their indented children are font descriptors and keyframe selectors, not page selectors, so they compile as self-contained blocks. `@font-face` takes plain declarations (`font-family "Newsreader"`, `src url("/fonts/n.woff2") format("woff2")`, `font-weight 200 800`, `font-display swap`). `@keyframes <name>` requires a plain identifier; each indented child is a keyframe selector (`from`, `to`, a percentage, or a comma list) with its own declarations one level deeper, and anything else is a compile error. Aliases and `$token` refs work inside both, both stay **global** in a `scoped` skin (same contract as `tokens`), and both can nest under an `@media` line (the home for `prefers-reduced-motion` animation guards).
|
|
600
|
+
|
|
601
|
+
**Stylesheet order (changed in 2.8):** include skins compile first, then the page's `skins:` entries in listed order, and the page's own colocated `.skin` LAST, so the page wins ties at equal specificity. Before 2.8 the page's skin came first; delete any specificity hacks that only existed to beat the old order.
|
|
602
|
+
|
|
589
603
|
**Layout gotcha — CTAs and link rows.** Markdown wraps adjacent links/buttons on consecutive lines into a single `<p>`, so putting `display:flex` on their container won't space them. To lay out a row of buttons or nav links, wrap them in explicit raw HTML and style that (the page needs `html: true` in its frontmatter for the raw `<div>`/`<a>` to render — see the raw-HTML note above):
|
|
590
604
|
```wd
|
|
591
605
|
<div class="actions">
|
|
@@ -594,6 +608,8 @@ Always include: a tokens block, a real type scale, generous spacing, a `max-widt
|
|
|
594
608
|
</div>
|
|
595
609
|
```
|
|
596
610
|
|
|
611
|
+
**Sibling raw HTML collapses into one `<p>`.** Consecutive lines that each open with an inline-level or unknown tag (`<a class="card">`, `<picture>`, a custom element) parse as ONE paragraph: the elements render inline instead of as layout siblings, which collapses a grid to one column. Lines opening with a block-level tag (`<div>`, `<section>`, …) are hoisted by CommonMark into an HTML block and never hit this. The compiler warns once per run of 2+ such lines when the opening tag carries a `class` or `id` (classless prose links stay quiet), naming the count and the fix: wrap the run in a raw container (`<div class="grid">…</div>`). No warning inside a raw container, fenced code, blank-line-separated lines, or `{ state }`-led lines, and none on `.md` pages; a run inside a `:::` directive container DOES warn, because directive containers are not raw HTML.
|
|
612
|
+
|
|
597
613
|
### Page structure & hierarchy — required for styles to apply
|
|
598
614
|
- **Wrap every page's body in `<main>`.** Your `main { … }` rules (centering, `max-width`, page padding) are dead unless the element exists.
|
|
599
615
|
- **One `#` per page** (the H1/title). Use `##` for section headings (they render as `<h2>` — style those). Never emit two `<h1>`s.
|
|
@@ -614,6 +630,66 @@ wd.subscribe("slide", (i) => {
|
|
|
614
630
|
|
|
615
631
|
Set `window.wd.debug = true` to log any `:computed` or `@loop … where` expression that fails to evaluate.
|
|
616
632
|
|
|
633
|
+
Every bound control (`:bind`, `:slider`, and the bound `:select`/`:radio`/`:checkbox`) carries a `data-wd-bind-input="<key>"` attribute naming the exact state key it is bound to, section scope included (`data-wd-bind-input="ba1:ba1"`). That attribute is the supported way for escape-hatch code to find a bound input instead of guessing a key.
|
|
634
|
+
|
|
635
|
+
## Recipes from real rebuilds
|
|
636
|
+
|
|
637
|
+
Patterns that came out of rebuilding a 67-route marketing site in Darkmown. Each is short on purpose; steal the shape.
|
|
638
|
+
|
|
639
|
+
### Zero-JS nav menu
|
|
640
|
+
|
|
641
|
+
A `:state open` + `:button` menu forces the ~6.6 KB runtime onto every page that includes the nav, even pages that are otherwise static. If the page's only state is chrome state, a CSS-only disclosure menu keeps the whole page zero-JS:
|
|
642
|
+
|
|
643
|
+
```wd
|
|
644
|
+
---
|
|
645
|
+
title: Home
|
|
646
|
+
html: true
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
<nav class="menu">
|
|
650
|
+
<details class="menu-drawer">
|
|
651
|
+
<summary>Menu</summary>
|
|
652
|
+
<a href="/about/">About</a>
|
|
653
|
+
<a href="/services/">Services</a>
|
|
654
|
+
<a href="/contact/">Contact</a>
|
|
655
|
+
</details>
|
|
656
|
+
</nav>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Style `summary` as the hamburger and `.menu-drawer[open]` as the drawer in the shared skin (a checkbox + `:checked` sibling works the same way). The tradeoff to say out loud: CSS-only menus cost focus and ARIA quality (`aria-expanded` is not managed, there is no Escape-to-close), so once a page is reactive anyway, the `:button` menu is the better menu. The decision rule is which side of the runtime line the page already sits on.
|
|
660
|
+
|
|
661
|
+
### Fonts done right
|
|
662
|
+
|
|
663
|
+
Raw TTFs are the most expensive asset a marketing site ships: in the measured rebuild, three variable TTFs cost 1.07 MB on the homepage where the original site served 93 to 158 KB of subset woff2 for the same two families, roughly a 7x difference. The recipe:
|
|
664
|
+
|
|
665
|
+
1. Convert every font to **woff2** and **subset** it to the glyphs the site actually uses.
|
|
666
|
+
2. Serve it from the shelf (`site/_/fonts/`, published at `/__wd/media/fonts/`) or colocate it under `site/pages/`.
|
|
667
|
+
3. Declare it with `@font-face` in a shared skin, always with `font-display swap`.
|
|
668
|
+
4. Preload the one critical face with a raw `<link rel="preload" as="font" type="font/woff2" crossorigin>` on an `html: true` page, or an HTTP `Link` header on the host.
|
|
669
|
+
|
|
670
|
+
### Pre-fill a form message from query params
|
|
671
|
+
|
|
672
|
+
A booking page that arrives as `/request-appointment?service=whitening` should preselect and pre-fill. `:state from-url` reads the parameter, and a colocated `.js` composes the message, because the action whitelist deliberately cannot build strings:
|
|
673
|
+
|
|
674
|
+
```wd
|
|
675
|
+
:state service = "" from-url
|
|
676
|
+
:state message = "" from-url
|
|
677
|
+
|
|
678
|
+
:bind service placeholder="Service"
|
|
679
|
+
:bind message placeholder="How can we help?"
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
```js
|
|
683
|
+
// index.js, colocated beside the page
|
|
684
|
+
wd.subscribe("service", (service) => {
|
|
685
|
+
if (service && !wd.get("message")) {
|
|
686
|
+
wd.set("message", `I'd like to ask about ${service}.`);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
`wd.set` writes the state and the runtime reflects it into the bound input. If the script needs the element itself, select it with `[data-wd-bind-input="message"]`, which is the supported selector for bound inputs (a section-scoped key appears as its full `sectionId:name`).
|
|
692
|
+
|
|
617
693
|
## Compile errors: read the code, not just the prose
|
|
618
694
|
|
|
619
695
|
Every author-facing compile error opens with a stable `WDxxx` code and names the file and line:
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.9.0 (2026-10-13)
|
|
6
|
+
|
|
7
|
+
- **Authoring/Assets: responsive image variants are generated at build time.** With [sharp](https://sharp.pixelplumbing.com/) installed (`npm i -D sharp`, declared as an `optionalDependency`, so `dependencies` stays empty and every build works without it), the builder scans `site/pages` for raster photos (jpg/jpeg/png/webp) wider than 700px and writes resized `name@640.ext` / `name@1280.ext` siblings into `dist`, handing the compiler a variant map that wires the `srcset` exactly like author-provided variants: the original stays the `src` fallback and joins the srcset at its intrinsic width. Only widths below the intrinsic size are generated (never upscale), variants keep the source format (webp and jpeg at quality 82, jpeg progressive; png stays png at maximum compression), and EXIF orientation is applied before resizing, so rotated phone photos come out upright. **BEHAVIOR CHANGE: a build with sharp installed now generates variants automatically** where before it shipped the original alone; **without sharp, nothing changes** except one hint per build naming the images that could gain variants (`Run npm i -D sharp to generate them at build time.`). The 2.8.0 `@width` convention becomes the override: an author-provided `name@640.ext` is copied verbatim, measured from its own header, and never overwritten, and generation fills only the widths the author left uncovered. Skipped silently: images at or under 700px, `@width` files themselves, hidden and symlinked paths, svg/gif/avif, and shelf images under `site/_`. A corrupt image (dimensions unreadable) or a sharp failure on one file prints a hint naming that file; the build never fails because of an image. Resizes cache in `node_modules/.cache/darkmown-images/` keyed by content hash + width + format (already gitignored with `node_modules`), so a second build does zero sharp work and a fully cached build never loads sharp; dev rebuilds recompute the variant map from the cache, and changing an image forces a full rebuild, exactly as before. The build summary gains an `image variants (N generated, M cached)` clause whenever variants materialized. The build is synchronous and sharp is not, so the resizes run in one child process per build, and a fully cached build spawns nothing.
|
|
8
|
+
|
|
9
|
+
## 2.8.0 (2026-10-13)
|
|
10
|
+
|
|
11
|
+
A release from a real client-site rebuild: a 67-route marketing site rebuilt in Darkmown, with every friction point the rebuild reported either fixed or written down as a recipe. The `.skin` language learned `@font-face` and `@keyframes`, frontmatter gained favicon, shared-skin, and discovery opt-out keys, format pipes now fold inside raw HTML, and `darkmown serve` reached parity with dev for `api/` functions while adding response compression. One deliberate behavior change (the stylesheet cascade order) is called out below with its migration note. Three new error codes (`WD017`, `WD706`, `WD707`), no breaking API change.
|
|
12
|
+
|
|
13
|
+
- **`.skin` compiles `@font-face` and `@keyframes` blocks.** Their indented children are font descriptors and keyframe selectors, never page selectors, so they compile as self-contained at-rule blocks next to the existing `@media`/`@supports` support. `@font-face` takes plain `.skin` declarations (`src url("/fonts/n.woff2") format("woff2")`, `font-display swap`), several blocks per skin. `@keyframes <name>` requires a plain identifier, and each indented child must be a keyframe selector (`from`, `to`, a percentage, or a comma list) with its own declarations one level deeper; anything else fails the build with a file:line message, so a keyframe selector is never misread as a declaration. Aliases and `$token` refs work inside both, both nest under an `@media` line (the home for `prefers-reduced-motion` animation guards), and both stay global in a `scoped` skin, same contract as `tokens` blocks.
|
|
14
|
+
- **Shared page skins via `skins:` frontmatter.** `skins: [/base.skin, /marketing.skin]` (an inline array, same parsing as `tags:`) compiles extra `.skin` files for that page only, emitted under `/__wd/styles/` exactly like colocated skins, and tracked as build dependencies so dev incremental rebuilds recompile the page when a shared skin changes. Paths resolve like `@include` paths; a missing file, a non-`.skin` file, or a sandbox escape is the new `WD017`. A shared skin whose first line is `scoped` scopes to the whole page body, so the practical rule is one scoped skin per page. Works on `.md` pages too.
|
|
15
|
+
- **BEHAVIOR CHANGE: the page's own skin now emits LAST in the cascade.** Stylesheet links now emit the framework highlight sheet first (code pages only), then include skins in the order their includes compiled, then the page's `skins:` entries in listed order, then the page's colocated `.skin`. Before 2.8 the page's skin came first, so a page-level override of a design-system rule needed a higher-specificity selector. Now the page wins ties at equal specificity. Migration: a page rule that unknowingly relied on LOSING to an include rule at equal specificity now wins; delete specificity hacks that only existed to beat the old order. No pre-2.8 test pinned the old order.
|
|
16
|
+
- **Format pipes fold inside raw HTML.** `{ value | pipe }` in an attribute value or an html block on an `html: true` page now behaves exactly like pipes in prose: static values (collection rows, JSON-file rows, in-scope sources in static `@loop` bodies, page-level `meta`) fold at build time with chaining, literal args, and HTML-escaped output, so a static loop with pipes in raw HTML stays zero-JS. Before 2.8 the piped form was emitted literally, braces and all. A reactive value there still cannot fold (raw HTML is a one-time paint), and it now hits the existing build warning naming the position instead of failing silently. Unknown pipe names in raw HTML throw the same `WD006` as prose.
|
|
17
|
+
- **Build-time bindings: `{ $build.year }`, `{ $build.date }`, `{ $build.datetime }`.** Three always-static bindings fold the build's date at compile time (local year, local `YYYY-MM-DD`, full ISO timestamp), in prose, raw HTML, link destinations, and directive lines alike. They fold at source level, so a page using only `$build` bindings can never flip reactive. An unknown `$build.*` name stays literal text, and `.md` files are not folded, consistent with every other binding in `.md`. `© { $build.year }` in a footer include is the standard use.
|
|
18
|
+
- **The directive-lookalike hint no longer fires inside raw `<style>`/`<script>` blocks.** The "looks like a directive but matches none" hint used to fire on every `@font-face {` / `@media … {` / `:root {` line inside a raw `<style>` block on an `html: true` page; the rebuild produced 199 identical warnings across 67 pages. The body parser now tracks raw style/script blocks the way it already tracks fences, and suppresses the hint between the opener and the closer. Block contents render through untouched, and the hint still fires in ordinary prose and other raw HTML.
|
|
19
|
+
- **Responsive image variants via the `@width` convention.** Darkmown still resizes nothing; it wires the variants the author already exported. Pre-sized copies next to the source (`dr-x@640.webp`, `dr-x@1280.webp`, same directory, same extension) give a Markdown image a `srcset` with the original kept as `src`. Descriptor widths are MEASURED from each variant's file header (a `@640` file really holding 600px emits `600w`; the filename is only a fallback), candidates sort ascending, and the original joins at its intrinsic width when measurable. Discovery probes a fixed list of common widths (64 through 3840) through the reader contract's existence checks, so `compileFromMemory` resolves the convention too, and an off-list width is a documented miss, never an error. `sizes` is never emitted (raw `<img srcset sizes>` on an `html: true` page is the way). Never wired, by design: bound srcs the runtime repaints, remote/`data:`/query/hash sources, page-relative and extensionless srcs, and raw-HTML `<img>` tags. With no variant present the emitted tag is byte-identical to 2.7. The srcset composes with the existing load-priority split (first image eager with `fetchpriority="high"`, the rest `loading="lazy"`, author-set attributes always win). Caveat: wiring runs in the `.wd` path; a plain `.md` page gets the dimension/lazy hardening but no srcset.
|
|
20
|
+
- **A compile-time hint catches paragraph-wrapped sibling HTML.** In the rebuild, 16 insurance-carrier cards written as consecutive `<a class="card">` lines collapsed a grid to one column: lines that open with an inline-level or unknown tag parse as ONE paragraph and render inline instead of as layout siblings. Lines opening with a block-level tag (`<div>`, `<section>`, …) are hoisted by CommonMark and never hit this, which is why the hint keys on the outcome: when 2+ consecutive lines of one paragraph each open with a tag carrying a `class` or `id`, the build warns once per run with the count and the fix (wrap the run in a raw container, e.g. a `<div class="grid">`). Classless prose links, runs inside a raw container or fenced code, blank-line-separated lines, `{ state }`-led lines, and `.md` pages stay silent; a run inside a `:::` directive container warns, because directive containers are not raw HTML.
|
|
21
|
+
- **`icon:` frontmatter and a favicon convention.** `icon: /path` is emitted verbatim as `<link rel="icon" href="…">` with a `type` for the known extensions (`.svg`, `.png`, `.ico`). Without it, the first convention file in `site/pages` wins (`icon.svg`, then `icon.png`, then `favicon.ico`; page-colocated assets already copy to `dist/`, so `/icon.svg` just resolves). Without either, the built-in data-URI mark remains the byte-identical fallback.
|
|
22
|
+
- **`noindex: true` frontmatter.** Emits `<meta name="robots" content="noindex,nofollow">` and excludes the route from `sitemap.xml`, `rss.xml`, and the generated `llms.txt`/`llms-full.txt`. The page still builds and serves; it is only removed from discovery surfaces, so it composes with `draft:` the way you would expect. Collection entries using `noindex:` with a `_schema.wd` declare it as `noindex: boolean?`.
|
|
23
|
+
- **`:embed` takes `aspect=`, `height=`, and `allow=`.** `aspect=4:3` swaps the default 16:9 ratio box for any `<w>:<h>` ratio, `height=400px` pins the frame height and drops the ratio box (units `px`, `rem`, `em`, `vh`, `vw`, `%`; `height` wins when both are given), and `allow="payment"` REPLACES the default iframe permission set with tokens from a fixed whitelist (`payment`, `autoplay`, `encrypted-media`, `fullscreen`, `picture-in-picture`, `clipboard-write`, `camera`, `microphone`, `geolocation`; `allow=""` strips all). An embed with none of the new attributes emits byte-identical HTML to 2.7. New codes: `WD706` for an allow token outside the whitelist, `WD707` for a malformed ratio or length; `WD705`'s supported-attribute list now reads `title`, `height`, `aspect`, `allow`.
|
|
24
|
+
- **`darkmown validate` ships in the CLI, as documented.** The 2.7.0 entry described the command's warning headline before the command existed: only the `@zvndev/darkmown/tools` `validate()` implemented it. The CLI now wires that same validator into `src/cli.js`. `darkmown validate` compile-checks every route under `site/pages/` (drafts included) against the real compiler, with includes, colocated skins, `@loop` JSON data, and collection schemas resolved exactly as the build resolves them; an optional file or directory argument narrows the check. Clean pages print `compiles.` with their static/reactive status, warnings print as bullets under a `compiles with N warnings.` headline, and failures print the coded `WD` error with its file, line, corrective hint, and example. The exit code is 0 when everything compiles and 1 when any page fails, so CI can gate on it. To make that possible the tools `validate()` gained an optional third parameter forwarding compiler options (the CLI passes the project-wide collection index and the build's discovery pager vars through it); the two-argument call is unchanged.
|
|
25
|
+
- **`darkmown serve` runs the project's `api/` functions.** The preview server wires the same local runner dev uses in front of static serving: a request to `/api/*` matching a handler under the project's top-level `api/` directory executes it (same contract, same file routing, same error behavior as dev and the production hosts), and anything else falls through to static `dist` serving. `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos that worked in `darkmown dev` now work in the preview, and editing a handler mid-preview is picked up without a restart. A project with no `api/` directory behaves exactly as before.
|
|
26
|
+
- **`darkmown serve` compresses text responses.** The preview negotiates off the request's `Accept-Encoding`: brotli preferred, then gzip, with q-values honored (explicit `q=0` refusals and the `*` wildcard included). Compressible types are `text/*`, `application/javascript`, `application/json`, `application/xml`, and `image/svg+xml`, across static files, the built 404 page, and `api/` responses; images, video, audio, fonts, and `application/octet-stream` are never compressed, and a handler that set its own `Content-Encoding` is never double-compressed. Compressed responses carry `Vary: Accept-Encoding`, appended to (never clobbering) an existing `Vary`, and identity responses for compressible types send it too, so caches key the variants correctly. `darkmown dev` is intentionally uncompressed, byte-identical to 2.7.
|
|
27
|
+
- **Runtime coverage is measured and gated in a real browser.** The chromium e2e leg now collects line coverage for the served runtime and behavior modules (mapped through the committed sourcemap back to `src/runtime.js`) and gates it against thresholds in `.runtime-coverage-snapshot.json`: measured **89.07%** of runtime lines (644/723, threshold 85), **97.48%** of the sortable behavior (116/119, threshold 90), and **81.42%** of the carousel behavior (92/113, threshold 75). Each threshold sits a few points below its measured value so noise never flakes the gate but a real regression trips it, and the snapshot records what the uncovered lines are. The new `tests/e2e/interactions.spec.js` drives the drag, keyboard, and pointer paths that made the sortable number what it is.
|
|
28
|
+
|
|
5
29
|
## 2.7.0 (2026-09-04)
|
|
6
30
|
|
|
7
31
|
A release about silent failure. An audit ran the compiler, the builder and the runtime against one question: when this goes wrong, does the author find out? Too often the answer was no. A `{ url }` inside a link destination stayed literal text with no diagnostic. A line that was exactly `:state` rendered as prose. A static `@loop` over table rows produced paragraphs beside a header-only table. A `:every` inside a reactive loop registered one timer per row, and a removed row kept ticking. Every one of those now either compiles correctly or is refused with a coded error that names the fix. Separately, the runtime started shipping minified, which returned about 2 KB of the 8 KB budget, and this release spent it on five things that used to be "Darkmown cannot do that": attribute binding, the URL as state, bound fields outside a form, real server error messages, and file upload. The shipped runtime is **6584 B gzipped** against the unchanged **8192 B** budget. Thirteen new error codes, several behavior changes, and no breaking API change.
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ no install and no build step.
|
|
|
42
42
|
- [Quick start](#quick-start) · [Showcase](#showcase) · [Working from this repo](#working-from-this-repo) · [Commands](#commands)
|
|
43
43
|
- **Authoring:** [Authoring model](#authoring-model) · [Directive lines](#directive-lines) · [Interpolation](#interpolation) · [Frontmatter](#frontmatter) · [Static assets](#static-assets) · [SEO & feeds](#seo--feeds-sitemap-rss-robots) · [Loops](#loops) · [Content collections](#content-collections) · [Sections](#sections)
|
|
44
44
|
- **Reactivity:** [Reactive directives](#reactive-directives) · [Fetching data](#fetching-data) · [Global state](#global-state--store) · [URL as state](#url-as-state-from-url) · [Forms and persistence](#forms-and-persistence) · [Backends & deploy](#backends--deploy) · [Interactions](#interactions--slider-sortable-carousel)
|
|
45
|
-
- **Presentation:** [Inline attributes](#inline-attributes) · [Media](#media--video-audio-embed) · [Syntax highlighting](#syntax-highlighting) · [Dark mode](#dark-mode--tokens-dark) · [Scoped styles](#scoped-styles--scoped)
|
|
45
|
+
- **Presentation:** [Inline attributes](#inline-attributes) · [Media](#media--video-audio-embed) · [Syntax highlighting](#syntax-highlighting) · [Dark mode](#dark-mode--tokens-dark) · [Scoped styles](#scoped-styles--scoped) · [Skin at-rules](#skin-at-rules-font-face-and-keyframes)
|
|
46
46
|
- **Extending:** [The escape hatch](#the-escape-hatch) · [Programmatic compile](#programmatic-compile--compilefrommemory) · [Compile error codes](#compile-error-codes) · [AI authoring](#ai-authoring) · [Editor support](#editor-support)
|
|
47
47
|
- **Before you commit:** [Accessibility](#accessibility) · [Security](#security) · [**Limits**](#limits) · [Spec status](#spec-status)
|
|
48
48
|
|
|
@@ -72,8 +72,9 @@ npm run dev # live demo site — the same site that runs darkmown.com
|
|
|
72
72
|
- `darkmown init [dir] [--template <name>]` scaffolds a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`). It also drops `AGENTS.md` (the full directive reference), a `CLAUDE.md` pointing at it, and a `.gitignore` into the project root, so a coding agent working in your new project has the directive vocabulary in context from the first prompt.
|
|
73
73
|
- `darkmown dev` starts the live compiler with browser reload, an in-browser error overlay, and a local runner for `api/*` functions. Rebuilds are incremental: each dev build records a per-route dependency map (page file, includes, colocated assets, loop data, collections), so editing one file recompiles only the routes that depend on it — with a full rebuild on any uncertainty (a new/deleted/renamed file, or a framework `src/` change). Rebuilds run one at a time; edits landing mid-build batch into the next one.
|
|
74
74
|
- `darkmown build [--target cloudflare] [--drafts]` writes static output to `dist` (plus `sitemap.xml`/`rss.xml`/`robots.txt`); `--drafts` includes `draft: true` pages for staging.
|
|
75
|
+
- `darkmown validate [path]` compile-checks `site/pages` (or one file or directory) against the real compiler without writing `dist`. Each page reports `compiles.` with its static/reactive status, warnings as bullets, or the coded error with its file, line, and fix hint; drafts are included and the exit code is 1 when any page fails, so it gates CI.
|
|
75
76
|
- `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
|
|
76
|
-
- `darkmown serve` previews the built `dist` locally.
|
|
77
|
+
- `darkmown serve` previews the built `dist` locally. It also runs the project's `api/` functions (same handler contract and routing as `darkmown dev` and the production hosts, so `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos work in the preview), and it compresses text responses: brotli preferred, then gzip, negotiated off the request's `Accept-Encoding` with `Vary: Accept-Encoding` set. `darkmown dev` is deliberately uncompressed.
|
|
77
78
|
- `darkmown catalog [--llms|--llms-full]` prints the machine-readable directive catalog (JSON), a compact cheatsheet with `--llms`, or the complete reference including every error code with `--llms-full`. See [AI authoring](#ai-authoring).
|
|
78
79
|
- `darkmown version` prints the installed package version.
|
|
79
80
|
- `darkmown help` prints CLI usage.
|
|
@@ -107,6 +108,7 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
|
|
|
107
108
|
- Anything else stays literal text — braces in prose never break a page or pull in the runtime.
|
|
108
109
|
- Values resolve **inside a link or image destination** too (`[{ item.label }]({ item.url })`, ``), so an `@loop` can drive an `href`/`src` and the page stays static. Every brace in the destination resolves, not just one filling the whole value: `[Docs](/docs/{ region }/)` works. A reactive value there becomes a live binding; see [Destinations that bind](#destinations-that-bind) below.
|
|
109
110
|
- Values resolve in **raw HTML** too (attributes and html blocks, HTML-escaped) on an `html: true` page. Those are painted once at build time, not bound. Fenced code blocks and inline code spans are never rewritten, so a page can still show `` `[x](/p/{ p.slug }/)` `` as syntax.
|
|
111
|
+
- `{ $build.year }`, `{ $build.date }`, and `{ $build.datetime }` fold the build's date at compile time (`2026`, `2026-10-13`, and a full ISO timestamp), in prose, raw HTML, link destinations, and directive lines alike. They always fold, so a page using only `$build` bindings stays zero-JS, and a footer like `© { $build.year }` is the standard use. An unknown `$build.*` name stays literal text, and `.md` files are not folded (they have no interpolation at all).
|
|
110
112
|
|
|
111
113
|
### Destinations that bind
|
|
112
114
|
|
|
@@ -159,6 +161,19 @@ A pipe shapes a value that is **in scope** — a loop variable, `:state`/`:store
|
|
|
159
161
|
|
|
160
162
|
The five **aggregates** double as pipes over a list — `{ cart \| count }`, `{ cart \| sum:"price" \| money }`, plus `avg` / `min` / `max` — and `join:", ":"name"` flattens a list of rows to a string. `date`/`time`/`datetime` are `Intl`-backed (`short` / `medium` / `long`); there is deliberately **no relative "time ago"** formatter, so builds stay byte-for-byte reproducible. An unknown pipe name is a compile error with the valid list.
|
|
161
163
|
|
|
164
|
+
Pipes also fold inside **raw HTML** on an `html: true` page (an attribute value or an html block), with the same chaining and literal arguments as prose. A static value there, a collection row, a JSON-file row, or `meta`, formats at build time and stays zero-JS:
|
|
165
|
+
|
|
166
|
+
```wd-fragment
|
|
167
|
+
@loop blog into post sort by post.date desc
|
|
168
|
+
<div class="card">
|
|
169
|
+
<h2>{ post.title }</h2>
|
|
170
|
+
<p>{ post.date | date:"medium" }</p>
|
|
171
|
+
</div>
|
|
172
|
+
@endloop
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A **reactive** value (`:state`/`:store`, a reactive loop row) can never fold in raw HTML, because raw HTML is a one-time paint with no element for the runtime to mark. Since 2.8 that hits a build hint naming the position (a `:state` pipe paints its seed once; a reactive row pipe stays literal) instead of the old behavior, which emitted the whole `{ … | pipe }` expression as literal text with no diagnostic. Move the value into ordinary text or a Markdown destination to make it live.
|
|
176
|
+
|
|
162
177
|
## Frontmatter
|
|
163
178
|
|
|
164
179
|
YAML-style `key: value` frontmatter between `---` fences. Values are strings, plus inline arrays:
|
|
@@ -176,6 +191,15 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
176
191
|
|
|
177
192
|
Three keys also drive the document `<head>`: `title` sets `<title>`, `description` adds the meta description plus Open Graph / Twitter tags, and `image` (an absolute URL) sets the social-share preview (`og:image` / `twitter:image` and a `summary_large_image` card). `lang:` sets the document language on `<html lang>` (`lang: fr`, `lang: pt-BR`, …) — it defaults to `en`.
|
|
178
193
|
|
|
194
|
+
Two more head keys arrived in 2.8:
|
|
195
|
+
|
|
196
|
+
- **`icon: /path`** sets the favicon, emitted verbatim as `<link rel="icon" href="…">` with a `type` for the known extensions (`.svg`, `.png`, `.ico`). Without it, a convention file in `site/pages` wins, first hit in the order `icon.svg`, `icon.png`, `favicon.ico` (page-colocated assets already copy to `dist/`, so `/icon.svg` just resolves). Without either, the built-in data-URI mark stays the fallback.
|
|
197
|
+
- **`noindex: true`** keeps a built page out of every discovery surface: it emits `<meta name="robots" content="noindex,nofollow">` and excludes the route from `sitemap.xml`, `rss.xml`, and the generated `llms.txt` / `llms-full.txt`. The page itself still builds and serves; it is only removed from discovery. (A `noindex:` field on a collection entry with a `_schema.wd` must be declared in the schema as `noindex: boolean?`, same as any other key.)
|
|
198
|
+
|
|
199
|
+
And one key moves styling into frontmatter: **`skins: [/base.skin, /marketing.skin]`** loads extra `.skin` files for just that page. Entries resolve like `@include` paths (a leading `/` checks the `site/_/` shelf then `site/pages/`; a relative path checks beside the page first), must exist, must end in `.skin`, and must stay inside the project sandbox, with violations reported as `[WD017]`. A shared skin whose first line is `scoped` scopes to the whole page body, so the practical rule is one scoped skin per page.
|
|
200
|
+
|
|
201
|
+
**Cascade order changed in 2.8 (behavior change).** A page's stylesheet links now emit in this order: the framework highlight sheet (code pages only), then include skins in the order their includes compiled, then the page's `skins:` entries in listed order, and **the page's own colocated `.skin` last**, so the page wins ties at equal specificity. Before 2.8 the page's own skin came first and an override of an include rule needed a higher-specificity selector. A page rule that unknowingly relied on losing to an include rule now wins; the migration is to delete specificity hacks that only existed to beat the old order.
|
|
202
|
+
|
|
179
203
|
A few reserved keys drive **drafts and feeds** (see [SEO & feeds](#seo--feeds-sitemap-rss-robots) below): `draft: true` excludes a page from production builds; `site_url` on the home page turns on `sitemap.xml` + `rss.xml`; `rss_limit:` on the home page caps how many posts the feed carries; `date:` marks a page as a blog post (it lands in `rss.xml` and sets the page's `<lastmod>`); `excerpt:` is the RSS summary. The same frontmatter is queryable when the page is an entry in a [content collection](#content-collections): `{ post.date }`, `{ post.excerpt }`, `{ post.tags }`, and any custom key resolve in an `@loop` over the folder.
|
|
180
204
|
|
|
181
205
|
`transitions: true` opts a page into **instant, flash-free navigation** — zero JavaScript, all declarative. It emits a directional fade+slide **view transition** for the page swap (old lifts up and out, new rises up and in — replacing the default cross-fade, which left both pages ghosted at ~50 % opacity mid-navigation), plus a **`<script type="speculationrules">` prerender** hint that renders the next same-origin page on hover/pointerdown so the click activates an already-painted page (no white render-gap flash). It honors `prefers-reduced-motion`. Only same-origin pages that both opt in transition; browsers without support — or with page-preloading disabled — navigate normally. Off by default; opt out with `transitions: false`. Mark a link `{.no-prefetch}` to exclude it from prerendering. (Chrome disables prerendering while DevTools is open, so test the built site with DevTools closed.)
|
|
@@ -191,6 +215,24 @@ Reference them with a normal URL: ``. The preview/`
|
|
|
191
215
|
|
|
192
216
|
**Images are hardened at compile time.** Every `<img>` the compiler emits is stamped with its intrinsic `width`/`height` (read from the source file, so the browser reserves space and the page doesn't reflow as images decode), `decoding="async"`, and a load-priority split — the first image on the page stays eager with `fetchpriority="high"` (the LCP candidate), the rest get `loading="lazy"`. Author-set attributes always win, and remote or unreadable sources just skip the dimensions. The compiler **measures**, it doesn't resize — keep source images web-sized (modern formats, sensible dimensions).
|
|
193
217
|
|
|
218
|
+
**Responsive variants, generated at build time.** Drop one photo into `site/pages` and the build does the rest: with [sharp](https://sharp.pixelplumbing.com/) installed (`npm i -D sharp`, an optional dependency; every build works without it), each raster image large enough to matter is resized to the default widths **640 and 1280**, written into `dist/` as `name@<width>.ext` siblings, and the Markdown image that references the original gains a `srcset`:
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<img src="/doctors/dr-aurora.jpg" alt="Dr. Aurora"
|
|
222
|
+
srcset="/doctors/dr-aurora@640.jpg 640w, /doctors/dr-aurora@1280.jpg 1280w, /doctors/dr-aurora.jpg 2400w"
|
|
223
|
+
width="2400" height="1600" decoding="async" fetchpriority="high">
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- **Only widths below the intrinsic size are generated (never upscale)**, and variants keep the source format: webp re-encodes at quality 82, jpeg at quality 82 progressive, png stays png. EXIF orientation is applied before resizing, so rotated phone photos come out upright. (The build is synchronous and sharp is not, so the resizes run in one child process per build; a fully cached build spawns nothing.)
|
|
227
|
+
- **Skipped silently:** images at or under 700px wide (already small everywhere), `name@<width>.ext` files themselves (output, never sources), hidden path segments (`.`, `-`, `_`) and symlinks (matching what the build publishes), and formats that are not resized (svg, gif, avif). The generator scans `site/pages` only; shelf images under `site/_` are not resized, though author-provided shelf variants still wire through the convention below.
|
|
228
|
+
- **Without sharp, the build behaves exactly as before plus one hint per build:** `hint: 5 images could gain responsive variants (site/pages/carousel/slide-1.jpg, ...). Run npm i -D sharp to generate them at build time.` A corrupt image (dimensions unreadable) or a sharp failure on one file prints a hint naming that file, and that image ships without variants; the build never fails because of an image.
|
|
229
|
+
- **Resizes are cached in `node_modules/.cache/darkmown-images/`**, keyed by content hash + width + format. A second build does zero sharp work for unchanged images and does not even load sharp when everything is cached (cached variants ship even if sharp is later removed). The cache lives under `node_modules`, which every project's `.gitignore` already excludes. Dev rebuilds recompute the variant map from the cache (no sharp spawn), so a partially rebuilt page is byte-identical to a full rebuild, srcset included; changing an image itself forces a full rebuild, which re-runs the pre-pass.
|
|
230
|
+
- The build summary reports the pre-pass when variants materialized: `Built 14 routes, sitemap (14 urls), rss (6 posts), image variants (2 generated, 5 cached) into dist`.
|
|
231
|
+
|
|
232
|
+
**Author-provided variants win (the `@width` convention).** Export `dr-aurora@640.jpg` yourself, same directory, same extension, and the build does not touch that width: your file is copied verbatim and measured from its own header (a `@640` file really holding 600px emits `600w`). Generation fills only the widths you did not provide, and the convention picks up any width on the compiler's fixed probe list (64, 96, 128, …, 3840), so other sizes are a hand export away; an off-list width like `x@500.webp` is a documented miss, never an error. Variant files (yours or generated) are ordinary colocated assets: they copy to `dist/` at their own path, and an `@`-suffixed filename is never a route. Everything else about the wiring is unchanged: descriptor widths are measured from file headers, candidates sort ascending with the original joining at its intrinsic width, `sizes` is never emitted (write raw `<img srcset sizes>` on an `html: true` page for that), and a page with no resizable images emits exactly what it did before.
|
|
233
|
+
|
|
234
|
+
**Never wired, by design:** a bound src (`` or a loop row's ``, which the runtime repaints), remote (`https://`, protocol-relative) and `data:` sources, srcs with a query/hash, page-relative or extensionless srcs, and raw-HTML `<img>` tags. **`.md` caveat:** variant wiring runs in the `.wd` compile path, so a plain `.md` page gets the width/height/lazy hardening but no srcset.
|
|
235
|
+
|
|
194
236
|
**Or colocate an asset next to the page that uses it.** Any non-page file under `site/pages/` (anything but `.md`/`.wd` routes and the colocated `.skin`/`.js`) is copied to `dist/` at its own path, so a clean relative URL just works:
|
|
195
237
|
|
|
196
238
|
- `site/pages/logo.svg` → `dist/logo.svg` → ``
|
|
@@ -317,7 +359,7 @@ draft: true
|
|
|
317
359
|
|
|
318
360
|
Drafts are filtered at one point — route discovery — so nothing downstream ever sees them. This is **separate from** the permanent `.`/`-`/`_` filename hiding: a hidden name (`-notes.wd`) is private forever; `draft:` is a toggle you flip when the page is ready to ship.
|
|
319
361
|
|
|
320
|
-
The `build` summary reports what it emitted: `Built 14 routes, sitemap (14 urls), rss (6 posts) into dist
|
|
362
|
+
The `build` summary reports what it emitted: `Built 14 routes, sitemap (14 urls), rss (6 posts) into dist`, with an `image variants (2 generated, 5 cached)` clause appended whenever the image pre-pass produced or served cached responsive variants (see [Static assets](#static-assets)).
|
|
321
363
|
|
|
322
364
|
## Loops
|
|
323
365
|
|
|
@@ -1137,6 +1179,14 @@ The block must follow the element with no space, and works on links, images, and
|
|
|
1137
1179
|
|
|
1138
1180
|
**Headings get anchors for free.** Every markdown heading carries a stable, GitHub-style slug `id` at build time (lowercased, punctuation stripped, whitespace → hyphens; duplicates dedupe with `-1`/`-2` suffixes across the whole document), so any section of any page is deep-linkable with a plain `#the-slug` fragment — zero JS. The [docs page's](https://darkmown.com/docs/) "On this page" table of contents is just markdown links to those anchors.
|
|
1139
1181
|
|
|
1182
|
+
**Consecutive raw-HTML lines collapse into one paragraph.** On an `html: true` page, a run of consecutive lines that each start with an inline-level or unknown tag (`<a class="card">`, `<picture>`, a custom element) parses as ONE paragraph: the elements render inline inside a single `<p>` instead of as layout siblings, which is how a card grid collapses to one column. Lines starting with a block-level tag (`<div>`, `<section>`, `<article>`, …) never hit this, because CommonMark hoists them into an HTML block even mid-paragraph, so sixteen consecutive `<div class="card">` lines render fine. Since 2.8 the compiler warns once per run when 2+ consecutive lines of one paragraph each open with a component-looking tag, meaning one that carries a `class` or `id` (classless `<a href>` prose links are meant to share a paragraph and stay quiet):
|
|
1183
|
+
|
|
1184
|
+
```
|
|
1185
|
+
site/pages/index.wd:6: 16 sibling HTML block lines will render inline in one <p>; wrap them in a container (e.g. a raw <div>)
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
The fix is the wrap: put the run inside a raw `<div class="grid">…</div>`, which is one HTML block and renders as real siblings. The hint stays silent for runs already inside a raw container, inside fenced code, separated by blank lines, led by a `{ state }` binding, and on `.md` pages; a run inside a `:::` directive container DOES warn, because directive containers are not raw HTML and the collapse is real there too.
|
|
1189
|
+
|
|
1140
1190
|
## Media — `:video`, `:audio`, `:embed`
|
|
1141
1191
|
|
|
1142
1192
|
Three one-line directives replace hand-written `<video>` / `<iframe>` markup. They are **compile-time only** — they emit no `data-wd-*`, so a media-only page still ships **zero** framework JavaScript.
|
|
@@ -1148,7 +1198,7 @@ Three one-line directives replace hand-written `<video>` / `<iframe>` markup. Th
|
|
|
1148
1198
|
```
|
|
1149
1199
|
|
|
1150
1200
|
- **`:video` / `:audio`** compile to a hardened HTML5 player. `preload="metadata"` and `controls` are added by default; flags (`controls`, `autoplay`, `loop`, `muted`, `playsinline`) and attributes (`poster`, `width`, `height`, `preload`) are validated against a per-element whitelist, and `autoplay` silently implies `muted` (browsers block sound-on autoplay). The `src`/`poster` URLs run through the same scheme guard as `:fetch` — relative or `http(s)` only.
|
|
1151
|
-
- **`:embed`** rewrites a YouTube or Vimeo URL to its **no-cookie / player** form, wraps it in a responsive `16/9` box, and marks the iframe `loading="lazy"` with a locked-down `referrerpolicy`. Any other `http(s)` URL becomes a generic lazy iframe. Add `title="…"` for the accessible name.
|
|
1201
|
+
- **`:embed`** rewrites a YouTube or Vimeo URL to its **no-cookie / player** form, wraps it in a responsive `16/9` box, and marks the iframe `loading="lazy"` with a locked-down `referrerpolicy`. Any other `http(s)` URL becomes a generic lazy iframe. Add `title="…"` for the accessible name. Three more attributes shape the frame (an embed with none of them emits byte-identical HTML to before): `aspect=4:3` swaps the ratio box for any `<w>:<h>` ratio, `height=400px` pins the frame height and drops the ratio box (units `px`, `rem`, `em`, `vh`, `vw`, `%`; `height` wins when both are given), and `allow="payment"` **replaces** the default iframe permission set with your own tokens from a fixed whitelist: `payment`, `autoplay`, `encrypted-media`, `fullscreen`, `picture-in-picture`, `clipboard-write`, `camera`, `microphone`, `geolocation` (`allow=""` strips all permissions). An unknown attribute is `[WD705]`, an unknown allow token `[WD706]`, and a malformed ratio or length `[WD707]`.
|
|
1152
1202
|
|
|
1153
1203
|
Darkmown's shipped CSP pre-authorizes exactly the two embed origins (`youtube-nocookie.com`, `player.vimeo.com`) and `media-src 'self' https:`, so embeds and remote media work out of the box on the bundled server, Cloudflare `_headers`, and Vercel.
|
|
1154
1204
|
|
|
@@ -1249,6 +1299,7 @@ A second component's `.card` gets a **different** id, so the two never collide.
|
|
|
1249
1299
|
| `tokens` / `tokens dark` / `tokens [data-theme=…]` | **Global** — design tokens always emit on `:root`, so `$accent` and dark mode keep working site-wide |
|
|
1250
1300
|
| `:global(.toast)` (whole selector) | **Opts back out** — emits a plain, unscoped `.toast { … }` |
|
|
1251
1301
|
| `@media` / `@supports` wrappers | Untouched; only the inner rule's selector is scoped |
|
|
1302
|
+
| `@font-face` / `@keyframes` | **Global**, never stamped with the scope attribute, same contract as `tokens` |
|
|
1252
1303
|
| `page` / `*` / `html` / `body` / `::selection` | **Compile error** — page-level styles belong in a global skin, not a scoped one |
|
|
1253
1304
|
|
|
1254
1305
|
Where the scope applies depends on **where the skin lives**:
|
|
@@ -1262,6 +1313,33 @@ Where the scope applies depends on **where the skin lives**:
|
|
|
1262
1313
|
- **Unused selectors warn, they aren't removed.** If a scoped selector's class/element/id never appears in the stamped subtree, the build prints `hint: scoped selector ".badge" in card.skin matches no element` — but the rule is **kept** (a colocated `.js` may add the class at runtime). It's a typo nudge, not a dead-code remover.
|
|
1263
1314
|
- **Scoping is opt-in.** Every existing `.skin` (no `scoped` marker) is byte-for-byte unchanged.
|
|
1264
1315
|
|
|
1316
|
+
## Skin at-rules: `@font-face` and `@keyframes`
|
|
1317
|
+
|
|
1318
|
+
A `.skin` already wraps nested rules in `@media` and `@supports`. Two more CSS at-rules compile as self-contained blocks, because their indented children are not page selectors: `@font-face` takes font descriptors, and `@keyframes` takes keyframe selectors.
|
|
1319
|
+
|
|
1320
|
+
```skin
|
|
1321
|
+
@font-face
|
|
1322
|
+
font-family "Newsreader"
|
|
1323
|
+
src url("/fonts/newsreader.woff2") format("woff2")
|
|
1324
|
+
font-weight 200 800
|
|
1325
|
+
font-display swap
|
|
1326
|
+
|
|
1327
|
+
@keyframes fade-in
|
|
1328
|
+
from
|
|
1329
|
+
opacity 0
|
|
1330
|
+
50%, 75%
|
|
1331
|
+
opacity .5
|
|
1332
|
+
to
|
|
1333
|
+
opacity 1
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
- `@font-face` takes a bare line with plain `.skin` declarations indented beneath it, and several blocks per skin are allowed. A name on the `@font-face` line, a block with no declarations, or a nested rule inside one is a compile error.
|
|
1337
|
+
- `@keyframes <name>` requires a plain CSS identifier. Each indented child is a keyframe selector, `from`, `to`, a percentage (`50%`, `12.5%`), or a comma list (`50%, 75%`), with its declarations one level deeper. A child that matches none of those is a compile error, so a keyframe selector is never misread as a declaration or an element selector.
|
|
1338
|
+
- Both run the normal declaration pipeline: property aliases (`bg`, `radius`, `font`) and `$token` refs work inside them. Indented under an `@media` line, the emitted block is wrapped in that query, which is the right home for `@media (prefers-reduced-motion: no-preference)` animations.
|
|
1339
|
+
- **Both stay global in a `scoped` skin**, the same contract as `tokens` blocks: font and keyframe names are page-global by nature, so the blocks are never stamped with the `data-wd-scope` attribute. Ordinary selectors in the same file are still scoped.
|
|
1340
|
+
|
|
1341
|
+
Since 2.8 the body parser also tracks raw `<style>` and `<script>` blocks on `html: true` pages, so `@font-face {` or `@media … {` lines inside a raw `<style>` no longer trip the "looks like a directive" hint on every line; the hint still fires for directive-shaped lines in ordinary prose and other raw HTML.
|
|
1342
|
+
|
|
1265
1343
|
## The escape hatch
|
|
1266
1344
|
|
|
1267
1345
|
Reactive pages expose `window.wd` so a colocated `.js` file can do anything the directives deliberately don't — keyboard, drag/touch, canvas, charts, maps:
|
package/docs/cli.md
CHANGED
|
@@ -85,6 +85,18 @@ Static pages do not receive `/__wd/runtime.js`; reactive pages do. Pay-for-what-
|
|
|
85
85
|
|
|
86
86
|
When a `.md` file contains `.wd` syntax (directives, includes, loops), the build prints a hint suggesting a rename to `.wd` — the syntax stays plain text in `.md` by design.
|
|
87
87
|
|
|
88
|
+
### `darkmown validate [path]`
|
|
89
|
+
|
|
90
|
+
Compile-checks pages against the real compiler without writing `dist`. With no argument it checks every route under `site/pages/`, drafts included (a broken draft breaks `darkmown dev`, so it counts). A file argument checks one page or shelf include; a directory argument checks one subtree.
|
|
91
|
+
|
|
92
|
+
Each page prints one line in the same vocabulary as the `validate` tool: `compiles. static (ships zero JavaScript), 1 skin.` for a clean page, `compiles with N warnings.` with a bullet per warning when the compiler has something non-fatal to say, or the coded `WD` error with its file, line, corrective `use:` template, and a concrete `e.g.` line. Includes, colocated `.skin` files, `@loop` JSON data, and collection `_schema.wd` files are resolved exactly the way the build resolves them, so a page that validates is a page that builds.
|
|
93
|
+
|
|
94
|
+
The exit code is 0 when everything compiles and 1 when any page fails, so it gates CI:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
darkmown validate && darkmown build
|
|
98
|
+
```
|
|
99
|
+
|
|
88
100
|
### `darkmown deploy <vercel|cloudflare> [--prod]`
|
|
89
101
|
|
|
90
102
|
Builds (target-aware) and deploys to the platform by wrapping its CLI:
|
|
@@ -96,7 +108,12 @@ The deploy URL is printed when the CLI reports it. If the platform CLI isn't sig
|
|
|
96
108
|
|
|
97
109
|
### `darkmown serve`
|
|
98
110
|
|
|
99
|
-
Serves the already-built `dist` directory for local preview
|
|
111
|
+
Serves the already-built `dist` directory for local preview. Run `darkmown build` first.
|
|
112
|
+
|
|
113
|
+
Since 2.8 the preview is at parity with dev for two things:
|
|
114
|
+
|
|
115
|
+
- **`api/` functions run.** A request to `/api/*` that matches a handler under the project's top-level `api/` directory executes it (same contract and file routing as `darkmown dev` and the production hosts: `api/echo.js` → `/api/echo`, `api/users/[id].js` → `/api/users/:id`); anything else falls through to static serving. `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos that worked in dev now work in the preview too, and editing a handler is picked up without a restart. A project with no `api/` directory behaves exactly as before.
|
|
116
|
+
- **Text responses are compressed.** The server negotiates off the request's `Accept-Encoding` (brotli preferred, then gzip, q-values honored) and sets `Vary: Accept-Encoding`, appending to an existing `Vary` rather than clobbering it. Compressible: `text/*`, `application/javascript`, `application/json`, `application/xml`, and `image/svg+xml`, across static files, the built 404 page, and `api/` responses (a handler that set its own `Content-Encoding` is never double-compressed). Images, video, audio, and fonts are never compressed. `darkmown dev` is intentionally uncompressed, byte-identical to 2.7.
|
|
100
117
|
|
|
101
118
|
### `darkmown catalog [--llms|--llms-full]`
|
|
102
119
|
|
|
@@ -126,7 +143,7 @@ export default async function (request, context) {
|
|
|
126
143
|
}
|
|
127
144
|
```
|
|
128
145
|
|
|
129
|
-
`api/users/[id].js` maps to `/api/users/:id` (`context.params.id`). These run in `darkmown dev` (local runner), on **Vercel** natively, and on **Cloudflare Pages** via the `dist/_worker.js` that `darkmown deploy cloudflare` emits. `:fetch`/`:form` point at `/api/*` with no extra config. Need a Node-only API on Vercel? Drop `export const config` and use `(req, res)
|
|
146
|
+
`api/users/[id].js` maps to `/api/users/:id` (`context.params.id`). These run in `darkmown dev` and in `darkmown serve` (same local runner), on **Vercel** natively, and on **Cloudflare Pages** via the `dist/_worker.js` that `darkmown deploy cloudflare` emits. `:fetch`/`:form` point at `/api/*` with no extra config. Need a Node-only API on Vercel? Drop `export const config` and use `(req, res)`, noting that local-dev parity is then lost for that function.
|
|
130
147
|
|
|
131
148
|
**Custom server / remote backend.** Point `:fetch`/`:form` at an absolute URL (`https://api.example.com/…`) and widen the CSP `connect-src` (and `form-action` for native form POSTs) in `vercel.json` / `dist/_headers` / your serve config. Darkmown still ships static; only the requests leave your origin.
|
|
132
149
|
|
package/docs/spec-alignment.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
This audit compares the current Darkmown implementation with the original MARROW/WhateverDown vision and the follow-up decisions made on 2026-06-11.
|
|
4
4
|
|
|
5
|
+
## 2.9.0 additions (unreleased, 2026-10-13)
|
|
6
|
+
|
|
7
|
+
- **The framework now makes the image variants itself, closing the rebuild's largest asset gap.** With the optional sharp dependency installed, `darkmown build` resizes page-colocated raster photos over 700px into 640/1280 `name@width.ext` siblings (never upscaling, EXIF-aware, cached by content hash under `node_modules/.cache/darkmown-images/` so rebuilds and dev reloads do no sharp work), the 2.8.0 author-provided `@width` convention stays the per-width override, and a build without sharp behaves exactly like 2.8.0 plus one hint.
|
|
8
|
+
|
|
9
|
+
## 2.8.0 additions: the client-site rebuild release (2026-10-13)
|
|
10
|
+
|
|
11
|
+
A 67-route marketing site rebuilt in Darkmown produced a friction report, and this release is the report answered: every item either became a feature, a coded error, or a documented recipe. Nothing in it moves the core thesis; it rounds out the authoring surface a real design system needs while keeping static pages zero-JS.
|
|
12
|
+
|
|
13
|
+
- **The `.skin` language covers a design system's last two at-rules.** `@font-face` and `@keyframes` compile as self-contained blocks (their children are descriptors and keyframe selectors, not page selectors), with the alias/`$token` pipeline inside, `@media` composition, and global-by-contract behavior in `scoped` skins. The rebuild's workaround (plain CSS assets linked by hand) is no longer needed.
|
|
14
|
+
- **Pages describe their own chrome.** `skins: [/base.skin]` shares skins per page (`WD017` on a bad entry), `icon:` plus the `icon.svg`/`icon.png`/`favicon.ico` convention replaces the hardcoded data-URI mark, and `noindex: true` removes a built page from the robots meta, sitemap, feed, and llms corpus. The cascade order flipped deliberately: include skins, then shared skins, then the page's own skin last, so a page override no longer needs a specificity hack.
|
|
15
|
+
- **Interpolation keeps closing its silent corners.** Format pipes fold inside raw HTML for static values (a static loop with pipes in raw HTML stays zero-JS), reactive values there paint once with a named hint instead of staying silently literal, and `{ $build.year }` / `{ $build.date }` / `{ $build.datetime }` fold the build date at source level so they can never flip a page reactive. The directive-lookalike hint no longer fires inside raw `<style>`/`<script>` blocks (199 duplicate warnings on the rebuild).
|
|
16
|
+
- **`:embed` grew sizing and permission attributes** (`aspect=`, `height=`, `allow=`, with `WD706`/`WD707` guarding them) because the rebuild needed a checkout iframe and a 4:3 video; defaults remain byte-identical.
|
|
17
|
+
- **The preview server caught up with dev.** `darkmown serve` runs `api/` functions through the same local runner and negotiates brotli/gzip for text responses with correct `Vary` handling. `darkmown validate` shipped in the CLI, closing the 2.7.0 gap where the docs described a command that existed only in the tools package.
|
|
18
|
+
- **Runtime claims are now measured.** Browser-collected line coverage is gated per artifact: 89.07% of the runtime, 97.48% of sortable, 81.42% of carousel, with thresholds recorded beside what the uncovered lines are. The runtime budget and the zero-JS default are unchanged.
|
|
19
|
+
- **The rebuild's remaining recipes are documentation, not syntax.** The zero-JS `<details>` nav menu (and its focus/ARIA tradeoff), the woff2/subset/preload font recipe (raw TTFs measured at 7x), and query-param form pre-filling via `:state from-url` plus `wd.subscribe` (with `data-wd-bind-input` documented as the supported bound-input selector) all live in AGENTS.md, which `darkmown init` ships into every scaffolded project.
|
|
20
|
+
- **Images wire the variants authors already export, and one more silent collapse got a hint.** The `name@640.ext` convention turns pre-sized exports into a measured, sorted `srcset` with the original kept as `src` (no resize pipeline, a probe list instead of directory reads, `sizes` deliberately unemitted), and the sibling-HTML hint names the rebuild's 16-card paragraph collapse the moment consecutive classed raw-HTML lines would render inline in one `<p>`.
|
|
21
|
+
|
|
5
22
|
## 2.7.0 additions: silent failures closed, and the headroom spent (2026-09-04)
|
|
6
23
|
|
|
7
24
|
An audit put one question to every path in the compiler, the builder and the runtime: when this goes wrong, does the author find out? Where the answer was no, the behavior is now either correct or a coded refusal. Separately the runtime started shipping minified, which returned roughly 2 KB of the 8 KB budget, and that headroom bought five features the original vision named but the framework could not reach.
|
|
@@ -52,7 +69,7 @@ The 1.3.0 cycle shipped the crawler *files* (sitemap, rss, robots). This closes
|
|
|
52
69
|
- **Every author-facing compile error carries a stable `WDxxx` code.** The original vision treated compile errors as a first-class DX surface ("errors include file path + corrective suggestion"); this closes the last gap by making them *identifiable*. 138 codes, grouped by subsystem, mirrored on `err.wd.code` beside the existing `file`/`line`/`hint`/`example`. `src/errors.js` is the single source of truth, `docs/errors.md` is generated from it, and `directiveCatalog()` exposes it as data for tooling. Drift guards run both directions: an author-facing throw without a code, a duplicate code, or an undocumented code all fail the build. Internal invariants (the AST parser reading the compiler's own validated output, the reader contract) stay plain `Error`s by design — they signal a framework bug, not an authoring mistake — and that split is itself enforced by test.
|
|
53
70
|
- **Zero known advisories, and one fewer runtime dependency.** `image-size` carried a high-severity advisory affecting every published version with no upgrade path, so its single build-time call site is now served by an inlined header reader (PNG/JPEG/GIF/WebP/SVG) with explicitly bounded parse loops. This keeps the project's "few runtime dependencies" posture honest rather than deferring to an unfixable upstream.
|
|
54
71
|
- **BUGFIX — CRLF sources lost their frontmatter.** `parseFrontmatter` tested an LF-shaped `---\n` opener, so any file authored on Windows or checked out with `core.autocrlf` had its whole frontmatter block silently reclassified as body text: no error, but the page lost its title, layout, `draft` flag, and collection schema fields. Newline normalization now happens at the reader boundary and in `parseFrontmatter`. This had been latent since the first commit; it surfaced within minutes of adding a Windows CI job.
|
|
55
|
-
- **Platform coverage is now part of the contract.** CI runs the suite on Linux, Windows, and macOS, and the browser e2e suite against Chromium, Firefox, and WebKit (sharded, so three engines cost roughly one engine's wall-clock). The reactive runtime had only ever been proven in one engine; the CLI's path handling had never executed on Windows at all.
|
|
72
|
+
- **Platform coverage is now part of the contract.** CI runs the suite on Linux, Windows, and macOS, and the browser e2e suite against Chromium, Firefox, and WebKit (sharded, so three engines cost roughly one engine's wall-clock). The reactive runtime had only ever been proven in one engine; the CLI's path handling had never executed on Windows at all. *(Extended 2026-10-12: e2e is no longer only a correctness vote — it is also the runtime's measured coverage net. Every chromium-leg spec records V8 coverage for the served `/__wd/runtime.js` and `/__wd/behaviors/*.js` through the shared fixture `tests/e2e/coverage.js`, and `scripts/runtime-coverage-gate.mjs` merges it (union semantics), maps the minified runtime's bytes back to `src/runtime.js` lines through the committed sourcemap, and enforces per-file thresholds committed in `.runtime-coverage-snapshot.json` — with the same anti-self-deception rules as the Node gate (zero entries for a measured file fails; stale snapshot entries fail). Measured at introduction: `src/runtime.js` 89.07% of lines, `sortable.js` 97.48%, `carousel.js` 81.42% (its autoplay and reduced-motion branches are the main gap — the demo carousel sets no `autoplay=`). The three engine legs stay correctness-only; one engine's byte ranges are the same as any other's.)*
|
|
56
73
|
- **A scaffolded project now carries its own agent context.** The package has always shipped a full build-with-Darkmown guide, but only at the package root, so a consumer's copy landed in `node_modules/@zvndev/darkmown/AGENTS.md` where no coding agent reads it. `darkmown init` now copies that same file to the project root (byte-identical, so a scaffolded project and this repo can never teach different syntax), plus a short `CLAUDE.md` pointing at it and a `.gitignore`. This matters because the directive vocabulary is small and closed: an agent that has it in context writes valid `.wd` on the first try, and an agent that does not reaches for JSX and JavaScript idioms the compiler rejects. A pack test keeps `AGENTS.md` in the tarball, since dropping it would make `init` silently scaffold a project with no agent context and no error.
|
|
57
74
|
|
|
58
75
|
- **Types resolve for modern TypeScript consumers.** The `exports` map lacked `types` conditions, so `moduleResolution: node16`/`nodenext`/`bundler` consumers got no types from either entrypoint. Fixed, `types/catalog.d.ts` authored, and the declaration build itself repaired (it had been failing silently under TypeScript 6, leaving the committed `.d.ts` stale).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"test": "node --test tests/*.test.js",
|
|
38
38
|
"test:cov": "node scripts/test-coverage-gate.mjs 100",
|
|
39
39
|
"test:e2e": "playwright test",
|
|
40
|
+
"test:e2e:coverage": "playwright test --project=chromium && node scripts/runtime-coverage-gate.mjs",
|
|
41
|
+
"runtime-coverage": "node scripts/runtime-coverage-gate.mjs",
|
|
40
42
|
"test:extension": "npm --prefix editors/vscode ci && npm --prefix editors/vscode test",
|
|
41
43
|
"pack:extension": "cd editors/vscode && npx --yes @vscode/vsce package",
|
|
42
44
|
"typecheck": "tsc --noEmit",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"esbuild": "0.28.2",
|
|
87
89
|
"husky": "^9.1.7",
|
|
88
90
|
"lint-staged": "^17.0.0",
|
|
91
|
+
"sharp": "^0.35.5",
|
|
89
92
|
"typescript": "^6.0.3"
|
|
90
93
|
},
|
|
91
94
|
"repository": {
|
|
@@ -105,5 +108,8 @@
|
|
|
105
108
|
"zero-js",
|
|
106
109
|
"reactive"
|
|
107
110
|
],
|
|
108
|
-
"license": "MIT"
|
|
111
|
+
"license": "MIT",
|
|
112
|
+
"optionalDependencies": {
|
|
113
|
+
"sharp": "^0.35.5"
|
|
114
|
+
}
|
|
109
115
|
}
|
package/site/_/footer.wd
CHANGED
|
@@ -9,10 +9,11 @@ html: true
|
|
|
9
9
|
<div class="small">Zero JS on static pages · one ~6.6 KB runtime when reactive.</div>
|
|
10
10
|
</div>
|
|
11
11
|
<nav class="foot-links">
|
|
12
|
-
<a href="/docs/">Docs</a>
|
|
12
|
+
<a href="/docs/" data-lf="footer.docs">Docs</a>
|
|
13
13
|
<a href="/showcase/">Showcase</a>
|
|
14
|
-
<a href="https://github.com/ZVN-DEV/darkmown">GitHub</a>
|
|
15
|
-
<a href="https://www.npmjs.com/package/@zvndev/darkmown">npm</a>
|
|
14
|
+
<a href="https://github.com/ZVN-DEV/darkmown" data-lf="footer.github">GitHub</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@zvndev/darkmown" data-lf="footer.npm">npm</a>
|
|
16
|
+
<a href="mailto:support@darkmown.com" data-lf="footer.support">support@darkmown.com</a>
|
|
16
17
|
</nav>
|
|
17
18
|
</div>
|
|
18
19
|
</footer>
|
package/site/_/nav.wd
CHANGED
|
@@ -5,12 +5,12 @@ html: true
|
|
|
5
5
|
<nav class="topnav">
|
|
6
6
|
<a class="brand" href="/">Darkmown</a>
|
|
7
7
|
<span class="nav-links">
|
|
8
|
-
<a href="/playground/">Playground</a>
|
|
9
|
-
<a href="/docs/">Docs</a>
|
|
8
|
+
<a href="/playground/" data-lf="nav.playground">Playground</a>
|
|
9
|
+
<a href="/docs/" data-lf="nav.docs">Docs</a>
|
|
10
10
|
<a href="/blog/">Blog</a>
|
|
11
11
|
<a href="/showcase/">Showcase</a>
|
|
12
12
|
<a href="/reactive/">Reactive</a>
|
|
13
13
|
<a href="/data/">Data</a>
|
|
14
14
|
</span>
|
|
15
|
-
<a class="nav-gh" href="https://github.com/ZVN-DEV/darkmown">GitHub ↗</a>
|
|
15
|
+
<a class="nav-gh" href="https://github.com/ZVN-DEV/darkmown" data-lf="nav.github">GitHub ↗</a>
|
|
16
16
|
</nav>
|
package/site/pages/404.wd
CHANGED
|
@@ -15,8 +15,10 @@ description: That route does not exist. Head back to the Darkmown homepage.
|
|
|
15
15
|
|
|
16
16
|
The route you asked for is hidden, was never created, or has moved. Nothing was lost — Markdown that runs is still one click away.
|
|
17
17
|
|
|
18
|
+
<div class="actions">
|
|
18
19
|
<a class="btn primary" href="/">Back to home</a>
|
|
19
20
|
<a class="btn" href="/docs/">Read the docs</a>
|
|
21
|
+
</div>
|
|
20
22
|
</section>
|
|
21
23
|
|
|
22
24
|
</main>
|
package/site/pages/data.wd
CHANGED
|
@@ -113,7 +113,7 @@ The reply will appear here.
|
|
|
113
113
|
The request failed: { reply_error }
|
|
114
114
|
:endif
|
|
115
115
|
|
|
116
|
-
In `darkmown dev` the echo endpoint
|
|
116
|
+
In `darkmown dev` and `darkmown serve` the echo endpoint runs locally; on darkmown.com it is a real serverless function behind the same URL. On static-only hosts such as a plain Cloudflare Pages deploy, replace `action="/api/echo/"` with your own API endpoint. Either way, Darkmown itself stays static, and sessions ride on `:fetch` plus ordinary cookies against your real API.
|
|
117
117
|
|
|
118
118
|
## Persistent state survives reload
|
|
119
119
|
|