@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 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**.
@@ -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.2.0",
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
@@ -5,6 +5,7 @@
5
5
  <a href="/showcase/">Showcase</a>
6
6
  <a href="/reactive/">Reactive</a>
7
7
  <a href="/data/">Data</a>
8
+ <a href="/highlight/">Highlight</a>
8
9
  </span>
9
10
  <a class="nav-gh" href="https://github.com/ZVN-DEV/darkmown">GitHub ↗</a>
10
11
  </nav>
@@ -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.
@@ -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>
@@ -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