@zvndev/darkmown 1.2.0 → 1.3.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/CHANGELOG.md +17 -0
- package/README.md +85 -1
- package/docs/spec-alignment.md +8 -0
- package/package.json +2 -1
- package/site/_/base.skin +14 -0
- package/site/_/nav.wd +1 -0
- package/site/pages/blog/drafts-that-never-leak.md +16 -0
- package/site/pages/blog/index.md +13 -0
- package/site/pages/blog/shipping-feeds.md +17 -0
- package/site/pages/blog/unfinished-thoughts.md +13 -0
- package/site/pages/docs/index.wd +47 -0
- package/site/pages/highlight/index.skin +78 -0
- package/site/pages/highlight/index.wd +142 -0
- package/site/pages/index.wd +1 -0
- package/src/builder.js +162 -6
- package/src/cli.js +51 -7
- package/src/compiler/context.js +5 -1
- package/src/compiler/index.js +1 -0
- package/src/compiler/markdown.js +7 -2
- package/src/compiler/page.js +25 -5
- package/src/dev.js +34 -5
- package/src/feeds.js +276 -0
- package/src/highlight.js +126 -0
- package/src/router.js +35 -3
- package/src/templates/blog/site/pages/index.wd +1 -0
- package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
- package/src/templates/blog/site/pages/posts/markdown-native.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
## 1.3.0 — 2026-06-28
|
|
6
|
+
|
|
7
|
+
The "table-stakes batteries" release — the build-time, zero-JS features a markdown site is expected to ship. The runtime is unchanged (still 7459 B gzipped, well under the 8 KB budget); everything here is compile-time HTML/CSS or build output.
|
|
8
|
+
|
|
9
|
+
- **Syntax highlighting.** Fenced code blocks with a known language are highlighted at compile time (via `highlight.js` through markdown-it) — HTML plus a small stylesheet, **zero client JS**. Colors are driven by a new `$code-*` skin-token set (`code-bg`, `code-fg`, `code-keyword`, `code-string`, `code-comment`, `code-function`, `code-number`, `code-punctuation`), so code **dark-modes for free** through the existing `tokens dark` / `:theme` system — override any token in your own `.skin`. The stylesheet (`/__wd/highlight.css`) ships **only on pages that contain a highlighted block** (pay-for-what-you-use). Unknown/absent languages degrade to plain escaped `<code>`; inline code and line numbers are intentionally excluded; a prose+code page stays `runtime: false`.
|
|
10
|
+
- **RSS + sitemap.xml + robots.txt.** `darkmown build` now emits `sitemap.xml`, `rss.xml`, and `robots.txt` at build time. Site identity lives in **one reserved home-page frontmatter field, `site_url`** (absolute origin) — which reuses the home `title`/`description` as the RSS channel and acts as the trigger; no config file is introduced. The sitemap lists every published route with a `<lastmod>` (frontmatter `date` → git last-commit date → file mtime). Any page with a `date:` becomes an RSS item (newest-first, capped; description from `excerpt:` → `description:` → first paragraph of a `.md`); an `<link rel="alternate" type="application/rss+xml">` is injected into every page `<head>`. With no `site_url`, `robots.txt` still emits (without the `Sitemap:` line) and the feeds are skipped with a build hint — never a crash.
|
|
11
|
+
- **Drafts.** `draft: true` frontmatter excludes a page from a production build — gone from `dist`, `routes.json`, the sitemap, and the feed (even if it has a `date:`). `darkmown dev` still builds and serves drafts with a dev-only banner; `darkmown build --drafts` includes them (for staging). Drafts are filtered at route discovery, so a feed can never leak one. This is distinct from the permanent `.`/`-`/`_` filename-prefix hiding: use a prefix to hide a file forever, `draft: true` to hide a page you intend to publish later.
|
|
12
|
+
- **New public frontmatter:** `site_url` (home page), `draft`, `date`, `excerpt`. New build flag: `--drafts`. The `blog` starter template now ships working feeds out of the box.
|
|
13
|
+
|
|
14
|
+
## 1.2.1 — 2026-06-27
|
|
15
|
+
|
|
16
|
+
Repository hygiene and documentation polish from a gold-standard audit (50/50 — every dimension at React-tier). No runtime, API, or behavior change; the shipped package is byte-identical except for one source comment.
|
|
17
|
+
|
|
18
|
+
- **Architecture docs name the value layer.** `src/compiler/format.js` (format pipes + list aggregates) is now listed in the compiler module map in both `CLAUDE.md` and `src/compiler/index.js` — previously the only module in the compile path absent from the architecture doc.
|
|
19
|
+
- **Honest CI labels.** The coverage-gate job was labeled "src-only line ≥ 90%" but `npm run test:cov` has always enforced **100%** (`scripts/test-coverage-gate.mjs 100`). The job name and its comment now state the real bar.
|
|
20
|
+
- **Tighter ignore rules.** `.env` / `.env.*` are now gitignored (the build's hidden-segment asset guard already kept them out of `dist`; this stops one from ever reaching git), and a stale internal design spec that predated the `docs/superpowers/` ignore rule is no longer tracked.
|
|
21
|
+
|
|
5
22
|
## 1.2.0 — 2026-06-27
|
|
6
23
|
|
|
7
24
|
Backends, deploy, and rich interactions — all additive. The core runtime is unchanged (still well under the 8 KB budget); the new behavior modules are budgeted separately so static pages stay zero-JS.
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
|
|
|
52
52
|
|
|
53
53
|
- `darkmown init [dir] [--template <name>]` scaffolds a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`).
|
|
54
54
|
- `darkmown dev` starts the live compiler with browser reload, an in-browser error overlay, and a local runner for `api/*` functions.
|
|
55
|
-
- `darkmown build [--target cloudflare]` writes static output to `dist
|
|
55
|
+
- `darkmown build [--target cloudflare] [--drafts]` writes static output to `dist` (plus `sitemap.xml`/`rss.xml`/`robots.txt`); `--drafts` includes `draft: true` pages for staging.
|
|
56
56
|
- `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
|
|
57
57
|
- `darkmown serve` previews the built `dist` locally.
|
|
58
58
|
- `darkmown version` prints the installed package version.
|
|
@@ -113,6 +113,8 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
113
113
|
|
|
114
114
|
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).
|
|
115
115
|
|
|
116
|
+
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`; `date:` marks a page as a blog post (it lands in `rss.xml` and sets the page's `<lastmod>`); `excerpt:` is the RSS summary.
|
|
117
|
+
|
|
116
118
|
`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.)
|
|
117
119
|
|
|
118
120
|
## Static assets
|
|
@@ -135,6 +137,55 @@ Hidden/private page paths stay private: any `site/pages` asset below a path segm
|
|
|
135
137
|
|
|
136
138
|
Use the shelf for assets shared across many pages; colocate the ones that belong to a single page or section. Both ship untouched with the correct content-type.
|
|
137
139
|
|
|
140
|
+
## SEO & feeds (sitemap, RSS, robots)
|
|
141
|
+
|
|
142
|
+
`darkmown build` emits the crawler files a site needs — all build-time, zero client JS — driven by a little frontmatter. There is no config file: the **home page** (`site/pages/index.md` or `.wd`) carries the site's identity.
|
|
143
|
+
|
|
144
|
+
```wd
|
|
145
|
+
---
|
|
146
|
+
title: My Blog
|
|
147
|
+
description: Notes on shipping Markdown that runs.
|
|
148
|
+
site_url: https://example.com
|
|
149
|
+
---
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`site_url`** (absolute origin, **no trailing slash**) turns on `sitemap.xml` and `rss.xml` and is the absolute prefix for every URL in them. The home `title`/`description` become the RSS channel title/description.
|
|
153
|
+
- **`robots.txt`** is *always* emitted (`User-agent: * / Allow: /`); the `Sitemap:` line is added only when `site_url` is set.
|
|
154
|
+
- **`sitemap.xml`** lists every built page (reactive pages included — they're indexable HTML). Each `<lastmod>` is the page's frontmatter `date:` if set, else its git last-commit date, else the file's mtime. No `<priority>`/`<changefreq>`.
|
|
155
|
+
- **`rss.xml`** syndicates your **posts** — any page with a `date:` in its frontmatter. Newest first, capped at the 20 most recent. Each item's `<description>` is the page's `excerpt:`, else its `description:`, else (for a plain `.md` post) its first paragraph. Every page links the feed with `<link rel="alternate" type="application/rss+xml">` so readers can autodiscover it.
|
|
156
|
+
|
|
157
|
+
```md
|
|
158
|
+
---
|
|
159
|
+
title: Hello, Darkmown
|
|
160
|
+
date: 2026-01-15
|
|
161
|
+
excerpt: Why I rewrote my blog as plain Markdown files.
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
# Hello, Darkmown
|
|
165
|
+
…
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Without `site_url`, `robots.txt` still emits and the build prints a one-line hint telling you which field to set; it never crashes.
|
|
169
|
+
|
|
170
|
+
### Drafts
|
|
171
|
+
|
|
172
|
+
Mark any page `draft: true` to keep it out of production:
|
|
173
|
+
|
|
174
|
+
```md
|
|
175
|
+
---
|
|
176
|
+
title: Work in progress
|
|
177
|
+
draft: true
|
|
178
|
+
---
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **`darkmown build`** excludes drafts everywhere — no HTML in `dist`, no entry in `routes.json`, `sitemap.xml`, or `rss.xml` (even a draft that *also* has a `date:` never reaches a feed).
|
|
182
|
+
- **`darkmown dev`** builds and serves drafts so you can preview them, with a visible "DRAFT" banner that exists only in the dev server — never in any shipped HTML.
|
|
183
|
+
- **`darkmown build --drafts`** includes drafts everywhere — for a staging deploy.
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
The `build` summary reports what it emitted: `Built 14 routes, sitemap (14 urls), rss (6 posts) into dist`.
|
|
188
|
+
|
|
138
189
|
## Loops
|
|
139
190
|
|
|
140
191
|
`@loop <things> into <thing>` is the only loop. The source decides the behavior:
|
|
@@ -692,6 +743,39 @@ The block must follow the element with no space, and works on links, images, emp
|
|
|
692
743
|
|
|
693
744
|
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.
|
|
694
745
|
|
|
746
|
+
## Syntax highlighting
|
|
747
|
+
|
|
748
|
+
*New in 1.3.* Fenced code blocks with a language are highlighted at **build time** — HTML and CSS only, no client JavaScript. Tag the fence with a language and it just works:
|
|
749
|
+
|
|
750
|
+
````md
|
|
751
|
+
```js
|
|
752
|
+
const greeting = "Darkmown"; // highlighted at build time
|
|
753
|
+
```
|
|
754
|
+
````
|
|
755
|
+
|
|
756
|
+
The highlighter is [highlight.js](https://highlightjs.org/) and is not configurable (one closed default, like the rest of the framework). Its token classes map onto your skin's `$code-*` tokens, so highlighted code **dark-modes for free** through the same [`tokens dark` / `:theme`](#dark-mode--tokens-dark) system below — no extra wiring. Tune the palette (or rely on the built-in default set) in your `.skin`:
|
|
757
|
+
|
|
758
|
+
```skin
|
|
759
|
+
tokens
|
|
760
|
+
code-bg #1b2420
|
|
761
|
+
code-fg #e9efe7
|
|
762
|
+
code-keyword #d9a8e0
|
|
763
|
+
code-string #97d892
|
|
764
|
+
code-comment #859289
|
|
765
|
+
code-function #88c4ee
|
|
766
|
+
code-number #ecae78
|
|
767
|
+
code-punctuation #c1ccc6
|
|
768
|
+
tokens dark
|
|
769
|
+
code-bg #100d0a
|
|
770
|
+
code-keyword #e2b9e8
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
- **Pay-for-what-you-use.** The stylesheet (`/__wd/highlight.css`) is emitted and linked **only** on pages that actually contain a highlighted block — a page with no code ships nothing extra.
|
|
774
|
+
- **Zero runtime.** Highlighting is build-time output, so a page of prose plus code stays `runtime: false` (no `/__wd/runtime.js`). It never pulls in the reactive runtime.
|
|
775
|
+
- **Graceful degradation.** A fence with an unknown or absent language renders as plain escaped `<code>` (no highlighting, no error). Inline `` `code` `` is never highlighted, and there are deliberately **no line numbers** (they break copy-paste).
|
|
776
|
+
|
|
777
|
+
See it recolor live on the [Syntax highlighting demo](https://darkmown.com/highlight/) — flip the theme toggle and every block recolors at once.
|
|
778
|
+
|
|
695
779
|
## Dark mode — `tokens dark`
|
|
696
780
|
|
|
697
781
|
*New in 0.19.0.* A colocated `.skin` file already declares its palette in a `tokens` block (`name value` pairs referenced elsewhere as `$name`). Add a second `tokens dark` block to override any of those tokens under the visitor's OS dark preference — it compiles to a `@media (prefers-color-scheme: dark) :root { … }` rule, so the page follows the system theme with **zero JavaScript**.
|
package/docs/spec-alignment.md
CHANGED
|
@@ -134,6 +134,14 @@ Four progressive-disclosure capabilities plus an assets and a security change. T
|
|
|
134
134
|
- **Page-colocated static assets:** any non-page file under `site/pages/` (anything but `.md`/`.wd` routes and colocated `.skin`/`.js`) is copied to `dist/` at its path (`site/pages/logo.svg` → `dist/logo.svg`). The shelf (`site/_/` → `/__wd/media`, `/__wd/data`) is unchanged; a name clash with an emitted route is skipped with a warning. As of v0.14.1, hidden path segments (`.`, `-`, `_`) and symlinked page assets are skipped. `.avif` was added to the served content-types.
|
|
135
135
|
- **`:fetch` URL scheme hardening (security):** `:fetch`/`refresh=` URLs are validated at compile time — relative, `http(s)://`, or a leading `{ state }` interpolation are allowed; protocol-relative `//host` and non-http(s) schemes (`file:`, `data:`, `javascript:`, …) are rejected with a corrective hint. **This is scheme validation, not a host allowlist** — SSRF to arbitrary http(s) hosts remains the author's responsibility (see the trust boundary).
|
|
136
136
|
|
|
137
|
+
## 1.3.0 additions — table-stakes batteries (2026-06-28)
|
|
138
|
+
|
|
139
|
+
The 1.3.0 cycle closes table-stakes gaps a new user expects out of the box. Tracks land in parallel; this entry covers the **syntax-highlighting** and **drafts + feeds** tracks.
|
|
140
|
+
|
|
141
|
+
- **Drafts + sitemap.xml + rss.xml + robots.txt:** three coupled SEO/feed capabilities sharing one draft-exclusion gate. Site identity lives in the **home page frontmatter** (no config loader this cycle): one reserved field `site_url` (absolute origin, no trailing slash) reuses the home `title`/`description` as the RSS channel fields and is the absolute prefix for — and the trigger for emitting — `sitemap.xml` + `rss.xml`. **Drafts:** `draft: true` excludes a page from a production build everywhere (not in `dist`, `routes.json`, sitemap, or rss — even a draft that *also* has a `date:`); `darkmown dev` builds/serves drafts with a dev-only visible banner injected at serve time (never in shipped HTML, like the SSE client); `darkmown build --drafts` includes them for staging. Drafts are filtered at the single route-discovery **chokepoint** (`discoverRoutes`), which now reads each file's frontmatter **once** and threads it out as `route.meta` (one source of truth for "is this a draft", and the data the feeds consume — no second parse). This is distinct from the permanent `.`/`-`/`_` filename hiding (private forever vs a publish-when-ready toggle). **sitemap.xml** (emitted whenever `site_url` is set) lists one `<url>` per built non-404 route — reactive pages included (indexable HTML) — with `<lastmod>` = frontmatter `date:` → git last-commit date of the source file → file mtime (git wrapped in try/catch so a scaffolded app or extracted tarball with no `.git` degrades to mtime, never crashes); no `<priority>`/`<changefreq>`. **rss.xml** (emitted only with `site_url` *and* at least one dated post) syndicates the pages with a `date:` (the "this is a post" signal), newest-first, capped at 20; `<description>` resolves `excerpt:` → `description:` → first paragraph of rendered prose (plain `.md` only — `.wd` directive markup is not stripped back to prose); `<pubDate>` is RFC-822; every page advertises the feed with `<link rel="alternate" type="application/rss+xml">`. **robots.txt** is *always* emitted (`User-agent: * / Allow: /`); the `Sitemap:` line appears only with `site_url`. **No `site_url`:** robots still emits (without the Sitemap line), sitemap + rss are skipped with a loud, actionable build hint, and the build never crashes. String/XML building lives in a new `src/feeds.js` (mirroring how `src/highlight.js` keeps `builder.js` lean); the `build` summary reports counts (`Built 14 routes, sitemap (14 urls), rss (6 posts) into dist`). **Zero runtime impact** — all build-time; `src/runtime.js` and the 8 KB budget are untouched. **The leak contract** (a `draft: true` page with a `date:` produces zero sitemap entries and zero rss items; `/404/` never appears in either) is regression-tested on both the production-exclusion and `--drafts`-inclusion paths, plus the git-date vs mtime-fallback paths.
|
|
142
|
+
|
|
143
|
+
- **Build-time syntax highlighting:** fenced code blocks with a known language are highlighted at compile time via [highlight.js](https://highlightjs.org/), wired onto both markdown-it instances (the default `html: true` and the lazy `html: false` strict variant) through markdown-it's `highlight` option in `src/compiler/markdown.js`. Output is **class-based** (`hljs-*` token spans, escaped) — no inline colors — and the framework ships a small stylesheet (`/__wd/highlight.css`) that maps the common token classes onto a `$code-*` skin-token set (`code-bg`, `code-fg`, `code-keyword`, `code-string`, `code-comment`, `code-function`, `code-number`, `code-punctuation`), each `var(--code-*, fallback)`. Because those tokens carry a `tokens dark` override in the base skin, **highlighted code dark-modes for free** through the existing `tokens dark` / `:theme` system with zero JavaScript. The highlighter is **not configurable** (closed-whitelist philosophy). Emission/injection is **pay-for-what-you-use**: a per-page `assets.hasCode` flag (set when the rendered HTML carries the `hljs` marker) emits and links the stylesheet **only** on pages that contain a highlighted block, mirroring the behavior modules — a page with no code ships nothing extra. **Zero runtime impact:** highlighting is build-time HTML + CSS only (`src/runtime.js` untouched, the 8 KB runtime budget unchanged), so a page of prose plus a code block stays `runtime: false`. **Graceful degradation:** a fence with an unknown or absent language renders as plain escaped `<code>` (no throw, no highlighting); inline `` `code` `` is never highlighted; there are deliberately **no line numbers** (they break copy-paste). **Honest caveat:** the stylesheet maps a curated common subset of highlight.js classes — any token class outside that subset inherits the base `--code-fg` color, so it stays legible but is not individually themed.
|
|
144
|
+
|
|
137
145
|
## 1.2.0 additions — backends, deploy & interactions (2026-06-27)
|
|
138
146
|
|
|
139
147
|
- **Serverless `api/` model:** backend endpoints are plain-JS Web-standard handlers — `export default (request, context) => Response` — in a top-level `api/` directory (`api/echo.js` → `/api/echo`; `api/users/[id].js` → `/api/users/:id` with `context.params.id`). Darkmown owns no server; it *adapts* to the host. The build stays 100% static. On Vercel the `api/` functions run natively (Edge); `darkmown deploy cloudflare` (or `build --target cloudflare`) emits `dist/_worker.js` that routes `/api/*` to the same functions and serves everything else from `env.ASSETS`. `darkmown dev` runs a local runner so `/api/*` behaves identically before deploy. No `.wd` backend DSL — minimal new learning, zero lock-in. `form-action 'self'` was added to both CSP variants (and `vercel.json`) so a native `:form action="/api/…"` POST is allowed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"node": ">=20"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"highlight.js": "^11.11.1",
|
|
60
61
|
"image-size": "^1.2.1",
|
|
61
62
|
"markdown-it": "^14.2.0"
|
|
62
63
|
},
|
package/site/_/base.skin
CHANGED
|
@@ -26,6 +26,13 @@ tokens
|
|
|
26
26
|
code-bg #1b2420
|
|
27
27
|
code-ink #e9efe7
|
|
28
28
|
code-line #2c3b34
|
|
29
|
+
code-fg #e9efe7
|
|
30
|
+
code-keyword #d9a8e0
|
|
31
|
+
code-string #97d892
|
|
32
|
+
code-comment #859289
|
|
33
|
+
code-function #88c4ee
|
|
34
|
+
code-number #ecae78
|
|
35
|
+
code-punctuation #c1ccc6
|
|
29
36
|
ring #0f6b5e
|
|
30
37
|
radius 12px
|
|
31
38
|
radius-sm 8px
|
|
@@ -51,6 +58,13 @@ tokens dark
|
|
|
51
58
|
code-bg #100d0a
|
|
52
59
|
code-ink #e4ebe2
|
|
53
60
|
code-line #2a231b
|
|
61
|
+
code-fg #e4ebe2
|
|
62
|
+
code-keyword #e2b9e8
|
|
63
|
+
code-string #a6e0a1
|
|
64
|
+
code-comment #7c8983
|
|
65
|
+
code-function #9bd0f4
|
|
66
|
+
code-number #f2bb8a
|
|
67
|
+
code-punctuation #b6c2bc
|
|
54
68
|
ring #5ccab4
|
|
55
69
|
|
|
56
70
|
/* ── Reset & base ───────────────────────────────────────────────── */
|
package/site/_/nav.wd
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Drafts that never leak
|
|
3
|
+
description: draft true excludes a page everywhere in a production build — dist, routes.json, sitemap, and rss.
|
|
4
|
+
date: 2026-06-27
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Drafts that never leak
|
|
8
|
+
|
|
9
|
+
Set `draft: true` on any page and it disappears from a production build — no
|
|
10
|
+
HTML in `dist`, no entry in `routes.json`, and crucially no entry in
|
|
11
|
+
`sitemap.xml` or `rss.xml`, even if the draft also carries a `date:`.
|
|
12
|
+
|
|
13
|
+
The filter lives at one chokepoint — route discovery — so every downstream
|
|
14
|
+
consumer only ever sees published pages. `darkmown dev` still builds and serves
|
|
15
|
+
your drafts (with a visible banner), and `darkmown build --drafts` includes them
|
|
16
|
+
for a staging preview.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Darkmown Blog
|
|
3
|
+
description: Notes on shipping Markdown that runs — feeds, drafts, and zero-JS releases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The Darkmown Blog
|
|
7
|
+
|
|
8
|
+
Short notes on the framework. Every post here is a plain `.md` file with a
|
|
9
|
+
`date:` in its frontmatter — that `date:` is what lands a page in `rss.xml` and
|
|
10
|
+
stamps its `<lastmod>` in `sitemap.xml`.
|
|
11
|
+
|
|
12
|
+
- [Shipping feeds without a config file](/blog/shipping-feeds/)
|
|
13
|
+
- [Drafts that never leak](/blog/drafts-that-never-leak/)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shipping feeds without a config file
|
|
3
|
+
description: Site identity lives in the home page frontmatter — one site_url field turns on sitemap.xml and rss.xml.
|
|
4
|
+
date: 2026-06-26
|
|
5
|
+
excerpt: One reserved frontmatter field on the home page — site_url — turns on the whole feeds story. No config loader, no plugin.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Shipping feeds without a config file
|
|
9
|
+
|
|
10
|
+
Darkmown has no config file, so the site's public origin lives where the rest of
|
|
11
|
+
its identity already does: the home page frontmatter. Add `site_url` to
|
|
12
|
+
`site/pages/index` and the build emits `sitemap.xml` and `rss.xml`, reusing the
|
|
13
|
+
home `title` and `description` as the RSS channel fields.
|
|
14
|
+
|
|
15
|
+
Every page picks up a `<link rel="alternate" type="application/rss+xml">` so
|
|
16
|
+
feed readers can autodiscover the feed, and `robots.txt` points crawlers at the
|
|
17
|
+
sitemap. All build-time — not a byte of client JavaScript.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Unfinished thoughts (do not ship)
|
|
3
|
+
description: A work-in-progress post that demonstrates draft exclusion — it carries a date but never reaches a production feed.
|
|
4
|
+
date: 2026-06-28
|
|
5
|
+
draft: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Unfinished thoughts
|
|
9
|
+
|
|
10
|
+
This post is marked `draft: true`. It also has a `date:`, which would normally
|
|
11
|
+
land it in `rss.xml` and `sitemap.xml` — but because it is a draft, a production
|
|
12
|
+
build excludes it from both, and from `dist` and `routes.json` entirely. You are
|
|
13
|
+
only reading this in `darkmown dev` or a `build --drafts` staging build.
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -176,6 +176,24 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
176
176
|
|
|
177
177
|
`{ meta.title }` prints a scalar and `{ meta.tags }` joins an array with `, `. You can loop a frontmatter array at build time — `@loop meta.tags into tag` — and the page stays static. Arrays are inline flow only (`[a, b]`); quoted items keep their commas.
|
|
178
178
|
|
|
179
|
+
## SEO & feeds
|
|
180
|
+
|
|
181
|
+
`darkmown build` writes `sitemap.xml`, `rss.xml`, and `robots.txt` — all build-time, zero client JS. There's no config file: the **home page** frontmatter carries the site's identity. Add `site_url` (absolute origin, no trailing slash) and the sitemap and feed switch on, prefixed with that origin and titled with the home `title`/`description`.
|
|
182
|
+
|
|
183
|
+
```wd
|
|
184
|
+
---
|
|
185
|
+
title: My Blog
|
|
186
|
+
description: Notes
|
|
187
|
+
site_url: https://example.com
|
|
188
|
+
---
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A page becomes an **RSS post** by carrying a `date:`; `excerpt:` is its feed summary (else `description:`, else — for a plain `.md` — its first paragraph). The sitemap's `<lastmod>` is the `date:` if set, else the file's git/last-modified date. `robots.txt` is always written; the `Sitemap:` line only appears with a `site_url`. Without `site_url`, robots still emits and the build prints a hint — it never breaks.
|
|
192
|
+
|
|
193
|
+
## Drafts
|
|
194
|
+
|
|
195
|
+
Mark a page `draft: true` and it stays out of production: `darkmown build` leaves it out of `dist`, `routes.json`, the sitemap, and the feed — even if it has a `date:`. `darkmown dev` still builds and serves it (with a "DRAFT" banner you only see in dev), and `darkmown build --drafts` includes drafts for a staging deploy. This is separate from the permanent `.`/`-`/`_` filename hiding — a hidden name is private forever; `draft:` is a switch you flip when the page is ready.
|
|
196
|
+
|
|
179
197
|
## Instant navigation
|
|
180
198
|
|
|
181
199
|
Add `transitions: true` to a page's frontmatter for **instant, flash-free navigation** — zero JavaScript. It emits a directional fade+slide view transition for the page swap 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). It honors `prefers-reduced-motion`, 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 — test the built site with DevTools closed.)
|
|
@@ -408,6 +426,35 @@ Three compile-time-only directives (they emit no `data-wd-*`, so a media page st
|
|
|
408
426
|
|
|
409
427
|
`:video`/`:audio` emit a hardened HTML5 player — `preload="metadata"` and `controls` by default, whitelisted flags (`autoplay` implies `muted`), and the `:fetch` URL scheme guard. `:embed` rewrites a YouTube/Vimeo link to its no-cookie/player form in a lazy, responsive `16/9` iframe. See the zero-JS [Media demo](/media/).
|
|
410
428
|
|
|
429
|
+
## Syntax highlighting
|
|
430
|
+
|
|
431
|
+
Fenced code blocks with a language are highlighted at **build time** — HTML and CSS only, no client JavaScript. Tag the fence with a language:
|
|
432
|
+
|
|
433
|
+
````md
|
|
434
|
+
```js
|
|
435
|
+
const greeting = "Darkmown"; // highlighted at build time
|
|
436
|
+
```
|
|
437
|
+
````
|
|
438
|
+
|
|
439
|
+
The highlighter is [highlight.js](https://highlightjs.org/) and isn't configurable (one closed default, like everything else). It maps onto your skin's `$code-*` tokens, so highlighted code **dark-modes for free** through the same `tokens dark` / `:theme` system — no extra wiring. Define the palette once:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
tokens
|
|
443
|
+
code-bg #1b2420
|
|
444
|
+
code-fg #e9efe7
|
|
445
|
+
code-keyword #d9a8e0
|
|
446
|
+
code-string #97d892
|
|
447
|
+
code-comment #859289
|
|
448
|
+
code-function #88c4ee
|
|
449
|
+
code-number #ecae78
|
|
450
|
+
code-punctuation #c1ccc6
|
|
451
|
+
tokens dark
|
|
452
|
+
code-bg #100d0a
|
|
453
|
+
code-keyword #e2b9e8
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The framework ships a small default `$code-*` set, so highlighting looks right out of the box. The stylesheet is **pay-for-what-you-use**: it's emitted and linked only on pages that actually contain a highlighted block — a page with no code ships nothing extra. A fence with an unknown or absent language renders as plain escaped `<code>` (no highlighting, no error); inline `` `code` `` is never highlighted, and there are deliberately no line numbers (they break copy-paste). See the live [Syntax highlighting demo](/highlight/).
|
|
457
|
+
|
|
411
458
|
## Trust boundary
|
|
412
459
|
|
|
413
460
|
Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
main.app-highlight
|
|
2
|
+
max-width 1000px
|
|
3
|
+
margin 0 auto
|
|
4
|
+
padding 2.5rem 1.5rem
|
|
5
|
+
|
|
6
|
+
.hl-eyebrow
|
|
7
|
+
text-transform uppercase
|
|
8
|
+
letter-spacing .08em
|
|
9
|
+
font-size .8rem
|
|
10
|
+
opacity .7
|
|
11
|
+
|
|
12
|
+
.hl-tagline
|
|
13
|
+
font-size 1.05rem
|
|
14
|
+
opacity .85
|
|
15
|
+
margin-top .5rem
|
|
16
|
+
max-width 60ch
|
|
17
|
+
|
|
18
|
+
/* Theme toggle — reuse the ledger's pill-button switch so flipping the theme
|
|
19
|
+
recolors every highlighted block at once. */
|
|
20
|
+
.app-highlight .theme-switch
|
|
21
|
+
display inline-flex
|
|
22
|
+
align-items center
|
|
23
|
+
gap .4rem
|
|
24
|
+
font-size .75rem
|
|
25
|
+
font-weight 600
|
|
26
|
+
letter-spacing .04em
|
|
27
|
+
text-transform uppercase
|
|
28
|
+
color $muted
|
|
29
|
+
margin 1.5rem 0 .5rem
|
|
30
|
+
|
|
31
|
+
.app-highlight .theme-switch button[data-wd-action]
|
|
32
|
+
border 1px solid $line
|
|
33
|
+
bg $panel
|
|
34
|
+
color $ink-soft
|
|
35
|
+
font-size .8rem
|
|
36
|
+
font-weight 600
|
|
37
|
+
padding .4rem .8rem
|
|
38
|
+
radius 999px
|
|
39
|
+
cursor pointer
|
|
40
|
+
transition color .14s ease, border-color .14s ease, background .14s ease
|
|
41
|
+
|
|
42
|
+
.app-highlight .theme-switch button[data-wd-action]:hover
|
|
43
|
+
color $ink
|
|
44
|
+
border-color $accent
|
|
45
|
+
|
|
46
|
+
.hl-grid
|
|
47
|
+
margin 1rem 0 1.5rem
|
|
48
|
+
|
|
49
|
+
.hl-panel
|
|
50
|
+
padding 1.4rem 1.5rem
|
|
51
|
+
|
|
52
|
+
.hl-panel h3
|
|
53
|
+
margin-top 0
|
|
54
|
+
margin-bottom .6rem
|
|
55
|
+
font-size 1rem
|
|
56
|
+
color $muted
|
|
57
|
+
text-transform uppercase
|
|
58
|
+
letter-spacing .04em
|
|
59
|
+
|
|
60
|
+
.hl-panel pre
|
|
61
|
+
margin 0
|
|
62
|
+
|
|
63
|
+
.hl-note
|
|
64
|
+
margin-top 1.5rem
|
|
65
|
+
padding 1rem 1.25rem
|
|
66
|
+
border 1px solid $line
|
|
67
|
+
border-left 3px solid $accent
|
|
68
|
+
bg $panel
|
|
69
|
+
radius $radius-sm
|
|
70
|
+
color $ink-soft
|
|
71
|
+
font-size .95rem
|
|
72
|
+
|
|
73
|
+
.hl-degrade
|
|
74
|
+
border-left-color $muted
|
|
75
|
+
|
|
76
|
+
@media (max-width: 760px)
|
|
77
|
+
.hl-grid
|
|
78
|
+
grid-template-columns 1fr
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Syntax highlighting — code that dark-modes for free
|
|
3
|
+
description: Build-time syntax highlighting in plain Markdown. Fenced code blocks highlight at compile time (zero client JS) and recolor with your theme through the same tokens dark / :theme system — flip the toggle and watch every language recolor at once.
|
|
4
|
+
image: https://darkmown.com/__wd/media/og-showcase.png
|
|
5
|
+
transitions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
@include /nav.wd
|
|
9
|
+
@include /base.wd
|
|
10
|
+
|
|
11
|
+
<main class="wide app-highlight">
|
|
12
|
+
|
|
13
|
+
::: section #highlight
|
|
14
|
+
|
|
15
|
+
:theme
|
|
16
|
+
|
|
17
|
+
::: hl-head
|
|
18
|
+
::: eyebrow .hl-eyebrow
|
|
19
|
+
Build-time syntax highlighting
|
|
20
|
+
:::
|
|
21
|
+
|
|
22
|
+
# *Highlighted code*
|
|
23
|
+
|
|
24
|
+
::: hl-tagline
|
|
25
|
+
Fenced code blocks highlight at **build time** — HTML and CSS only, no client
|
|
26
|
+
JavaScript. Colors come from your skin's `$code-*` tokens, so highlighting
|
|
27
|
+
recolors with the theme through the same `tokens dark` / `:theme` system that
|
|
28
|
+
drives the rest of the page. Flip the toggle and watch every block below recolor
|
|
29
|
+
at once — no toggle script for the code, no flash.
|
|
30
|
+
:::
|
|
31
|
+
:::
|
|
32
|
+
|
|
33
|
+
::: theme-switch
|
|
34
|
+
Theme
|
|
35
|
+
:button "Auto" -> theme = "auto"
|
|
36
|
+
:button "Light" -> theme = "light"
|
|
37
|
+
:button "Dark" -> theme = "dark"
|
|
38
|
+
:::
|
|
39
|
+
|
|
40
|
+
::: grid .cols-2 .hl-grid
|
|
41
|
+
|
|
42
|
+
::: card .hl-panel
|
|
43
|
+
### JavaScript
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// A tiny reactive island, declared in Markdown.
|
|
47
|
+
const cart = wd.state("cart");
|
|
48
|
+
function total(items) {
|
|
49
|
+
return items.reduce((sum, it) => sum + it.price, 0);
|
|
50
|
+
}
|
|
51
|
+
wd.subscribe("cart", (items) => {
|
|
52
|
+
document.title = `Cart (${items.length})`;
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
:::
|
|
56
|
+
|
|
57
|
+
::: card .hl-panel
|
|
58
|
+
### Python
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from dataclasses import dataclass
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Expense:
|
|
65
|
+
merchant: str
|
|
66
|
+
amount: float # in dollars
|
|
67
|
+
|
|
68
|
+
def total(rows: list[Expense]) -> float:
|
|
69
|
+
return sum(r.amount for r in rows)
|
|
70
|
+
```
|
|
71
|
+
:::
|
|
72
|
+
|
|
73
|
+
::: card .hl-panel
|
|
74
|
+
### CSS
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
.card {
|
|
78
|
+
background: var(--panel, #fff);
|
|
79
|
+
border: 1px solid var(--line);
|
|
80
|
+
border-radius: 12px;
|
|
81
|
+
padding: 1.4rem;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
:::
|
|
85
|
+
|
|
86
|
+
::: card .hl-panel
|
|
87
|
+
### JSON
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"name": "@zvndev/darkmown",
|
|
92
|
+
"type": "module",
|
|
93
|
+
"zeroJsOnStaticPages": true,
|
|
94
|
+
"runtimeBudgetKb": 8
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
:::
|
|
98
|
+
|
|
99
|
+
::: card .hl-panel
|
|
100
|
+
### Bash
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx @zvndev/darkmown init --template blog
|
|
104
|
+
cd blog && npx darkmown dev # live reload on :5173
|
|
105
|
+
```
|
|
106
|
+
:::
|
|
107
|
+
|
|
108
|
+
::: card .hl-panel
|
|
109
|
+
### HTML
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<main class="wide">
|
|
113
|
+
<a class="btn primary" href="/docs/">Read the docs</a>
|
|
114
|
+
</main>
|
|
115
|
+
```
|
|
116
|
+
:::
|
|
117
|
+
|
|
118
|
+
:::
|
|
119
|
+
|
|
120
|
+
::: hl-note
|
|
121
|
+
**How it works.** The highlighter is [highlight.js](https://highlightjs.org/),
|
|
122
|
+
wired into Markdown's fenced-code path and mapped onto `$code-*` skin tokens.
|
|
123
|
+
The stylesheet is **pay-for-what-you-use** — emitted and linked only on pages
|
|
124
|
+
with a highlighted block. The highlighting itself ships zero JavaScript; the only
|
|
125
|
+
reactive thing on this page is the theme toggle above (so you can demo the
|
|
126
|
+
recolor without changing your OS setting). A fence with an unknown or absent
|
|
127
|
+
language renders as plain escaped `<code>`, and inline `` `code` `` is never
|
|
128
|
+
highlighted.
|
|
129
|
+
:::
|
|
130
|
+
|
|
131
|
+
::: hl-note .hl-degrade
|
|
132
|
+
**Graceful degradation.** A bare fence (no language) or an unknown one stays
|
|
133
|
+
plain — no highlighting, no error:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
just a plain block — no language, so no highlighting
|
|
137
|
+
```
|
|
138
|
+
:::
|
|
139
|
+
|
|
140
|
+
:::
|
|
141
|
+
|
|
142
|
+
</main>
|
package/site/pages/index.wd
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
title: Darkmown — Markdown that runs, not Markdown that needs React
|
|
3
3
|
description: A live store widget — search filter, add-to-cart, running total — written as one Markdown file that runs. The same outcome in Markdoc needs a React component, hydration, and a build step. Compare them side by side.
|
|
4
4
|
image: https://darkmown.com/__wd/media/og.png
|
|
5
|
+
site_url: https://darkmown.com
|
|
5
6
|
transitions: true
|
|
6
7
|
---
|
|
7
8
|
|