@zvndev/darkmown 1.5.0 → 2.0.1

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.
Files changed (95) hide show
  1. package/AGENTS.md +18 -4
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +49 -19
  4. package/SECURITY.md +21 -18
  5. package/docs/cli.md +5 -1
  6. package/docs/spec-alignment.md +21 -7
  7. package/package.json +3 -2
  8. package/site/_/base.skin +8 -0
  9. package/site/_/base.wd +4 -0
  10. package/site/_/feature-card.wd +4 -0
  11. package/site/_/footer.wd +5 -1
  12. package/site/_/nav.wd +4 -0
  13. package/site/_/scoped-promo.wd +4 -0
  14. package/site/_/scoped-stat.wd +4 -0
  15. package/site/pages/404.wd +1 -0
  16. package/site/pages/app.wd +2 -1
  17. package/site/pages/blog/collections-and-pagination.skin +127 -0
  18. package/site/pages/blog/drafts-that-never-leak.skin +127 -0
  19. package/site/pages/blog/index.wd +1 -0
  20. package/site/pages/blog/shipping-feeds.skin +127 -0
  21. package/site/pages/blog/typed-frontmatter.skin +127 -0
  22. package/site/pages/blog/unfinished-thoughts.skin +127 -0
  23. package/site/pages/blog/zero-js-by-default.md +1 -1
  24. package/site/pages/blog/zero-js-by-default.skin +127 -0
  25. package/site/pages/carousel/index.wd +1 -0
  26. package/site/pages/compass/index.wd +1 -0
  27. package/site/pages/data.wd +3 -0
  28. package/site/pages/docs/-relative-note.wd +4 -0
  29. package/site/pages/docs/index.skin +27 -0
  30. package/site/pages/docs/index.wd +85 -14
  31. package/site/pages/folio/-style.wd +4 -0
  32. package/site/pages/folio/cart.wd +1 -0
  33. package/site/pages/folio/index.wd +1 -0
  34. package/site/pages/forge/index.wd +1 -0
  35. package/site/pages/highlight/index.wd +1 -0
  36. package/site/pages/index.js +19 -0
  37. package/site/pages/index.skin +46 -1
  38. package/site/pages/index.wd +6 -3
  39. package/site/pages/interactions/index.wd +1 -0
  40. package/site/pages/ledger/index.wd +1 -0
  41. package/site/pages/loops.wd +1 -0
  42. package/site/pages/media/index.wd +1 -0
  43. package/site/pages/pulse/index.wd +1 -0
  44. package/site/pages/reactive.wd +1 -0
  45. package/site/pages/scoped/index.wd +1 -0
  46. package/site/pages/showcase/index.wd +2 -1
  47. package/site/pages/store.wd +1 -0
  48. package/site/pages/theme.wd +1 -0
  49. package/src/builder.js +444 -65
  50. package/src/cli.js +96 -18
  51. package/src/compiler/actions.js +303 -0
  52. package/src/compiler/body.js +36 -24
  53. package/src/compiler/context.js +65 -6
  54. package/src/compiler/directives.js +31 -1470
  55. package/src/compiler/fetch.js +159 -0
  56. package/src/compiler/format.js +17 -4
  57. package/src/compiler/forms.js +456 -0
  58. package/src/compiler/frontmatter.js +10 -5
  59. package/src/compiler/includes.js +75 -4
  60. package/src/compiler/index.js +15 -3
  61. package/src/compiler/loops.js +85 -23
  62. package/src/compiler/markdown.js +65 -15
  63. package/src/compiler/media.js +133 -0
  64. package/src/compiler/page.js +88 -77
  65. package/src/compiler/state.js +168 -0
  66. package/src/compiler/structure.js +399 -0
  67. package/src/dev.js +76 -0
  68. package/src/headers.js +56 -17
  69. package/src/router.js +12 -1
  70. package/src/runtime.js +5 -3
  71. package/src/statics.js +63 -12
  72. package/src/templates/blog/README.md +13 -4
  73. package/src/templates/blog/site/_/nav.wd +7 -6
  74. package/src/templates/blog/site/pages/index.skin +10 -4
  75. package/src/templates/blog/site/pages/index.wd +11 -6
  76. package/src/templates/blog/site/pages/posts/_schema.wd +16 -0
  77. package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
  78. package/src/templates/blog/site/pages/posts/hello-darkmown.skin +59 -0
  79. package/src/templates/blog/site/pages/posts/markdown-native.md +9 -4
  80. package/src/templates/blog/site/pages/posts/markdown-native.skin +59 -0
  81. package/src/templates/dashboard/site/_/nav.wd +7 -6
  82. package/src/templates/dashboard/site/pages/index.skin +7 -4
  83. package/src/templates/dashboard/site/pages/index.wd +2 -2
  84. package/src/templates/landing/site/_/nav.wd +7 -6
  85. package/src/templates/landing/site/pages/index.skin +13 -4
  86. package/src/templates/landing/site/pages/index.wd +22 -8
  87. package/src/templates/starter/site/_/nav.wd +8 -7
  88. package/src/templates/starter/site/pages/about.md +8 -2
  89. package/src/templates/starter/site/pages/about.skin +44 -0
  90. package/src/templates/starter/site/pages/index.skin +10 -6
  91. package/src/templates/starter/site/pages/index.wd +2 -2
  92. package/src/templates/store/site/_/nav.wd +7 -6
  93. package/src/templates/store/site/pages/index.skin +7 -4
  94. package/src/templates/store/site/pages/index.wd +2 -2
  95. package/src/templates/blog/site/_/posts.json +0 -16
package/src/statics.js CHANGED
@@ -1,19 +1,67 @@
1
1
  import fs from "node:fs";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
- import { BASE_SECURITY_HEADERS, REACTIVE_CSP } from "./headers.js";
4
+ import { BASE_SECURITY_HEADERS, REACTIVE_CSP, STATIC_CSP } from "./headers.js";
5
5
 
6
6
  /**
7
- * Security headers for an HTML response from the local server. The relaxed
8
- * (reactive) CSP is a superset that satisfies both static and reactive pages,
9
- * so the runtime server uses it for every HTML response; the build-time
10
- * outputs (`vercel.json`, `dist/_headers`) tighten static routes per-path.
11
- * @type {Record<string, string>}
7
+ * The security headers (including the right CSP variant) for an HTML response.
8
+ * Mirrors what production ships: a route the build marked reactive gets the
9
+ * relaxed CSP (`'unsafe-eval'` for `new Function`); a static route gets the
10
+ * stricter, eval-free CSP — so a local preview matches the per-route `_headers`
11
+ * / `vercel.json` the same build writes instead of blanket-relaxing every page.
12
+ * Anything not found in `routes.json` (an unknown route, a 404, a missing
13
+ * manifest) falls back to the reactive CSP, exactly like the production
14
+ * `_headers` catch-all block.
15
+ * @param {string} distRoot Absolute path to the build output directory.
16
+ * @param {string} url Request URL (may include a query string).
17
+ * @returns {Record<string, string>}
12
18
  */
13
- const HTML_SECURITY_HEADERS = {
14
- ...BASE_SECURITY_HEADERS,
15
- "Content-Security-Policy": REACTIVE_CSP
16
- };
19
+ function htmlSecurityHeaders(distRoot, url) {
20
+ return {
21
+ ...BASE_SECURITY_HEADERS,
22
+ "Content-Security-Policy": routeIsReactive(distRoot, routeOf(url)) ? REACTIVE_CSP : STATIC_CSP
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Normalize a request URL to its clean, trailing-slashed route path so it can be
28
+ * matched against `routes.json` (`/docs` / `/docs/` / `/docs/index.html` all →
29
+ * `/docs/`). Returns null for an unparseable URL.
30
+ * @param {string} url
31
+ * @returns {string | null}
32
+ */
33
+ function routeOf(url) {
34
+ let pathname;
35
+ try {
36
+ pathname = decodeURIComponent((url || "/").split("?")[0]);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (pathname.endsWith("/index.html")) pathname = pathname.slice(0, -"index.html".length);
41
+ if (!pathname.endsWith("/")) pathname += "/";
42
+ return pathname;
43
+ }
44
+
45
+ /**
46
+ * Whether the route ships the reactive runtime, per the build's `routes.json`.
47
+ * Defaults to `true` (reactive) when the route is absent or the manifest can't be
48
+ * read — matching the production `_headers` catch-all, so an unknown route is
49
+ * never accidentally served the stricter policy.
50
+ * @param {string} distRoot
51
+ * @param {string | null} route
52
+ * @returns {boolean}
53
+ */
54
+ function routeIsReactive(distRoot, route) {
55
+ if (!route) return true;
56
+ try {
57
+ const manifest = JSON.parse(fs.readFileSync(path.join(distRoot, "routes.json"), "utf8"));
58
+ const entry = manifest.find((/** @type {{ route: string }} */ e) => e.route === route);
59
+ if (entry) return Boolean(entry.assets?.runtime);
60
+ } catch {
61
+ /* no/unreadable manifest → reactive default (the catch-all policy) */
62
+ }
63
+ return true;
64
+ }
17
65
 
18
66
  /**
19
67
  * Serve a built file for a request URL out of `distRoot`, or a 404 page.
@@ -25,7 +73,10 @@ const HTML_SECURITY_HEADERS = {
25
73
  export function serve(distRoot, url, res) {
26
74
  const file = resolvePublicFile(distRoot, url || "/");
27
75
  if (!file || !fs.existsSync(file)) {
28
- res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...HTML_SECURITY_HEADERS });
76
+ res.writeHead(404, {
77
+ "content-type": "text/html; charset=utf-8",
78
+ ...htmlSecurityHeaders(distRoot, url)
79
+ });
29
80
  const notFound = path.join(distRoot, "404.html");
30
81
  if (fs.existsSync(notFound)) {
31
82
  res.end(fs.readFileSync(notFound));
@@ -36,7 +87,7 @@ export function serve(distRoot, url, res) {
36
87
  }
37
88
  const type = contentType(file);
38
89
  const headers = type.startsWith("text/html")
39
- ? { "content-type": type, ...HTML_SECURITY_HEADERS }
90
+ ? { "content-type": type, ...htmlSecurityHeaders(distRoot, url) }
40
91
  : { "content-type": type };
41
92
  res.writeHead(200, headers);
42
93
  fs.createReadStream(file).pipe(res);
@@ -1,12 +1,21 @@
1
1
  # Blog — a Darkmown template
2
2
 
3
- A markdown-native blog. Posts are plain `.md` files in `site/pages/posts/`; the
4
- home page loops `site/_/posts.json` to list them. Static pages, zero JavaScript.
3
+ A markdown-native blog. Posts are plain `.md` files in `site/pages/posts/` —
4
+ that folder is a typed content collection, and the home page is one `@loop`
5
+ over it. No manifest, no config. Static pages, zero JavaScript.
5
6
 
6
7
  ## Add a post
7
8
 
8
- 1. Create `site/pages/posts/<slug>.md` with a `title` in frontmatter.
9
- 2. Add an entry to `site/_/posts.json` (`slug`, `url`, `title`, `date`, `excerpt`).
9
+ 1. Create `site/pages/posts/<slug>.md` with `title`, `date`, and `description`
10
+ in frontmatter (`excerpt` is optional). That's it — the home page lists it
11
+ automatically, newest first, and dated posts land in `rss.xml`.
12
+ 2. Optional: copy an existing post's `.skin` beside the new file — a colocated
13
+ `.skin` attaches automatically, keeping the reading design.
14
+
15
+ `site/pages/posts/_schema.wd` type-checks every post's frontmatter at build
16
+ time, so a typo'd or missing field fails the build with a file and line. When
17
+ the list grows, add `paginate 10` to the loop in `site/pages/index.wd` to split
18
+ it into `/page/2/`, `/page/3/`, … automatically.
10
19
 
11
20
  ## Commands
12
21
 
@@ -1,6 +1,7 @@
1
- <nav>
2
- <strong>My Blog</strong>
3
- <span class="links">
4
- <a href="/">Posts</a>
5
- </span>
6
- </nav>
1
+ ::: nav
2
+ **My Blog**
3
+
4
+ ::: links
5
+ [Posts](/)
6
+ :::
7
+ :::
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.6
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 720px
32
32
  margin 0 auto
33
33
  padding 2.5rem 1.5rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -40,10 +40,13 @@ nav
40
40
  margin 0 auto
41
41
  padding 1.25rem 1.5rem
42
42
 
43
- nav strong
43
+ .nav p
44
+ margin 0
45
+
46
+ .nav strong
44
47
  font-size 1.1rem
45
48
 
46
- nav a
49
+ .nav a
47
50
  color $accent
48
51
  text-decoration none
49
52
 
@@ -66,6 +69,9 @@ nav a
66
69
  color $muted
67
70
  font-size .9rem
68
71
 
72
+ .post-meta p
73
+ margin 0
74
+
69
75
  /* Buttons & form controls — for newsletter signups, :button actions, etc. */
70
76
  button
71
77
  display inline-flex
@@ -7,19 +7,24 @@ transitions: true
7
7
 
8
8
  @include /nav.wd
9
9
 
10
- <main>
10
+ ::: main
11
11
 
12
12
  # My Blog
13
13
 
14
- Posts are plain Markdown files in `site/pages/posts/`. This index is one `.wd`
15
- loop over a small JSON manifest — sorted newest-first, shipping zero JavaScript.
14
+ Posts are plain Markdown files in `site/pages/posts/` — that folder is a typed
15
+ content collection, and this index is one `.wd` loop over it. No manifest, no
16
+ config, zero JavaScript; each post's frontmatter is the data.
16
17
 
17
- @loop /posts.json into post sort by post.date desc
18
+ @loop posts into post sort by post.date desc
18
19
  ::: post-card
19
20
  ### [{ post.title }]({ post.url })
20
- <p class="post-meta">{ post.date }</p>
21
+ ::: post-meta
22
+ { post.date }
23
+ :::
21
24
  { post.excerpt }
22
25
  :::
26
+ @empty
27
+ No posts yet — add a `.md` file to `site/pages/posts/`.
23
28
  @endloop
24
29
 
25
- </main>
30
+ :::
@@ -0,0 +1,16 @@
1
+ ---
2
+ # Typed schema for the posts collection. Every .md file next to this file is
3
+ # validated against these rules at build time — a missing required field, a
4
+ # wrong type, or an unknown extra field fails the build with a file:line.
5
+ # Append "?" to a type to make a field optional. Page-shell flags a post sets
6
+ # (like transitions) are declared here too. Remove this file to turn
7
+ # validation off (it is opt-in).
8
+ title: string
9
+ date: date
10
+ description: string
11
+ excerpt: string?
12
+ transitions: boolean?
13
+ ---
14
+
15
+ The posts collection schema. Posts are plain `.md` files next to this file;
16
+ their frontmatter must match the rules above.
@@ -6,6 +6,8 @@ excerpt: I rewrote my blog as plain Markdown files — no React, no build ceremo
6
6
  transitions: true
7
7
  ---
8
8
 
9
+ [← All posts](/)
10
+
9
11
  # Hello, Darkmown
10
12
 
11
13
  This post is a plain `.md` file — strict CommonMark, **zero** framework
@@ -0,0 +1,59 @@
1
+ /* Colocated post styling — a .skin next to a .md attaches automatically.
2
+ Copy this file beside each new post to keep the reading design. */
3
+
4
+ tokens
5
+ ink #1d2530
6
+ paper #fbfaf7
7
+ panel #ffffff
8
+ line #e7e3da
9
+ muted #6b7785
10
+ accent #2f6df0
11
+
12
+ tokens dark
13
+ ink #e7ecf3
14
+ paper #0f141b
15
+ panel #161d27
16
+ line #28303c
17
+ muted #93a0b2
18
+ accent #5b8cff
19
+
20
+ html
21
+ bg $paper
22
+
23
+ page
24
+ max-width 680px
25
+ margin 0 auto
26
+ padding 2.5rem 1.5rem 4rem
27
+ font ui-serif, Georgia, Cambria, serif
28
+ color $ink
29
+ bg $paper
30
+ line-height 1.7
31
+ -webkit-font-smoothing antialiased
32
+
33
+ h1
34
+ font-size 2.1rem
35
+ line-height 1.2
36
+ margin 1rem 0 1.25rem
37
+
38
+ a
39
+ color $accent
40
+
41
+ code
42
+ font ui-monospace, SFMono-Regular, Menlo, monospace
43
+ font-size .88em
44
+ bg $panel
45
+ border 1px solid $line
46
+ padding .08rem .28rem
47
+ radius 5px
48
+
49
+ pre code
50
+ display block
51
+ padding .9rem 1.1rem
52
+ overflow-x auto
53
+
54
+ blockquote
55
+ margin 1rem 0
56
+ padding .5rem 1.2rem
57
+ border-left 3px solid $accent
58
+ bg $panel
59
+ color $muted
@@ -6,17 +6,22 @@ excerpt: Every post is a plain .md file; the index is one .wd loop. The date in
6
6
  transitions: true
7
7
  ---
8
8
 
9
+ [← All posts](/)
10
+
9
11
  # Markdown-native, by default
10
12
 
11
- To add a post, drop a Markdown file in `site/pages/posts/` and add a line to
12
- `site/_/posts.json`. The home page loops that manifest:
13
+ To add a post, drop a Markdown file in `site/pages/posts/`. That folder is a
14
+ typed content collection, and the home page loops it directly:
13
15
 
14
16
  ```wd
15
- @loop /posts.json into post sort by post.date desc
17
+ @loop posts into post sort by post.date desc
16
18
  ### [{ post.title }]({ post.url })
17
19
  @endloop
18
20
  ```
19
21
 
20
- No build config, no plugins, no content layer. The file *is* the page.
22
+ No manifest, no build config, no content layer. The file *is* the page, and its
23
+ frontmatter is the data — `site/pages/posts/_schema.wd` type-checks every
24
+ post's `title`, `date`, and `description` at build time, so a typo fails the
25
+ build with a file and line instead of shipping a broken list.
21
26
 
22
27
  [← Back to all posts](/)
@@ -0,0 +1,59 @@
1
+ /* Colocated post styling — a .skin next to a .md attaches automatically.
2
+ Copy this file beside each new post to keep the reading design. */
3
+
4
+ tokens
5
+ ink #1d2530
6
+ paper #fbfaf7
7
+ panel #ffffff
8
+ line #e7e3da
9
+ muted #6b7785
10
+ accent #2f6df0
11
+
12
+ tokens dark
13
+ ink #e7ecf3
14
+ paper #0f141b
15
+ panel #161d27
16
+ line #28303c
17
+ muted #93a0b2
18
+ accent #5b8cff
19
+
20
+ html
21
+ bg $paper
22
+
23
+ page
24
+ max-width 680px
25
+ margin 0 auto
26
+ padding 2.5rem 1.5rem 4rem
27
+ font ui-serif, Georgia, Cambria, serif
28
+ color $ink
29
+ bg $paper
30
+ line-height 1.7
31
+ -webkit-font-smoothing antialiased
32
+
33
+ h1
34
+ font-size 2.1rem
35
+ line-height 1.2
36
+ margin 1rem 0 1.25rem
37
+
38
+ a
39
+ color $accent
40
+
41
+ code
42
+ font ui-monospace, SFMono-Regular, Menlo, monospace
43
+ font-size .88em
44
+ bg $panel
45
+ border 1px solid $line
46
+ padding .08rem .28rem
47
+ radius 5px
48
+
49
+ pre code
50
+ display block
51
+ padding .9rem 1.1rem
52
+ overflow-x auto
53
+
54
+ blockquote
55
+ margin 1rem 0
56
+ padding .5rem 1.2rem
57
+ border-left 3px solid $accent
58
+ bg $panel
59
+ color $muted
@@ -1,6 +1,7 @@
1
- <nav>
2
- <strong>Dashboard</strong>
3
- <span class="links">
4
- <a href="/">Overview</a>
5
- </span>
6
- </nav>
1
+ ::: nav
2
+ **Dashboard**
3
+
4
+ ::: links
5
+ [Overview](/)
6
+ :::
7
+ :::
@@ -18,12 +18,12 @@ page
18
18
  line-height 1.55
19
19
  -webkit-font-smoothing antialiased
20
20
 
21
- main
21
+ .main
22
22
  max-width 880px
23
23
  margin 0 auto
24
24
  padding 2rem 1.5rem
25
25
 
26
- nav
26
+ .nav
27
27
  display flex
28
28
  align-items center
29
29
  justify-content space-between
@@ -31,10 +31,13 @@ nav
31
31
  margin 0 auto
32
32
  padding 1.25rem 1.5rem
33
33
 
34
- nav strong
34
+ .nav p
35
+ margin 0
36
+
37
+ .nav strong
35
38
  font-size 1.1rem
36
39
 
37
- nav a
40
+ .nav a
38
41
  color $accent
39
42
  text-decoration none
40
43
 
@@ -6,7 +6,7 @@ transitions: true
6
6
 
7
7
  @include /nav.wd
8
8
 
9
- <main>
9
+ ::: main
10
10
 
11
11
  ::: section #dash
12
12
 
@@ -72,4 +72,4 @@ The numbers come from `api/metrics.js` — a serverless function that runs in
72
72
 
73
73
  :::
74
74
 
75
- </main>
75
+ :::
@@ -1,6 +1,7 @@
1
- <nav>
2
- <strong>Acme</strong>
3
- <span class="links">
4
- <a href="#start">Get started</a>
5
- </span>
6
- </nav>
1
+ ::: nav
2
+ **Acme**
3
+
4
+ ::: links
5
+ [Get started](#start)
6
+ :::
7
+ :::
@@ -27,12 +27,12 @@ page
27
27
  line-height 1.6
28
28
  -webkit-font-smoothing antialiased
29
29
 
30
- main
30
+ .main
31
31
  max-width 900px
32
32
  margin 0 auto
33
33
  padding 1rem 1.5rem 4rem
34
34
 
35
- nav
35
+ .nav
36
36
  display flex
37
37
  align-items center
38
38
  justify-content space-between
@@ -40,10 +40,13 @@ nav
40
40
  margin 0 auto
41
41
  padding 1.25rem 1.5rem
42
42
 
43
- nav strong
43
+ .nav p
44
+ margin 0
45
+
46
+ .nav strong
44
47
  font-size 1.15rem
45
48
 
46
- nav a
49
+ .nav a
47
50
  color $accent
48
51
  text-decoration none
49
52
 
@@ -60,6 +63,9 @@ nav a
60
63
  color $muted
61
64
  margin 0 0 1.5rem
62
65
 
66
+ .lede p
67
+ margin 0
68
+
63
69
  .cta
64
70
  display inline-flex
65
71
  align-items center
@@ -186,6 +192,9 @@ input:focus-visible, textarea:focus-visible, select:focus-visible
186
192
  color $muted
187
193
  font-size .95rem
188
194
 
195
+ .quote .who p
196
+ margin 0
197
+
189
198
  .wd-carousel-prev
190
199
  position absolute
191
200
  left -.5rem
@@ -6,12 +6,16 @@ transitions: true
6
6
 
7
7
  @include /nav.wd
8
8
 
9
- <main>
9
+ ::: main
10
10
 
11
11
  ::: hero
12
12
  # Ship faster with *Acme*
13
- <p class="lede">The all-in-one toolkit your team actually enjoys using.</p>
14
- <a class="cta" href="#start">Get started</a>
13
+
14
+ ::: lede
15
+ The all-in-one toolkit your team actually enjoys using.
16
+ :::
17
+
18
+ [Get started](#start){.cta}
15
19
  :::
16
20
 
17
21
  ::: features
@@ -34,21 +38,31 @@ One command to Vercel or Cloudflare — your API comes along for free.
34
38
  :carousel
35
39
  ::: quote
36
40
  > "Acme cut our launch time in half."
37
- <p class="who">— Riley, Head of Product</p>
41
+
42
+ ::: who
43
+ — Riley, Head of Product
44
+ :::
38
45
  :::
39
46
  ::: quote
40
47
  > "The simplest stack we've ever shipped on."
41
- <p class="who">— Sam, CTO</p>
48
+
49
+ ::: who
50
+ — Sam, CTO
51
+ :::
42
52
  :::
43
53
  ::: quote
44
54
  > "Our marketing pages load instantly now."
45
- <p class="who">— Jordan, Growth</p>
55
+
56
+ ::: who
57
+ — Jordan, Growth
58
+ :::
46
59
  :::
47
60
  :endcarousel
48
61
 
49
62
  ::: cta-band #start
50
63
  ## Ready to build?
51
- <a class="cta" href="/">Start now</a>
64
+
65
+ [Start now](/){.cta}
52
66
  :::
53
67
 
54
- </main>
68
+ :::
@@ -1,7 +1,8 @@
1
- <nav>
2
- <strong>My site</strong>
3
- <span class="links">
4
- <a href="/">Home</a>
5
- <a href="/about/">About</a>
6
- </span>
7
- </nav>
1
+ ::: nav
2
+ **My site**
3
+
4
+ ::: links
5
+ [Home](/)
6
+ [About](/about/)
7
+ :::
8
+ :::
@@ -4,7 +4,13 @@ title: About
4
4
 
5
5
  # About
6
6
 
7
- This page is plain Markdown (`.md`) — strict CommonMark, zero framework JavaScript.
8
- Rename it to `.wd` when you want directives like state, loops, or forms.
7
+ This page is plain Markdown (`.md`) — strict CommonMark, zero framework
8
+ JavaScript, and no raw HTML (Darkmown escapes it by default, so untrusted
9
+ content can't inject markup). That's why there's no shared nav bar here: a
10
+ `.md` page can't `@include` or use directives.
11
+
12
+ Rename it to `.wd` when you want that — state, loops, forms, and an
13
+ `@include /nav.wd` for the shared nav. The colocated `about.skin` styles this
14
+ page either way; it attaches automatically by basename, `.md` or `.wd` alike.
9
15
 
10
16
  [Back home](/)
@@ -0,0 +1,44 @@
1
+ /* Colocated page styling — a .skin next to a .md attaches automatically. */
2
+
3
+ tokens
4
+ ink #172026
5
+ muted #5b6b72
6
+ paper #f7f2e8
7
+ panel #ffffff
8
+ line #e4ddcd
9
+ accent #0f8b8d
10
+
11
+ tokens dark
12
+ ink #e8eef0
13
+ muted #93a3a8
14
+ paper #11181b
15
+ panel #182226
16
+ line #2a363a
17
+ accent #3fc6c8
18
+
19
+ html
20
+ bg $paper
21
+
22
+ page
23
+ margin 0
24
+ font ui-sans-serif, system-ui, sans-serif
25
+ color $ink
26
+ bg $paper
27
+ line-height 1.6
28
+ -webkit-font-smoothing antialiased
29
+
30
+ main
31
+ max-width 760px
32
+ margin 0 auto
33
+ padding 3rem 1.5rem
34
+
35
+ a
36
+ color $accent
37
+
38
+ code
39
+ font ui-monospace, SFMono-Regular, Menlo, monospace
40
+ font-size .88em
41
+ bg $panel
42
+ border 1px solid $line
43
+ padding .08rem .28rem
44
+ radius 5px