@zvndev/darkmown 2.12.1 → 2.12.3
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 +1 -1
- package/CHANGELOG.md +14 -1
- package/README.md +10 -1
- package/SECURITY.md +9 -2
- package/docs/cli.md +10 -0
- package/docs/spec-alignment.md +1 -1
- package/package.json +2 -2
- package/site/pages/docs/index.wd +1 -1
- package/site/pages/playground/index.js +52 -58
- package/src/cli.js +23 -1
- package/src/compiler/format.js +3 -2
- package/src/compiler/frontmatter.js +2 -1
- package/src/compiler/markdown.js +61 -7
- package/src/compiler/page.js +11 -14
- package/src/dev.js +63 -0
- package/src/feeds.js +24 -9
- package/src/headers.js +8 -2
- package/src/image-variant-resize.js +6 -1
- package/src/router.js +4 -1
- package/src/runtime.js +16 -6
- package/src/runtime.min.js +1 -1
- package/src/runtime.min.js.map +1 -1
- package/src/scaffold.js +13 -5
- package/src/statics.js +16 -1
- package/types/dev.d.ts +28 -0
- package/types/feeds.d.ts +19 -26
- package/types/headers.d.ts +6 -1
package/AGENTS.md
CHANGED
|
@@ -73,7 +73,7 @@ rss_limit: 20
|
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- `title`, `description`, `image` drive `<title>`, the meta description, and the Open Graph / Twitter card. `lang` sets `<html lang>` (default `en`).
|
|
76
|
-
- `html: true` allows raw HTML in that file's body. `transitions: true` opts into a view transition (
|
|
76
|
+
- `html: true` allows raw HTML in that file's body. `transitions: true` opts into a view transition (instant swap, no root animation) plus link prerendering.
|
|
77
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
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
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.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
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.12.3 (2026-10-22)
|
|
6
|
+
|
|
7
|
+
A security pass over the runtime, the compiler's output escaping, and the local servers. Nothing found executed script on a default (`html: false`) page; the fixes close denial-of-service, redirect, and leak paths, plus script injection on `html: true` pages fed by data.
|
|
8
|
+
|
|
9
|
+
- **Local dev/preview servers refuse other websites.** `darkmown dev` and `darkmown serve` now answer `403` to an unrecognized `Host` (DNS rebinding) and to cross-site calls into `/api/*`. Before, any page you visited could rebind its domain to `127.0.0.1` and read drafts, the dependency map, and `api/*` responses, whose handlers run with the full `process.env`. New `DARKMOWN_ALLOWED_HOSTS` env var for tunnels (`*` disables). **BEHAVIOR CHANGE** if you open the dev server through a custom domain or tunnel: add it to the variable.
|
|
10
|
+
- **Runtime: hostile JSON can no longer wedge a page.** `?n={"toString":1}` on a `from-url` key made every render throw, and a `persist` key then kept the page broken after reloading the clean URL. All untrusted JSON (fetch bodies, URL params, localStorage, storage events) now drops own `toString`/`valueOf` keys. A `:fetch` placeholder valued `.` or `..` is treated as missing, so `?id=..` can no longer retarget `/api/users/{ id }/profile` at `/api/profile`. Runtime 6,603 → 6,621 B gzipped.
|
|
11
|
+
- **Raw HTML interpolation is attribute-context aware** on `html: true` pages: values in `on*=`, `srcdoc=`, or bare tag position are emitted empty with a warning; unquoted values are entity-encoded; `data`, `poster`, `cite`, `background` and `ping` join the URL scheme guard.
|
|
12
|
+
- **Collection-author vectors closed.** The excerpt/RSS description regex was quadratic (a 200k-character body took 17 s or more per call); one control character in a post title made `rss.xml` invalid XML; a data-driven image src could probe for `@width` variants outside `site/`; a symlinked `.md` page compiled any file on the machine into `dist`. All fixed. A `__proto__` frontmatter key is ignored.
|
|
13
|
+
- **Preview parity.** `serve` no longer serves `_worker.js/` (your `api/` handler source), `_headers`, `_redirects`, `_routes.json`, or the dev dependency map, and sends `nosniff` and the other base headers on every file, not only HTML.
|
|
14
|
+
- **HSTS.** Every surface now sends `Strict-Transport-Security: max-age=31536000` (no `includeSubDomains`/`preload`).
|
|
15
|
+
- **Scaffold.** `init` writes with exclusive opens, so a dangling symlink in the target can no longer redirect a write, and `--template` must name a shipped template.
|
|
16
|
+
- **Dependencies and CI.** `markdown-it` 14.3.3 fixes two quadratic-time advisories (GHSA-vfgh-jcmw-g37q, GHSA-gfr2-rv44-r23c; the second affects default options). The CI workflow token is now `contents: read`.
|
|
17
|
+
|
|
5
18
|
## 2.12.1 (2026-10-16)
|
|
6
19
|
|
|
7
20
|
A one-item patch revising the navigation animation again, this time from frame-level measurement instead of reading the stylesheet.
|
|
8
21
|
|
|
9
|
-
- **BEHAVIOR CHANGE: the default view transition is now
|
|
22
|
+
- **BEHAVIOR CHANGE: the default view transition is now no animation at all.** Three animated defaults were field-tested on the real demo and each failed somewhere: the directional slide read as a bounce, the 2.12.0 simultaneous cross-fade double-exposed on camera (the incoming snapshot stacks over the outgoing one), and the sequenced out-then-in shipped hours earlier was ignored by a shipping browser (Arc) in favor of its own cross-fade. View-transition animation compositing varies too much across engines to ship as a default, so both roots now get `animation: none`: the swap is an instant cut with no possible artifact, and the speculation-rules prerender (unchanged) is what makes navigation feel smooth. Authors who want a fade can add `::view-transition-old`/`::view-transition-new` rules in their own skin. The `@view-transition` opt-in is unchanged, and a `transitions: true` page still ships zero JS.
|
|
10
23
|
|
|
11
24
|
## 2.12.0 (2026-10-16)
|
|
12
25
|
|
package/README.md
CHANGED
|
@@ -68,6 +68,15 @@ We rebuilt a live production marketing site (Next.js 16, React 19, Tailwind, on
|
|
|
68
68
|
|
|
69
69
|
The honest losses, because a benchmark that only wins is marketing: the rebuild shipped 1.07 MB of raw TTF fonts against `next/font`'s 158 KB of subset woff2, and 1,045 KB of unoptimized photos against 147 KB of CDN width variants. Fonts now have a documented woff2 + subsetting recipe (2.8), and responsive image variants are generated at build time (2.9).
|
|
70
70
|
|
|
71
|
+
## Example rebuilds
|
|
72
|
+
|
|
73
|
+
Private example repos under ZVN-DEV. Each one rebuilds a live Next.js marketing site in Darkmown, pulls content from the same production CMS at build time, and leaves the live site untouched. Each repo's commit history includes Darkmown friction notes for the framework.
|
|
74
|
+
|
|
75
|
+
- `darkmown-toothdocs`: ToothDocs rebuild, 67 routes. The benchmark subject above.
|
|
76
|
+
- `darkmown-7thst`: 7th Street Orthodontics rebuild, 36 routes.
|
|
77
|
+
- `darkmown-eetf`: East End Tooth Ferry rebuild, 31 routes.
|
|
78
|
+
- `darkmown-dafko`: DAFKO rebuild, 40 routes.
|
|
79
|
+
|
|
71
80
|
## Working from this repo
|
|
72
81
|
|
|
73
82
|
```sh
|
|
@@ -288,7 +297,7 @@ A few reserved keys drive **drafts and feeds** (see [SEO & feeds](#seo--feeds-si
|
|
|
288
297
|
|
|
289
298
|
**Markdown sources: `md_source: true`** on the home page opts the whole site in to publishing every page's byte-exact source beside its HTML: `/docs/` also answers `/docs.md` (the exact `.md`/`.wd` file the author wrote, frontmatter and CRLF endings included) with a `text/markdown` content type, and each published page carries `<link rel="alternate" type="text/markdown" href="/docs.md" title="Markdown source">`, emitted exactly when the file is, so the link never 404s. A page opts back out with `md_source: false`; a page-level `true` without the home flag does nothing, drafts never emit a source, and sitemap/rss/robots never list the `.md` files (they are alternates, not routes). Off by default, and a site that never sets it builds byte-identical output.
|
|
290
299
|
|
|
291
|
-
`transitions: true` opts a page into **instant, flash-free navigation**: zero JavaScript, all declarative. It emits a
|
|
300
|
+
`transitions: true` opts a page into **instant, flash-free navigation**: zero JavaScript, all declarative. It emits a **view transition** with no root animation, so the page swap is an instant cut with no possible compositing artifact (animated defaults were measured to double-expose or be replaced by the browser's own cross-fade; an instant swap is the only artifact-free choice), 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, and this prerender is what makes navigation feel smooth). Authors who want a fade can add `::view-transition-old`/`::view-transition-new` rules in their own skin. It honors `prefers-reduced-motion` trivially, since nothing animates. 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.)
|
|
292
301
|
|
|
293
302
|
## Static assets
|
|
294
303
|
|
package/SECURITY.md
CHANGED
|
@@ -18,6 +18,9 @@ Darkmown is a **trusted-author** static site generator. The threat model assumes
|
|
|
18
18
|
2. **Raw HTML is escaped by default; `html: true` re-opens the boundary per page.** `markdown-it` runs with `html: false`, so raw HTML in content renders as inert escaped text — a `<script>` or `onerror=` attribute in a contributed markdown file cannot execute. A page opts back into verbatim raw HTML with `html: true` frontmatter; on such a page there is no built-in sanitizer and untrusted content would execute in the visitor's browser, so reserve the opt-in for pages whose HTML you wrote yourself.
|
|
19
19
|
3. **`:fetch` and `:form action=` have no host allowlist.** A `:fetch`, `refresh=`, or `:form action=` URL is read directly from the page source; the compiler rejects non-http(s) schemes (`file:`, `data:`, `javascript:`, protocol-relative `//host`) but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. *(Since 2.1, reactive pages no longer need `'unsafe-eval'` — the runtime interprets a validated expression AST instead of building a `new Function`, so reactive and static pages share the same strict, eval-free CSP.)*
|
|
20
20
|
|
|
21
|
+
4. **Collections that take contributions need a `_schema.wd`.** `html:` is per-file frontmatter, so without a schema a contributor can put `html: true` on their own entry and opt it back into raw HTML. A collection with a `_schema.wd` rejects any undeclared frontmatter key at build time (`WD122`), so `html: true` fails the build unless the maintainer declares `html` in the schema.
|
|
22
|
+
5. **Bound link destinations are not restricted to your origin.** `[Next]({ next })` paints whatever `next` holds. `javascript:`, `data:` and `vbscript:` are always blanked, but an `https://other.example` or `//other.example` value is a working link. If the value comes from the URL (`from-url`) or a `:fetch`, that is an open-redirect shape; build the destination from a literal prefix (`/posts/{ slug }/`) instead.
|
|
23
|
+
|
|
21
24
|
The default is deliberately stricter than most Markdown SSGs: with content collections making multi-author markdown a first-class input, escaped-by-default is the only default that is safe when one of those authors isn't you. Unsafe content on an `html: true` page remains the author's responsibility to sanitize before it reaches the compiler.
|
|
22
25
|
|
|
23
26
|
## Security model
|
|
@@ -28,8 +31,11 @@ Things Darkmown deliberately guards at compile time and runtime:
|
|
|
28
31
|
- **Prototype-walk protection.** Path lookups (`{ a.b.c }`, computed expressions) reject `constructor`, `prototype`, and `__proto__` segments in both the compiler and the runtime.
|
|
29
32
|
- **Include sandboxing.** `@include` and `@loop` data sources must resolve inside `site/pages` or `site/_`; traversal outside the site tree is a compile error, and include cycles are detected.
|
|
30
33
|
- **Private page assets stay private.** Page-colocated assets under `site/pages` are copied only when their relative path has no hidden segment (`.`, `-`, or `_`) and is not a symlink. This keeps `.env`, `_private/*`, `-draft/*`, and symlink-to-outside files out of `dist`.
|
|
31
|
-
- **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout.
|
|
32
|
-
- **
|
|
34
|
+
- **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout. In raw HTML on an `html: true` page, escaping is context-aware: a value that lands in an `on*=` handler, a `srcdoc=`, or bare tag position (`<div { x }>`) is emitted empty with a warning, an unquoted attribute value is entity-encoded so it stays one value, and every URL-bearing attribute (`href`, `src`, `action`, `formaction`, `xlink:href`, `data`, `poster`, `cite`, `background`, `ping`) refuses executable schemes.
|
|
35
|
+
- **Untrusted runtime JSON.** A `:fetch` body, a `from-url` parameter, a `persist` value from localStorage and a cross-tab storage event all parse through one helper that drops own `toString`/`valueOf` keys, so a crafted value cannot make every render throw. A `:fetch` URL placeholder whose value is `.` or `..` is treated as missing rather than resolved as a path segment.
|
|
36
|
+
- **Symlinked pages are not routes.** A `.md`/`.wd` symlink under `site/pages` is skipped, like every other symlinked asset, so a contributed symlink cannot compile a file from elsewhere on the build machine into `dist`.
|
|
37
|
+
- **Static server path containment.** The dev/preview servers resolve requests strictly inside `dist`, and never serve host-only build files (`_worker.js/`, `_headers`, `_redirects`, `_routes.json`, the dev dependency map).
|
|
38
|
+
- **Local servers refuse other websites.** `darkmown dev` and `darkmown serve` bind to `127.0.0.1` and also answer `403` to a `Host` that is not `localhost`, `*.localhost`, an IP literal, or the bind address. That blocks DNS rebinding, which would otherwise let any page you visit read drafts and `api/*` responses (handlers run with your full environment). Requests to `/api/*` additionally refuse `Sec-Fetch-Site: cross-site` and non-local `Origin`s, so a website cannot trigger a handler's side effects with a blind POST. Set `DARKMOWN_ALLOWED_HOSTS=abc.ngrok.app` (comma-separated) to preview through a tunnel; `*` turns the guard off.
|
|
33
39
|
- **Build caches are trusted input.** The `node_modules/.cache/darkmown-*` directories (image variants, font subsets) are copied into `dist` verbatim when their content-hash keys match, so a poisoned shared CI cache ships attacker bytes. Pin and verify cache artifacts in CI, or delete the directories to rebuild from source.
|
|
34
40
|
|
|
35
41
|
## The #1 footgun: the raw HTML opt-in (`html: true`)
|
|
@@ -63,6 +69,7 @@ Builds emit security response headers so a deployed site is hardened by default
|
|
|
63
69
|
|
|
64
70
|
- **`Content-Security-Policy`** — **no `'unsafe-inline'` and no `'unsafe-eval'` on `script-src`, for any page.** The inline state seed is a `<script type="application/json">` data block, which is non-executable and therefore not gated by `script-src` at all; the one inline script CSP does gate — the fixed `<script type="speculationrules">` block a `transitions: true` page emits — is authorized by a build-time `'sha256-…'` hash source (plus the `'inline-speculation-rules'` keyword for browsers that check that instead). Since 2.1, **static and reactive pages share the same strict, eval-free policy**: the reactive runtime interprets a validated `:computed` / `@loop … where` / `.class when` expression AST instead of building a `new Function`, so it needs no `'unsafe-eval'`. The only `script-src` a reactive page adds over a static one is `'self'` for the same-origin `/__wd/runtime.js` — already granted.
|
|
65
71
|
- **`X-Content-Type-Options: nosniff`** — stops MIME-type sniffing.
|
|
72
|
+
- **`Strict-Transport-Security: max-age=31536000`**: one year of HTTPS-only for the host. It deliberately omits `includeSubDomains` and `preload`, which pin every sibling subdomain; add them in your deploy config if you own the whole domain.
|
|
66
73
|
- **`Referrer-Policy`** — limits referrer leakage.
|
|
67
74
|
- **`frame-ancestors`** — clickjacking protection (controls who may frame the page).
|
|
68
75
|
|
package/docs/cli.md
CHANGED
|
@@ -73,6 +73,16 @@ Rebuilds are **incremental** for `site/` content changes. Every dev build writes
|
|
|
73
73
|
|
|
74
74
|
Correctness beats speed: **any uncertainty runs a full rebuild** — a new, deleted, or renamed file; a file no dependency graph accounts for; a change to the site-wide feed link; a missing or stale map. A change under `src/` (the framework's own code) always runs a full rebuild in a child process so fresh modules load. Production `darkmown build` writes no map and is unaffected.
|
|
75
75
|
|
|
76
|
+
Both `dev` and `serve` read three environment variables:
|
|
77
|
+
|
|
78
|
+
| Variable | Default | Meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `PORT` | `5173` (dev), `4173` (serve) | Port to listen on. |
|
|
81
|
+
| `HOST` | `127.0.0.1` | Address to bind. `0.0.0.0` exposes the server to your network. |
|
|
82
|
+
| `DARKMOWN_ALLOWED_HOSTS` | empty | Extra `Host` names to accept, comma-separated, such as a tunnel domain. `*` disables the check. |
|
|
83
|
+
|
|
84
|
+
Requests whose `Host` is not `localhost`, `*.localhost`, an IP literal, the bind address, or listed in `DARKMOWN_ALLOWED_HOSTS` get a `403`. This stops DNS rebinding, where a website you visit points its own domain at `127.0.0.1` to read your dev server. Requests to `/api/*` also get a `403` when they are cross-site (`Sec-Fetch-Site: cross-site`, or an `Origin` that is not local or allow-listed).
|
|
85
|
+
|
|
76
86
|
### `darkmown build [--target cloudflare] [--drafts]`
|
|
77
87
|
|
|
78
88
|
Compiles `site/pages` into `dist`. The output is always 100% static HTML.
|
package/docs/spec-alignment.md
CHANGED
|
@@ -4,7 +4,7 @@ This audit compares the current Darkmown implementation with the original MARROW
|
|
|
4
4
|
|
|
5
5
|
## 2.12.1 additions: the transition, measured frame by frame (2026-10-16)
|
|
6
6
|
|
|
7
|
-
- **The navigation animation is
|
|
7
|
+
- **The navigation animation is gone, because animated defaults kept failing in the field.** The directional slide read as a bounce; the 2.12.0 simultaneous cross-fade double-exposed on camera (incoming snapshot stacks over outgoing); the sequenced out-then-in was ignored by a shipping browser (Arc), which substituted its own cross-fade. VT animation compositing varies too much across engines for a default, so both roots get `animation: none` (an instant, artifact-free cut) and the prerender hint carries the perceived speed. Authors can still animate in their own skins.
|
|
8
8
|
|
|
9
9
|
## 2.12.0 additions: perceived performance from demo feedback (2026-10-16)
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@types/markdown-it": "^14.1.2",
|
|
82
82
|
"highlight.js": "^11.11.1",
|
|
83
|
-
"markdown-it": "^14.3.
|
|
83
|
+
"markdown-it": "^14.3.3"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@biomejs/biome": "^2.5.7",
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -475,7 +475,7 @@ Mark a page `draft: true` and it stays out of production: `darkmown build` leave
|
|
|
475
475
|
|
|
476
476
|
## Instant navigation
|
|
477
477
|
|
|
478
|
-
Add `transitions: true` to a page's frontmatter for **instant, flash-free navigation**: zero JavaScript. It emits a
|
|
478
|
+
Add `transitions: true` to a page's frontmatter for **instant, flash-free navigation**: zero JavaScript. It emits a view transition with no root animation, so the page swap is an instant cut with no possible compositing artifact (animated defaults were measured to double-expose or be replaced by the browser's own cross-fade), and a `<script type="speculationrules">` prerender hint, so the browser renders the next same-origin page on hover/pointerdown and the click activates an already-painted page (no white render-gap; the prerender is what makes navigation feel smooth). Authors who want a fade can add `::view-transition-old`/`::view-transition-new` rules in their own skin. and only same-origin pages that both opt in transition. Off by default; opt out with `transitions: false`, or exclude a single link from prerendering with `{.no-prefetch}`. (Chrome turns prerendering off while DevTools is open, so test the built site with DevTools closed.)
|
|
479
479
|
|
|
480
480
|
## Images
|
|
481
481
|
|