@zvndev/darkmown 1.4.0 → 1.5.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.
@@ -0,0 +1,144 @@
1
+ /* The Darkmown blog index — a collection-driven, paginated listing. Thin layer
2
+ over /base.wd (tokens, typography, dark mode); only the post-card grid and the
3
+ pager live here. Namespaced under .app-blog so it can't leak. */
4
+
5
+ .app-blog
6
+ max-width 52rem
7
+
8
+ .app-blog .blog-head
9
+ margin 2.5rem 0 2rem
10
+
11
+ .app-blog .blog-head .eyebrow
12
+ display inline-block
13
+ font-size .78rem
14
+ font-weight 600
15
+ text-transform uppercase
16
+ letter-spacing .12em
17
+ color var(--accent)
18
+ margin-bottom .6rem
19
+
20
+ .app-blog .blog-head h1
21
+ margin .2rem 0 .8rem
22
+ line-height 1.1
23
+
24
+ .app-blog .blog-head p
25
+ color var(--muted)
26
+ max-width 38rem
27
+
28
+ /* The list of posts. */
29
+ .app-blog .post-list
30
+ display flex
31
+ flex-direction column
32
+ gap 1.1rem
33
+ margin 2rem 0
34
+
35
+ .app-blog .post-card
36
+ background var(--panel)
37
+ border 1px solid var(--line)
38
+ radius var(--radius-lg)
39
+ padding 1.5rem 1.6rem
40
+ transition border-color .15s ease, transform .15s ease, box-shadow .15s ease
41
+
42
+ .app-blog .post-card:hover
43
+ border-color var(--accent)
44
+ transform translateY(-2px)
45
+ box-shadow 0 8px 24px -16px var(--accent-deep)
46
+
47
+ .app-blog .post-meta
48
+ display flex
49
+ align-items center
50
+ flex-wrap wrap
51
+ gap .55rem
52
+ margin-bottom .55rem
53
+
54
+ /* Each meta item (time / tag) renders as its own paragraph; strip the default
55
+ block margins so they sit inline in the flex row. */
56
+ .app-blog .post-meta p
57
+ margin 0
58
+ display inline-flex
59
+
60
+ .app-blog .post-meta time
61
+ font-size .82rem
62
+ font-weight 600
63
+ color var(--muted)
64
+ font-variant-numeric tabular-nums
65
+
66
+ .app-blog .tag
67
+ font-size .72rem
68
+ font-weight 600
69
+ text-transform uppercase
70
+ letter-spacing .06em
71
+ color var(--accent-deep)
72
+ background var(--accent-soft)
73
+ border 1px solid color-mix(in srgb, var(--accent) 30%, transparent)
74
+ padding .15rem .5rem
75
+ radius 999px
76
+
77
+ .app-blog .post-card h3
78
+ margin .1rem 0 .5rem
79
+ font-size 1.4rem
80
+ line-height 1.2
81
+
82
+ .app-blog .post-card h3 a
83
+ color var(--ink)
84
+ text-decoration none
85
+
86
+ .app-blog .post-card h3 a:hover
87
+ color var(--accent)
88
+
89
+ .app-blog .post-excerpt
90
+ color var(--ink-soft)
91
+ margin 0 0 .8rem
92
+ line-height 1.55
93
+
94
+ .app-blog .post-more a
95
+ font-size .88rem
96
+ font-weight 600
97
+ color var(--accent)
98
+ text-decoration none
99
+
100
+ .app-blog .post-more a:hover
101
+ text-decoration underline
102
+
103
+ .app-blog .empty-note
104
+ color var(--muted)
105
+ font-style italic
106
+ padding 2rem
107
+ text-align center
108
+ border 1px dashed var(--line)
109
+ radius var(--radius)
110
+
111
+ /* The pager — plain links to the generated routes, zero JS. */
112
+ .app-blog .pager
113
+ display flex
114
+ align-items center
115
+ justify-content space-between
116
+ gap 1rem
117
+ margin 2.5rem 0 3rem
118
+ padding-top 1.4rem
119
+ border-top 1px solid var(--line)
120
+
121
+ .app-blog .pager-side
122
+ flex 1
123
+ min-width 0
124
+
125
+ .app-blog .pager-side p
126
+ margin 0
127
+
128
+ .app-blog .pager-side--end
129
+ text-align right
130
+
131
+ .app-blog .pager-now
132
+ font-size .85rem
133
+ font-weight 600
134
+ color var(--muted)
135
+ white-space nowrap
136
+ font-variant-numeric tabular-nums
137
+
138
+ .app-blog .pager-link
139
+ font-weight 600
140
+ color var(--accent)
141
+ text-decoration none
142
+
143
+ .app-blog .pager-link:hover
144
+ text-decoration underline
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: The Darkmown Blog
3
+ description: Notes on shipping Markdown that runs — collections, feeds, drafts, and zero-JS releases. This whole index is one @loop over a folder.
4
+ image: https://darkmown.com/__wd/media/og.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <main class="wide app-blog">
12
+
13
+ <header class="blog-head">
14
+ <span class="eyebrow">The Darkmown Blog</span>
15
+
16
+ # Notes from the framework
17
+
18
+ Every post below is a plain `.md` file in `site/pages/blog/`. This page is **one
19
+ `@loop` over that folder** — sorted newest-first, paginated, and 100% static
20
+ HTML. No hand-maintained list, no config, no JavaScript.
21
+
22
+ </header>
23
+
24
+ <section class="post-list">
25
+
26
+ @loop blog into post sort by post.date desc paginate 3
27
+ ::: card .post-card
28
+ ::: post-meta
29
+ <time>{ post.date }</time>
30
+ @loop post.tags into tag
31
+ <span class="tag">{ tag }</span>
32
+ @endloop
33
+ :::
34
+
35
+ ### [{ post.title }]({ post.url })
36
+
37
+ ::: post-excerpt
38
+ { post.excerpt }
39
+ :::
40
+
41
+ ::: post-more
42
+ [Read post → ]({ post.url })
43
+ :::
44
+ :::
45
+ @empty
46
+ ::: empty-note
47
+ No posts yet. Add a dated `.md` file to `site/pages/blog/`.
48
+ :::
49
+ @endloop
50
+
51
+ </section>
52
+
53
+ ::: pager
54
+ ::: pager-side
55
+ :if page.prev
56
+ [← Newer posts]({ page.prev }){.pager-link}
57
+ :endif
58
+ :::
59
+
60
+ ::: pager-now
61
+ Page { page.current } of { page.total }
62
+ :::
63
+
64
+ ::: pager-side .pager-side--end
65
+ :if page.next
66
+ [Older posts →]({ page.next }){.pager-link}
67
+ :endif
68
+ :::
69
+ :::
70
+
71
+ </main>
72
+
73
+ @include /footer.wd
@@ -3,6 +3,7 @@ title: Shipping feeds without a config file
3
3
  description: Site identity lives in the home page frontmatter — one site_url field turns on sitemap.xml and rss.xml.
4
4
  date: 2026-06-26
5
5
  excerpt: One reserved frontmatter field on the home page — site_url — turns on the whole feeds story. No config loader, no plugin.
6
+ tags: [feeds, seo]
6
7
  ---
7
8
 
8
9
  # Shipping feeds without a config file
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: Typed frontmatter with a four-line schema
3
+ description: Drop a _schema.wd in a collection and every entry is validated at build time — missing fields, wrong types, and typos all fail the build.
4
+ date: 2026-06-29
5
+ excerpt: A typo'd frontmatter key used to ship silently. With a collection schema, titel instead of title fails the build with a file and line.
6
+ tags: [collections, schema, dx]
7
+ ---
8
+
9
+ # Typed frontmatter with a four-line schema
10
+
11
+ Frontmatter is convenient until a typo bites. You write `titel:` instead of
12
+ `title:`, the field silently becomes empty, and the bug surfaces as a blank
13
+ heading in production. Darkmown closes that gap with an *optional* schema — one
14
+ file at the collection root, frontmatter-shaped, written exactly like the
15
+ frontmatter it checks:
16
+
17
+ ```wd
18
+ ---
19
+ title: string
20
+ date: date
21
+ description: string
22
+ excerpt: string?
23
+ tags: string[]?
24
+ ---
25
+ ```
26
+
27
+ The vocabulary is small and closed on purpose: `string`, `number`, `boolean`,
28
+ `date`, and `string[]`, with a trailing `?` to make a field optional. That is
29
+ the whole DSL. An unknown type token is a compile error in the schema file
30
+ itself.
31
+
32
+ ## What it catches
33
+
34
+ At build time, every entry in the collection is checked against the rules. A
35
+ missing required field, a value of the wrong type, or an extra field the schema
36
+ never declared each fails the build — and the error names the offending entry
37
+ by path and the field by name, so the fix is obvious:
38
+
39
+ ```
40
+ Missing required field "date" (date) in /blog/example/ (collection "blog").
41
+ ```
42
+
43
+ Validation is opt-in. No `_schema.wd`, no validation — the same posture as
44
+ `scoped` styles and the `draft` flag. Add the file when you want the guardrails;
45
+ leave it out while you are still sketching.
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Zero JavaScript, by default
3
+ description: A Darkmown page ships no framework JavaScript unless it declares reactive behavior. The blog you are reading proves it.
4
+ date: 2026-06-25
5
+ excerpt: View source on this page. There is no runtime script tag, because nothing on it is reactive — and the framework knows the difference.
6
+ tags: [performance, philosophy]
7
+ ---
8
+
9
+ # Zero JavaScript, by default
10
+
11
+ Open your browser's view-source on this post. Look for a `<script>` that loads a
12
+ framework runtime. There isn't one — and there isn't one on the blog index, or
13
+ on any of its paginated pages either. None of them declare reactive behavior, so
14
+ none of them pay for it.
15
+
16
+ That is the whole bargain. A `.md` file is plain CommonMark. Rename it to `.wd`
17
+ and you unlock directives — state, loops, conditionals — but you only ship the
18
+ ~7.4 KB runtime on the pages that actually use them. A listing built from a
19
+ collection loop resolves entirely at build time, so it stays static HTML.
20
+
21
+ ## The extension is the feature gate
22
+
23
+ There is no flag to toggle, no "static export" mode to remember. The compiler
24
+ tracks whether a page declared anything reactive and emits `runtime: false` into
25
+ `routes.json` when it didn't. A collection listing, a paginated archive, a page
26
+ of prose with a code block — all static, all cacheable on a CDN forever, all
27
+ fast on the first byte because there is nothing to hydrate.
28
+
29
+ When you do want an island of interactivity — a search box, a cart, a sortable
30
+ table — you declare it, and only that page loads the runtime. The rest of the
31
+ site stays weightless.
@@ -29,18 +29,44 @@ h2
29
29
  padding-top 1.2rem
30
30
 
31
31
  button
32
- border 1px solid $ink
33
- bg $ink
32
+ display inline-flex
33
+ align-items center
34
+ justify-content center
35
+ gap .5rem
36
+ border 1px solid $accent
37
+ bg $accent
34
38
  color white
35
- padding .72rem 1rem
39
+ padding .6rem 1.1rem
36
40
  margin .3rem .5rem .3rem 0
37
- radius $radius
38
- font inherit
41
+ radius 999px
42
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
43
+ font-size .92rem
44
+ font-weight 560
45
+ line-height 1.15
39
46
  cursor pointer
47
+ appearance none
48
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
49
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
40
50
 
41
51
  button:hover
42
52
  bg $accent
43
53
  border-color $accent
54
+ transform translateY(-1px)
55
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
56
+
57
+ button:active
58
+ transform translateY(0)
59
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
60
+
61
+ button:focus-visible
62
+ outline 2px solid $accent
63
+ outline-offset 2px
64
+
65
+ button:disabled
66
+ opacity .5
67
+ cursor not-allowed
68
+ box-shadow none
69
+ transform none
44
70
 
45
71
  a
46
72
  color $accent
@@ -75,21 +101,36 @@ form
75
101
  margin 1rem 0
76
102
 
77
103
  input
78
- padding .7rem .9rem
104
+ padding .62rem .85rem
79
105
  border 1px solid $line
80
106
  radius $radius
81
107
  font inherit
108
+ font-size .95rem
82
109
  bg $panel
110
+ appearance none
111
+ transition border-color .14s ease, box-shadow .14s ease
112
+
113
+ input:hover
114
+ border-color $accent
115
+
116
+ input:focus-visible
117
+ outline none
118
+ border-color $accent
119
+ box-shadow 0 0 0 3px rgba(22,100,90,.18)
83
120
 
84
121
  .pulse-button
85
- border 0
122
+ border 1px solid $warm
86
123
  bg $warm
87
124
  color white
88
- padding .85rem 1rem
89
- radius $radius
125
+ padding .6rem 1.1rem
126
+ radius 999px
90
127
  font inherit
91
128
  cursor pointer
92
129
 
130
+ .pulse-button:hover
131
+ transform translateY(-1px)
132
+ box-shadow 0 8px 18px -8px rgba(168,63,42,.5)
133
+
93
134
  .tally-card
94
135
  border 1px solid $line
95
136
  bg $panel
@@ -148,6 +148,53 @@ Nothing left to do.
148
148
 
149
149
  These clauses stay build-time when the source and every argument are static — a sorted, limited loop over a JSON file ships zero JS. The loop turns reactive only when the source or a clause reads `:state`/`:store`/`:fetch` data.
150
150
 
151
+ ## Content collections
152
+
153
+ Any folder under `site/pages/` is a **collection** — loop it by its bare name. No `content/` root, no marker file: a `site/pages/blog/` folder of posts *is* the `blog` collection. The whole [blog on this site](/blog/) is one such loop.
154
+
155
+ ```wd
156
+ @loop blog into post sort by post.date desc
157
+ - [{ post.title }]({ post.url }) — { post.date }
158
+ @endloop
159
+ ```
160
+
161
+ Each entry's frontmatter is a row, plus three derived fields: `{ post.url }` (the route), `{ post.slug }` (the filename), and `{ post.excerpt }` (the frontmatter `excerpt:`, else the first paragraph of a `.md` body). It's the same loop with the same clauses, resolved at build time — a pure listing ships zero JS. Drafts never appear in a default-build listing (only under `darkmown build --drafts`).
162
+
163
+ ### Typed schema — `_schema.wd`
164
+
165
+ Drop a `_schema.wd` at a collection's root to validate every entry at build time. It's frontmatter-shaped, one `field: type` per line:
166
+
167
+ ```wd
168
+ ---
169
+ title: string
170
+ date: date
171
+ excerpt: string?
172
+ tags: string[]?
173
+ ---
174
+ ```
175
+
176
+ Closed types: `string`, `number`, `boolean`, `date`, `string[]`, each with `?` for optional. A missing required field, a wrong type, an unknown extra field (typo guard), or an unknown type token fails the build with a `file:line`. No `_schema.wd` means no validation — it's opt-in.
177
+
178
+ ### Pagination — `paginate N`
179
+
180
+ Add `paginate N` (collections only) to split a listing into static pages. Page 1 keeps the listing route; pages 2+ are at `/<route>/page/2/`, `/page/3/`, … all static HTML.
181
+
182
+ ```wd
183
+ @loop blog into post sort by post.date desc paginate 5
184
+ - [{ post.title }]({ post.url })
185
+ @endloop
186
+
187
+ Page { page.current } of { page.total }
188
+ :if page.prev
189
+ [← Newer]({ page.prev })
190
+ :endif
191
+ :if page.next
192
+ [Older →]({ page.next })
193
+ :endif
194
+ ```
195
+
196
+ The pager — `{ page.current }`, `{ page.total }`, `{ page.prev }`, `{ page.next }` (URLs, empty at the ends) — is exposed to the whole page as plain links. Zero JS. `paginate` can't combine with `offset`/`limit` (it owns the slice).
197
+
151
198
  ## Interpolation
152
199
 
153
200
  One syntax everywhere: `{ name }` or `{ name.path }`.
@@ -32,28 +32,65 @@ a
32
32
  color $accent
33
33
 
34
34
  button
35
- border 1px solid $ink
36
- bg $ink
35
+ display inline-flex
36
+ align-items center
37
+ justify-content center
38
+ gap .5rem
39
+ border 1px solid $accent
40
+ bg $accent
37
41
  color white
38
- padding .72rem 1rem
42
+ padding .6rem 1.1rem
39
43
  margin .3rem .5rem .3rem 0
40
- radius $radius
41
- font inherit
44
+ radius 999px
45
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
46
+ font-size .92rem
47
+ font-weight 560
48
+ line-height 1.15
42
49
  cursor pointer
50
+ appearance none
51
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
52
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
43
53
 
44
54
  button:hover
45
55
  bg $accent
46
56
  border-color $accent
57
+ transform translateY(-1px)
58
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
59
+
60
+ button:active
61
+ transform translateY(0)
62
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
63
+
64
+ button:focus-visible
65
+ outline 2px solid $accent
66
+ outline-offset 2px
67
+
68
+ button:disabled
69
+ opacity .5
70
+ cursor not-allowed
71
+ box-shadow none
72
+ transform none
47
73
 
48
74
  input
49
- padding .7rem .9rem
75
+ padding .62rem .85rem
50
76
  border 1px solid $line
51
77
  radius $radius
52
78
  font inherit
79
+ font-size .95rem
53
80
  bg $panel
54
81
  width 100%
55
82
  max-width 360px
56
83
  margin 1rem 0
84
+ appearance none
85
+ transition border-color .14s ease, box-shadow .14s ease
86
+
87
+ input:hover
88
+ border-color $accent
89
+
90
+ input:focus-visible
91
+ outline none
92
+ border-color $accent
93
+ box-shadow 0 0 0 3px rgba(22,100,90,.18)
57
94
 
58
95
  ul
59
96
  border 1px solid $line
@@ -29,18 +29,44 @@ h2
29
29
  padding-top 1.2rem
30
30
 
31
31
  button
32
- border 1px solid $ink
33
- bg $ink
32
+ display inline-flex
33
+ align-items center
34
+ justify-content center
35
+ gap .5rem
36
+ border 1px solid $accent
37
+ bg $accent
34
38
  color white
35
- padding .72rem 1rem
39
+ padding .6rem 1.1rem
36
40
  margin .3rem .5rem .3rem 0
37
- radius $radius
38
- font inherit
41
+ radius 999px
42
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
43
+ font-size .92rem
44
+ font-weight 560
45
+ line-height 1.15
39
46
  cursor pointer
47
+ appearance none
48
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
49
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
40
50
 
41
51
  button:hover
42
52
  bg $accent
43
53
  border-color $accent
54
+ transform translateY(-1px)
55
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
56
+
57
+ button:active
58
+ transform translateY(0)
59
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
60
+
61
+ button:focus-visible
62
+ outline 2px solid $accent
63
+ outline-offset 2px
64
+
65
+ button:disabled
66
+ opacity .5
67
+ cursor not-allowed
68
+ box-shadow none
69
+ transform none
44
70
 
45
71
  a
46
72
  color $accent
@@ -89,4 +115,10 @@ ul
89
115
  bg transparent
90
116
  color $warm
91
117
  border 1px solid $line
92
- padding .35rem .8rem
118
+ padding .4rem .9rem
119
+ box-shadow none
120
+
121
+ .card.line button:hover
122
+ bg $warm
123
+ border-color $warm
124
+ color white
@@ -32,28 +32,65 @@ a
32
32
  color $accent
33
33
 
34
34
  button
35
- border 1px solid $ink
36
- bg $ink
35
+ display inline-flex
36
+ align-items center
37
+ justify-content center
38
+ gap .5rem
39
+ border 1px solid $accent
40
+ bg $accent
37
41
  color white
38
- padding .72rem 1rem
42
+ padding .6rem 1.1rem
39
43
  margin .3rem .5rem .3rem 0
40
- radius $radius
41
- font inherit
44
+ radius 999px
45
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
46
+ font-size .92rem
47
+ font-weight 560
48
+ line-height 1.15
42
49
  cursor pointer
50
+ appearance none
51
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
52
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
43
53
 
44
54
  button:hover
45
55
  bg $accent
46
56
  border-color $accent
57
+ transform translateY(-1px)
58
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
59
+
60
+ button:active
61
+ transform translateY(0)
62
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
63
+
64
+ button:focus-visible
65
+ outline 2px solid $accent
66
+ outline-offset 2px
67
+
68
+ button:disabled
69
+ opacity .5
70
+ cursor not-allowed
71
+ box-shadow none
72
+ transform none
47
73
 
48
74
  input
49
- padding .7rem .9rem
75
+ padding .62rem .85rem
50
76
  border 1px solid $line
51
77
  radius $radius
52
78
  font inherit
79
+ font-size .95rem
53
80
  bg $panel
54
81
  width 100%
55
82
  max-width 360px
56
83
  margin 1rem 0
84
+ appearance none
85
+ transition border-color .14s ease, box-shadow .14s ease
86
+
87
+ input:hover
88
+ border-color $accent
89
+
90
+ input:focus-visible
91
+ outline none
92
+ border-color $accent
93
+ box-shadow 0 0 0 3px rgba(22,100,90,.18)
57
94
 
58
95
  #shop
59
96
  border 1px solid $line