hd-garden 0.7.0 → 0.8.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.
Files changed (110) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +26 -19
  2. package/README.en.md +57 -0
  3. package/README.md +31 -46
  4. package/app/blog/[...path]/page.tsx +9 -1
  5. package/app/columns/[column]/[...path]/page.tsx +10 -12
  6. package/app/components/ArticlePager.tsx +32 -0
  7. package/app/components/Comments.tsx +2 -1
  8. package/app/components/FeedMore.tsx +70 -0
  9. package/app/components/GardenDocsNav.tsx +25 -20
  10. package/app/components/HandbookLangRedirect.tsx +30 -0
  11. package/app/components/HandbookLangSwitch.tsx +49 -0
  12. package/app/components/HandbookView.tsx +187 -0
  13. package/app/components/MarkdownArticle.tsx +11 -5
  14. package/app/components/MarkdownImage.tsx +6 -4
  15. package/app/components/SiteChrome.tsx +11 -2
  16. package/app/components/SiteFooter.tsx +5 -3
  17. package/app/components/SiteHeader.tsx +27 -23
  18. package/app/components/TableOfContents.tsx +15 -9
  19. package/app/components/ThoughtFeed.tsx +41 -0
  20. package/app/components/UiLang.tsx +18 -0
  21. package/app/components/Webmentions.tsx +3 -2
  22. package/app/components/garden/FilePreview.tsx +13 -11
  23. package/app/components/garden/GardenBlock.tsx +3 -1
  24. package/app/components/garden/GardenFile.tsx +9 -7
  25. package/app/components/garden/blocks.tsx +34 -24
  26. package/app/components/garden/chrome.tsx +20 -13
  27. package/app/garden/[...slug]/page.tsx +10 -130
  28. package/app/garden/en/[...slug]/page.tsx +28 -0
  29. package/app/garden/en/page.tsx +16 -0
  30. package/app/garden/page.tsx +7 -74
  31. package/app/garden-modules.d.ts +3 -0
  32. package/app/layout.tsx +2 -4
  33. package/app/lib/content.ts +12 -2
  34. package/app/lib/garden-docs-meta.ts +60 -5
  35. package/app/lib/handbook-path.ts +11 -0
  36. package/app/lib/i18n.ts +222 -0
  37. package/app/lib/page-alternates.ts +2 -2
  38. package/app/not-found.tsx +5 -3
  39. package/app/site.config.ts +1 -1
  40. package/app/styles/a2.css +101 -32
  41. package/app/styles/shell.css +6 -0
  42. package/app/thoughts/page/[n]/page.tsx +44 -0
  43. package/app/thoughts/page.tsx +2 -6
  44. package/bin/garden.mjs +4 -0
  45. package/docs/handbook/ai-writing.md +99 -0
  46. package/docs/handbook/components/card.md +2 -0
  47. package/docs/handbook/components/chart.md +2 -0
  48. package/docs/handbook/components/compare.md +10 -12
  49. package/docs/handbook/components/figure.md +2 -0
  50. package/docs/handbook/components/file.md +2 -0
  51. package/docs/handbook/components/html.md +2 -0
  52. package/docs/handbook/components/interactive.md +2 -0
  53. package/docs/handbook/components/stat.md +2 -0
  54. package/docs/handbook/components/timeline.md +2 -0
  55. package/docs/handbook/components.md +1 -1
  56. package/docs/handbook/config.md +2 -2
  57. package/docs/handbook/deploy.md +1 -1
  58. package/docs/handbook/design.md +3 -3
  59. package/docs/handbook/en/ai-writing.md +99 -0
  60. package/docs/handbook/en/components/card.md +46 -0
  61. package/docs/handbook/en/components/chart.md +54 -0
  62. package/docs/handbook/en/components/compare.md +38 -0
  63. package/docs/handbook/en/components/figure.md +72 -0
  64. package/docs/handbook/en/components/file/checklist.md +17 -0
  65. package/docs/handbook/en/components/file.md +34 -0
  66. package/docs/handbook/en/components/html.md +69 -0
  67. package/docs/handbook/en/components/interactive.md +67 -0
  68. package/docs/handbook/en/components/stat.md +57 -0
  69. package/docs/handbook/en/components/timeline.md +52 -0
  70. package/docs/handbook/en/components.md +30 -0
  71. package/docs/handbook/en/config.md +125 -0
  72. package/docs/handbook/en/deploy.md +273 -0
  73. package/docs/handbook/en/design.md +81 -0
  74. package/docs/handbook/en/index.md +192 -0
  75. package/docs/handbook/en/start.md +92 -0
  76. package/docs/handbook/en/workflow.md +81 -0
  77. package/docs/handbook/en/writing.md +156 -0
  78. package/docs/handbook/index.md +98 -3
  79. package/docs/handbook/writing.md +18 -10
  80. package/package.json +1 -1
  81. package/scripts/build-content.mjs +23 -3
  82. package/scripts/handbook-facts.mjs +34 -0
  83. package/scripts/html-lang.mjs +36 -0
  84. package/scripts/static-404.mjs +25 -0
  85. package/scripts/ui-cli.mjs +14 -8
  86. package/templates/site/site.config.yaml +2 -0
  87. package/ui/core/apply.mjs +22 -4
  88. package/ui/core/cli-locale.mjs +67 -0
  89. package/ui/core/fence.mjs +18 -3
  90. package/ui/core/fix-prompt.mjs +18 -15
  91. package/ui/core/garden-fence.mjs +64 -0
  92. package/ui/core/scan.mjs +19 -3
  93. package/ui/garden/adjacent.mjs +21 -0
  94. package/ui/garden/check.mjs +6 -0
  95. package/ui/garden/html-lang.mjs +16 -0
  96. package/ui/garden/paging.mjs +30 -0
  97. package/ui/preview/dist/garden-ui.css +9 -9
  98. package/ui/preview/dist/garden-ui.esm.js +169 -169
  99. package/ui/preview/dist/garden-ui.iife.js +169 -169
  100. package/ui/react/components/chart.tsx +7 -4
  101. package/ui/react/components/display.tsx +6 -3
  102. package/ui/react/components/inputs.tsx +6 -3
  103. package/ui/react/components/layout.tsx +6 -4
  104. package/ui/react/components/media.tsx +7 -4
  105. package/ui/react/components/sandbox.tsx +3 -1
  106. package/ui/react/index.tsx +11 -7
  107. package/ui/react/lang.tsx +85 -0
  108. package/ui/react/runtime.tsx +3 -1
  109. package/ui/react/ui.css +9 -9
  110. package/worker/index.ts +8 -1
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Checks & Shortcuts
3
+ navTitle: Checks & Shortcuts
4
+ description: Validate posts and diagnostic codes with npx garden check, install the Git pre-commit hook, and use editor snippets and Agent skills.
5
+ section: workflow
6
+ order: 40
7
+ ---
8
+
9
+ To catch YAML indentation mistakes, missing required fields, or broken local file paths before publishing, Garden ships a CLI validator, a Git pre-commit hook, snippet templates, and bundled Agent skills.
10
+
11
+ ## 1. Validation Commands
12
+
13
+ Three commands check your content at different scopes:
14
+
15
+ - **Check one or more posts (`npx garden check`)**:
16
+ ```bash
17
+ npx garden check content/posts/my-post.md
18
+ # Or pass multiple Markdown files at once
19
+ npx garden check content/posts/*.md
20
+ ```
21
+ - **Check interactive blocks and component syntax (`npx garden ui check`)**:
22
+ ```bash
23
+ npx garden ui check content/posts/my-post.md
24
+ # Check staged Markdown files in Git
25
+ npx garden ui check --staged
26
+ # Check all Markdown files in the repository
27
+ npx garden ui check --all
28
+ ```
29
+ - **Sync and build the entire site (`make check`)**:
30
+ ```bash
31
+ make check
32
+ ```
33
+ In the site `Makefile`, `make check` runs `npx garden sync && npx garden build` to execute full content synchronization, component validation, and the production build.
34
+
35
+ ## 2. `npx garden check` Diagnostic Codes
36
+
37
+ `npx garden check` (backed by `scripts/check-post.mjs`, `ui/garden/check.mjs`, and `ui/garden/file-paths.mjs`) reports exact line numbers, diagnostic codes, and fix suggestions:
38
+
39
+ | Code | Severity | Cause and Fix |
40
+ | :--- | :--- | :--- |
41
+ | `missing-title` / `missing-description` | Error | Front Matter is missing a non-empty `title` or `description` |
42
+ | `unexpected-slug` | Error | Front Matter includes a manual `slug` field (remove `slug`; the filename is the slug) |
43
+ | `date-format` | Error | `date` is not a valid `YYYY-MM-DD`, `YYYY-MM-DD HH:mm:ss`, or ISO timestamp with timezone |
44
+ | `missing-cover` / `missing-asset` | Error | A local file referenced by `cover`, a Markdown image, or `garden file` does not exist |
45
+ | `heading-h1` / `heading-skip` | Error | The post body uses an `#` H1 heading or skips a heading level (such as jumping from `##` to `####`) |
46
+ | `reference-heading` | Error | The post body contains a references section heading (move references to the `references` Front Matter list) |
47
+ | `callout-type` | Error | `> [!TYPE]` uses an unsupported callout type |
48
+ | `title-split` | Warning | The title contains a split delimiter with an empty left or right side |
49
+ | `diff-orphan` | Error | A `diff` code fence is not immediately preceded by a complete code block |
50
+ | `legacy-fence` | Error | Uses a legacy fence name (such as `garden:stat`, `widget`, `embed`, or `html garden`); replace with `garden <name>` |
51
+ | `garden-idea` | Error | A non-draft post still contains an uncompiled `garden` idea fence |
52
+ | `garden-name` | Error | Unknown `garden` component name (must be `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `interactive`, or `file`) |
53
+ | `garden-yaml` | Error | Invalid YAML syntax, wrong top-level shape, or unsupported keys inside a `garden` fence |
54
+ | `garden-file` / `file-outside` | Error | `garden file` manually sets `resolved`, is missing a valid `src` / `files`, or points outside `content/` |
55
+ | `file-large` | Warning | A local file referenced by `garden file` exceeds `5 MB` |
56
+ | `compare-long` | Error | A single cell in `garden compare` exceeds roughly 60 characters |
57
+ | `interactive-trivial` / `steps-prose` | Error | `garden interactive` only computes a single multiplication, or `<Steps>` contains only prose without state changes |
58
+
59
+ ## 3. Git Pre-commit Hook and Component Catalog
60
+
61
+ - **Install the pre-commit hook**: Run `npx garden hooks` to write `.git/hooks/pre-commit`, which runs `npx --no-install garden ui check --staged --quiet` on every `git commit`:
62
+ ```bash
63
+ npx garden hooks
64
+ ```
65
+ - **Generate the local component catalog**: Run `npx garden ui catalog` to write a static preview page of all built-in components to `.garden/ui-catalog.html`:
66
+ ```bash
67
+ npx garden ui catalog
68
+ ```
69
+
70
+ ## 4. Component Templates, Snippets, and Agent Skills
71
+
72
+ Garden provides three ways to insert or generate components quickly:
73
+
74
+ 1. **Draft cheat sheet comment**: Running `make new SLUG=xxx TITLE="..."` appends an HTML comment to the bottom of the new `.md` file with examples for idea fences and all 9 components. HTML comments are hidden from the rendered page and can be deleted when finished.
75
+ 2. **Snippet template files**: The `templates/` directory inside `hd-garden` includes two template files:
76
+ - `templates/garden-blocks.md`: Copyable Markdown templates for idea fences and all 9 built-in components.
77
+ - `templates/garden-snippets.plist`: macOS text replacements (drag into **System Settings → Keyboard → Text Replacements**) with triggers `;gidea`, `;gcard`, `;gstat`, `;gcompare`, `;gtimeline`, `;gchart`, `;gfigure`, `;ghtml`, `;ginteractive`, and `;gfile`.
78
+ 3. **Install AI Agent skills (`npx garden skill`)**: Running `npx garden skill` in a site repository (also run automatically during `npx hd-garden init`) copies three skills into `.agents/skills/`:
79
+ - `garden-ui`: Compiles natural-language `garden` idea fences into concrete component blocks.
80
+ - `garden-interactive-chart`: Authors `interactive`, `chart`, `figure`, and `html` components from article data.
81
+ - `garden-post-review`: Reviews an entire post against layout and component rules.
@@ -0,0 +1,156 @@
1
+ ---
2
+ title: Writing Posts & Thoughts
3
+ navTitle: Writing Posts & Thoughts
4
+ description: Front Matter fields for posts and thoughts, code diff toggles, callouts, link cards, math formulas, and Mermaid diagrams.
5
+ section: writing-and-reading
6
+ order: 10
7
+ ---
8
+
9
+ Garden organizes content into **posts** (`content/posts/`) and **thoughts** (`content/thoughts/`). Both are UTF-8 Markdown files whose filenames contain only lowercase letters, digits, and hyphens (`a-z`, `0-9`, `-`).
10
+
11
+ ## 1. Post Front Matter (`content/posts/*.md`)
12
+
13
+ Create a post with `make new DIR=systems SLUG=wal-checkpoint TITLE="Understanding WAL | Checkpoints and Recovery"` or create a `.md` file directly:
14
+
15
+ ```yaml
16
+ ---
17
+ title: Understanding WAL | Checkpoints and Recovery
18
+ date: 2026-10-18
19
+ updated: 2026-10-19
20
+ description: How write-ahead logging preserves snapshot visibility under concurrent reads and writes, and how background checkpoints bound log growth.
21
+ tags:
22
+ - Database
23
+ - Storage
24
+ pinned: false
25
+ draft: false
26
+ cover: windowsill-herbs.svg
27
+ original: https://example.com/original-post
28
+ references:
29
+ - title: SQLite Write-Ahead Logging
30
+ url: https://www.sqlite.org/wal.html
31
+ - title: Database Internals
32
+ ---
33
+ ```
34
+
35
+ | Field | Required | Type | Description |
36
+ | :--- | :--- | :--- | :--- |
37
+ | `title` | Yes | String | Post title. Write ` \| ` (with spaces on both sides) to split the heading into two lines and scale the font size to fit |
38
+ | `date` | Yes | Date string | Publication date (such as `2026-10-18` or an ISO timestamp with timezone) |
39
+ | `description` | Yes | String | Summary shown in the article deck, archive lists, and search engine descriptions |
40
+ | `updated` | No | Date string | Last updated date, shown in the article meta bar and used for RSS sorting |
41
+ | `publishAt` | No | Date string | Scheduled publish time; included only when earlier than the build time (or when `CONTENT_INCLUDE_DRAFTS=1`) |
42
+ | `tags` | No | String array | Tags for the post; clicking a tag filters `/blog?t=<tag>` |
43
+ | `pinned` | No | Boolean | Set to `true` to feature the post in the pinned section on the home page |
44
+ | `draft` | No | Boolean | Set to `true` to mark as a draft and exclude from production builds |
45
+ | `cover` | No | Path string | Cover image path relative to `content/covers/` |
46
+ | `original` | No | Full URL | External canonical source link, rendered in the article meta bar |
47
+ | `references` | No | `{ title, url? }[]` | Reference list rendered at the bottom of the post with automatically extracted domain names |
48
+
49
+ At the bottom of every post page, between the Webmention section and the comments, sit the Previous and Next navigation cards. Posts link only to other posts ordered by `date` (where Previous is the older post), while column entries follow their order in `content/columns.yaml`; the oldest and newest entries show only one card.
50
+
51
+ > [!CAUTION] Caution
52
+ > Do not write a `slug` field in Front Matter (`npx garden check` reports an `unexpected-slug` error because the filename is already the slug). Posts do not use a `topic` field either.
53
+
54
+ ## 2. Code Blocks and the Diff Toggle
55
+
56
+ Standard code blocks show their language label (such as `RUST`, `TS`, `PYTHON`, or `SHELL`) and a copy button in the top-right corner. To show how a snippet changed from an earlier version, place the updated code block first and follow it with a `garden` idea fence stating what changed in one sentence:
57
+
58
+ ````markdown
59
+ ```go
60
+ func openStore(path string) (*Store, error) {
61
+ return openWithMode(path, ModeWAL)
62
+ }
63
+ ```
64
+
65
+ ```garden
66
+ Compared with the previous version: open the store with ModeWAL instead of ModeJournal.
67
+ ```
68
+ ````
69
+
70
+ Following the `garden-ui` skill, the Agent runs `npx garden ui apply` to replace that idea fence with a `diff` fence right below the code block (see [Writing with AI](/garden/en/ai-writing)). The page combines the two adjacent blocks into a single code frame with tabs for the full code and the diff against the previous version, while the copy button always copies the new full code:
71
+
72
+ ```go
73
+ func openStore(path string) (*Store, error) {
74
+ return openWithMode(path, ModeWAL)
75
+ }
76
+ ```
77
+
78
+ ```diff
79
+ func openStore(path string) (*Store, error) {
80
+ - return openWithMode(path, ModeJournal)
81
+ + return openWithMode(path, ModeWAL)
82
+ }
83
+ ```
84
+
85
+ **Hand-written format**: Without an Agent, you can write the `diff` code block directly below the complete code block (separated only by a blank line):
86
+
87
+ ````markdown
88
+ ```diff
89
+ func openStore(path string) (*Store, error) {
90
+ - return openWithMode(path, ModeJournal)
91
+ + return openWithMode(path, ModeWAL)
92
+ }
93
+ ```
94
+ ````
95
+
96
+ > [!IMPORTANT] Important
97
+ > A `diff` fence must immediately follow a complete code block; a standalone `diff` fence with no preceding code block triggers a `diff-orphan` error in `npx garden check`.
98
+
99
+ ## 3. CJK Bold, Highlighter Strokes, and Callouts
100
+
101
+ - **CJK-friendly bold**: `remark-cjk-friendly` is enabled so `**bold next to CJK punctuation**` parses cleanly without extra spaces around the asterisks.
102
+ - **Highlighter stroke**: Wrap text in `<u>key phrase</u>` or `<mark>key phrase</mark>` to apply a warm yellow highlighter band, like <u>this highlighted phrase</u>.
103
+ - **Callouts**: Supports `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, and `> [!CAUTION]` (plus `INFO`, `SUCCESS`, `DANGER`, and `FAILURE`). Write a title on the same line after the marker to set a custom heading:
104
+
105
+ ```markdown
106
+ > [!WARNING] Back Up the Data Directory Before Upgrading
107
+ > Changing the page size requires rebuilding the index files and cannot be rolled back in place.
108
+ ```
109
+
110
+ > [!WARNING] Back Up the Data Directory Before Upgrading
111
+ > Changing the page size requires rebuilding the index files and cannot be rolled back in place.
112
+
113
+ ## 4. Local Images and External Link Cards
114
+
115
+ - **Local images and sidecar folders**: For a post at `content/posts/my-post.md`, place its images inside `content/posts/my-post/` and reference them with relative paths such as `./my-post/diagram.svg`. At build time, Garden copies the file, records its pixel dimensions, and renders the `alt` text (unless it starts with default placeholders like `image` or `img`) as a `<figcaption>`.
116
+ - **Inline external links**: External links in body text automatically display the target site's favicon and keep the icon attached to the first word of the link so it never wraps alone.
117
+ - **Link cards**: Turn an external URL into a preview card with site name, title, and description in either of two ways:
118
+ 1. Put the full URL on its own paragraph line (surrounded by blank lines): `https://spec.commonmark.org/`
119
+ 2. Append `{card}` directly to an inline link (with no space): `See the [CommonMark Spec](https://spec.commonmark.org/){card}.`
120
+ You can also run `make embed URL="https://example.com"` to fetch Open Graph metadata for a URL.
121
+
122
+ ## 5. Math Formulas and Mermaid Diagrams
123
+
124
+ - **LaTeX math**: Rendered at build and server-render time with KaTeX. Use `$L = \lambda W$` for inline math and `$$...$$` for display blocks. Multi-letter identifiers containing underscores (such as `group_num` or `max_batch`) are automatically wrapped as upright `\mathrm{group\_num}`, while single-letter subscripts like `W_q` or `H_{kv}` remain standard math subscripts.
125
+ - **Mermaid diagrams**: Use ```` ```mermaid ```` fences to draw flowcharts or sequence diagrams that adapt to light and dark themes:
126
+
127
+ ```mermaid
128
+ graph LR
129
+ A[Markdown Source] --> B[garden sync]
130
+ B --> C[Search & Static Assets]
131
+ C --> D[Rendered Page]
132
+ ```
133
+
134
+ ## 6. Writing Thoughts (`content/thoughts/*.md`)
135
+
136
+ Thoughts are short notes with optional image grids, audio/video files, or external link cards:
137
+
138
+ ```yaml
139
+ ---
140
+ title: Late-Night Debugging Notes
141
+ date: 2026-10-19 21:30
142
+ tags:
143
+ - Notes
144
+ media:
145
+ - type: image
146
+ src: ./rainy-night/terminal.png
147
+ alt: Benchmark output in the terminal
148
+ - type: link
149
+ src: https://sqlite.org/wal.html
150
+ title: SQLite Write-Ahead Logging
151
+ ---
152
+
153
+ A short note written in standard Markdown.
154
+ ```
155
+
156
+ The `media` array accepts four item types: `image` (multiple images automatically form a grid), `audio` (native `<audio>` player), `video` (native `<video>` player with optional `poster`), and `link` (rendered as an external link card supporting `src`, `title`, `description`, and `image`).
@@ -5,11 +5,106 @@ section: overview
5
5
  order: 0
6
6
  ---
7
7
 
8
- 长篇技术文章除了段落,常常还有架构图、多列对比和改过的代码。在 Garden 里这些都用普通 Markdown 写,排版交给构建流程和页面脚本。下面每一节都附带实际效果。
8
+ Garden 是一个 Claude 风格的博客框架,文章用 Markdown 写,组件用自然语言描述就行。
9
+
10
+ ## 一个完整的例子
11
+
12
+ 下面四步是同一篇稿子的真实过程,命令输出均来自实际运行结果。
13
+
14
+ ### 1. 作者写一句想法
15
+
16
+ 在想放交互的地方写一个不带名字的 `garden` 围栏,里面写清想让读者做什么、看到什么:
17
+
18
+ ````markdown
19
+ ## 写作节奏
20
+
21
+ ```garden
22
+ 让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
23
+ ```
24
+ ````
25
+
26
+ ### 2. Agent 扫描并写回组件
27
+
28
+ 对 Agent 说「编译这篇:content/posts/writing-pace.md」。Agent 按站点里的 `garden-ui` skill 先运行 `npx garden ui scan`,拿到这一块的行号、原话和哈希:
29
+
30
+ ```console
31
+ $ npx garden ui scan content/posts/writing-pace.md
32
+ {
33
+ "file": "content/posts/writing-pace.md",
34
+ "draft": false,
35
+ "blocks": [
36
+ {
37
+ "lang": "garden-idea",
38
+ "status": "uncompiled",
39
+ "startLine": 9,
40
+ "intentHash": "cb3a3569",
41
+ "intent": "让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。",
42
+ "section": "写作节奏"
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ 接着 Agent 读取 `npx garden ui prompt` 里的组件目录,写出 `garden interactive`,再用 `npx garden ui apply` 按 `--intent-hash` 只替换这一块,围栏外的正文逐字节不变:
49
+
50
+ ```console
51
+ $ npx garden ui apply content/posts/writing-pace.md --intent-hash cb3a3569 --file new.md
52
+ {"ok":true,"file":"content/posts/writing-pace.md","kind":"garden","line":9}
53
+ ```
54
+
55
+ ### 3. 提交前钩子检查并生成修复提示词
56
+
57
+ 首次运行 `garden sync` 或 `garden dev` 时,Garden 会在 `.git/hooks/pre-commit` 写入 `exec npx --no-install garden ui check --staged --quiet`。如果作者在第 2 步之前直接提交,钩子会拦下提交,并打印一段可以直接粘贴给 AI 的修复提示词:
58
+
59
+ ```console
60
+ $ git commit -m "写作节奏"
61
+ content/posts/writing-pace.md:9 - garden-idea 还有没编译的想法
62
+
63
+ ──────────────────────── 发给 AI 的修复提示词 ────────────────────────
64
+ 请处理下面 Garden 博文里列出的块:没编译的想法要编译,有错误的交互块要修好。
65
+
66
+ 要求:
67
+ - 先运行 npx garden ui prompt,读组件目录和规则;仓库里有 garden-ui skill 时按它的流程做。
68
+ - 只改列出的块,用 npx garden ui apply <稿件> --id <id> 写回(没有 id 的想法用 --intent-hash),不要直接编辑稿件。
69
+ - 不改作者原话的意思。改完运行 npx garden ui check <稿件>,直到通过。
70
+ - 如果你不能访问这个仓库,就只回复每块修好后的完整围栏。
71
+
72
+ 1. content/posts/writing-pace.md,第 9–11 行,intent-hash: cb3a3569
73
+ - garden-idea:这段想法还没编译,请按原话编译成 garden interactive 或其他 garden 组件(或判断后写回正文)。
74
+ (后面附当前围栏原文)
75
+ ────────────────────────────────────────────────────────────
76
+ 已写入 .garden/ui-fix-prompt.md。确实要先提交,可以用 git commit --no-verify。
77
+ ```
78
+
79
+ 这段提示词同时写入 `.garden/ui-fix-prompt.md`,有剪贴板命令时自动复制。把提示词贴给 Agent 完成转换后再提交,钩子静默通过(退出码 `0`)。
80
+
81
+ ### 4. 页面渲染结果
82
+
83
+ 第 2 步写回的 `garden interactive` 块在文章页面直接渲染为可拖动的交互控件:
84
+
85
+ ```garden interactive #writing-pace
86
+ caption: 每天多写几百字,比每周多写一天更快。
87
+ intent: |
88
+ 让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
89
+ ---
90
+ <State daily={500} days={3} />
91
+ <Let weekly={daily * days} />
92
+ <Let weeks={ceil(8000 / weekly)} />
93
+
94
+ <Slider bind="daily" label="每天写" min={200} max={2000} step={100} unit="字" />
95
+ <Slider bind="days" label="每周写" min={1} max={7} step={1} unit="天" />
96
+
97
+ <Stat label="写完 8000 字需要" value={weeks} unit="周" hint="不足一周按一周算" />
98
+ ```
99
+
100
+ > [!IMPORTANT] 人和 Agent 用同一套工具
101
+ > Agent 找块用的 `npx garden ui scan`、写回用的 `npx garden ui apply`、自查用的 `npx garden ui check`,和作者提交时 pre-commit 钩子运行的检查是同一组命令。钩子拦截时打印的提示词,就是发给 Agent 的下一步指令。详见 [用 AI 写作](/garden/ai-writing)。
102
+
103
+ 下面几节介绍不用 Agent 也会自动生效的版面与组件规则。
9
104
 
10
105
  ## 标题分行与 `740px` 正文列
11
106
 
12
- 上方的标题在 `丨` 处分成两行,字号按较长一行自动计算。正文保持在 `740px` 阅读列内,宽图会在两侧留白里对称外扩,不会盖住导航和目录。
107
+ 标题里写一个「丨」,就在那里分成两行,字号按较长一行自动计算。正文保持在 `740px` 阅读列内,宽图会在两侧留白里对称外扩,不会盖住导航和目录。
13
108
 
14
109
  下面这张透明底的线稿在深色模式下会自动垫上暖白底,不用另做一套配图:
15
110
 
@@ -27,7 +122,7 @@ order: 0
27
122
 
28
123
  ## 代码块与「对比上一版」切换
29
124
 
30
- 把改好的完整代码块和一段 `diff` 紧挨着写,页面会把它们合成一个可以切换「代码」和「对比上一版」的代码框。复制按钮复制的总是新版完整代码。
125
+ 在改好的完整代码下面写一句想法说明和上一版比改了什么,Agent 会生成一段紧挨着的 `diff` 代码块;你也可以直接手写 `diff`。完整代码和 `diff` 紧挨着时,页面会把它们合成一个可切换「代码」和「对比上一版」的代码框,复制按钮始终复制新版完整代码。
31
126
 
32
127
  ```yaml
33
128
  pages:
@@ -46,14 +46,14 @@ references:
46
46
  | `original` | 否 | 完整链接 | 原文链接,设置后在标题下方元信息栏显示 `原文 ↗` |
47
47
  | `references` | 否 | `{ title, url? }[]` | 文末参考资料列表,构建期自动提取域名并渲染在正文底部的「参考」卡片中 |
48
48
 
49
+ 文章页底部,在「来自开放网络的回应」和评论之间,有「上一篇」「下一篇」两个按钮。长文只和长文排,按 `date` 先后,上一篇是更早的那篇;专栏文章按专栏里的顺序排;最早或最新的一篇只显示一侧。
50
+
49
51
  > [!CAUTION]
50
52
  > 不要在 Front Matter 里手写 `slug` 字段(`npx garden check` 会报 `unexpected-slug` 错误:`不要写 slug,文件名就是标识`)。文章也没有 `topic` 分类字段。
51
53
 
52
54
  ## 2. 代码块与「代码 / 对比上一版」切换
53
55
 
54
- 普通代码块在右上角带有语言名称(如 `RUST`、`TS`、`PYTHON`、`SHELL`)和复制按钮。
55
-
56
- 如果你想展示某段代码修改前后的对比,**先放一段完整代码块,紧接着放一段 `diff` 代码块**。渲染器会自动把它们合并成一个带 **「代码」** 和 **「对比上一版」** 两个切换按钮的代码框:
56
+ 普通代码块在右上角带有语言名称(如 `RUST`、`TS`、`PYTHON`、`SHELL`)和复制按钮。想展示一段代码和上一版相比改了什么,先放改好的完整代码,再紧跟着写一个 `garden` 想法块,用一句话写出改动内容:
57
57
 
58
58
  ````markdown
59
59
  ```go
@@ -62,15 +62,12 @@ func openStore(path string) (*Store, error) {
62
62
  }
63
63
  ```
64
64
 
65
- ```diff
66
- func openStore(path string) (*Store, error) {
67
- - return openWithMode(path, ModeJournal)
68
- + return openWithMode(path, ModeWAL)
69
- }
65
+ ```garden
66
+ 和上一版比:打开存储时的模式从 ModeJournal 改成了 ModeWAL。
70
67
  ```
71
68
  ````
72
69
 
73
- 实际渲染效果如下(点击右上角的 **「代码」** 与 **「对比上一版」** 可以切换,点击复制按钮始终复制前面的完整代码):
70
+ Agent 按 `garden-ui` skill 会通过 `npx garden ui apply` 把这段想法替换成紧挨着完整代码的 `diff` 围栏(详见 [用 AI 写作](/garden/ai-writing))。页面渲染时会把这两段合成一个可切换 **「代码」** 和 **「对比上一版」** 的代码框,右上角的复制按钮始终复制新版完整代码:
74
71
 
75
72
  ```go
76
73
  func openStore(path string) (*Store, error) {
@@ -85,8 +82,19 @@ func openStore(path string) (*Store, error) {
85
82
  }
86
83
  ```
87
84
 
85
+ **手写格式**:如果不使用 Agent,也可以直接在完整代码块正下方手写一段 `diff` 代码块(两段之间只隔空行):
86
+
87
+ ````markdown
88
+ ```diff
89
+ func openStore(path string) (*Store, error) {
90
+ - return openWithMode(path, ModeJournal)
91
+ + return openWithMode(path, ModeWAL)
92
+ }
93
+ ```
94
+ ````
95
+
88
96
  > [!IMPORTANT]
89
- > `diff` 代码块前面必须紧挨着一段完整代码块(中间只能隔空行);如果单独放一段 `diff` 而前面没有完整代码块,`npx garden check` 会报 `diff-orphan` 错误。
97
+ > `diff` 代码块必须紧跟在一段完整代码块后面;如果单独写一段 `diff` 而前面没有完整代码,`npx garden check` 会报 `diff-orphan` 错误。
90
98
 
91
99
  ## 3. 中文加粗、荧光笔高亮与提示框
92
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -716,9 +716,20 @@ const columns = readColumnReferences(posts).sort(
716
716
  const thoughts = readMarkdownTree(path.join(contentRoot, "thoughts"), "thought").sort(
717
717
  (a, b) => Date.parse(b.date) - Date.parse(a.date),
718
718
  );
719
- const allHandbookDocs = readHandbookDocs();
720
- const gardenIndexDoc = allHandbookDocs.find((doc) => doc.path === "index") ?? null;
721
- const gardenDocs = allHandbookDocs.filter((doc) => doc.path !== "index");
719
+ // docs/handbook/en/** mirrors the Chinese handbook page for page: the same
720
+ // relative path is the counterpart. Each entry carries its lang; English paths
721
+ // drop the en/ prefix and are served under /garden/en/.
722
+ const allHandbookDocs = readHandbookDocs().map((doc) =>
723
+ doc.path === "en" || doc.path.startsWith("en/")
724
+ ? { ...doc, lang: "en", path: doc.path === "en" ? "index" : doc.path.slice(3) }
725
+ : { ...doc, lang: "zh" },
726
+ );
727
+ const zhHandbookDocs = allHandbookDocs.filter((doc) => doc.lang === "zh");
728
+ const enHandbookDocs = allHandbookDocs.filter((doc) => doc.lang === "en");
729
+ const gardenIndexDoc = zhHandbookDocs.find((doc) => doc.path === "index") ?? null;
730
+ const gardenDocs = zhHandbookDocs.filter((doc) => doc.path !== "index");
731
+ const gardenIndexDocEn = enHandbookDocs.find((doc) => doc.path === "index") ?? null;
732
+ const gardenDocsEn = enHandbookDocs.filter((doc) => doc.path !== "index");
722
733
 
723
734
  // Intrinsic image sizes for width/height on <img>: local files are read now,
724
735
  // remote ones come from image-sizes.json (written by scripts/image-sizes.mjs).
@@ -762,7 +773,12 @@ assertUnique(
762
773
  "专栏文章顺序",
763
774
  );
764
775
  assertUnique(thoughts, (item) => item.slug, "随想 slug");
776
+ // /thoughts/page/<n>/ holds the static pages of the feed.
777
+ for (const thought of thoughts) {
778
+ if (thought.slug === "page") fail(thought.sourcePath ?? "content/thoughts/page.md", "随想不能叫 page:/thoughts/page/ 留给分页");
779
+ }
765
780
  assertUnique(gardenDocs, (item) => item.path, "Garden 文档路径");
781
+ assertUnique(gardenDocsEn, (item) => item.path, "Garden English handbook path");
766
782
 
767
783
  const parityDir = path.join(root, "parity", "articles");
768
784
  const a2WordCounts = fs.existsSync(parityDir)
@@ -857,6 +873,8 @@ export const columns = ${JSON.stringify(columns, null, 2)} as ContentEntry[];
857
873
  export const thoughts = ${JSON.stringify(thoughts, null, 2)} as ContentEntry[];
858
874
  export const gardenDocs = ${JSON.stringify(gardenDocs, null, 2)} as GardenDocEntry[];
859
875
  export const gardenIndexDoc = ${JSON.stringify(gardenIndexDoc, null, 2)} as GardenDocEntry | null;
876
+ export const gardenDocsEn = ${JSON.stringify(gardenDocsEn, null, 2)} as GardenDocEntry[];
877
+ export const gardenIndexDocEn = ${JSON.stringify(gardenIndexDocEn, null, 2)} as GardenDocEntry | null;
860
878
  export const a2WordCounts = ${JSON.stringify(a2WordCounts, null, 2)} as Record<string, number>;
861
879
  export const linkAssets = ${JSON.stringify(linkAssetsForRender(root, basePath), null, 2)} as LinkAssets;
862
880
  export const imageSizes: Record<string, { width: number; height: number }> = ${JSON.stringify(imageSizes)};
@@ -949,6 +967,8 @@ writeRssFeed(
949
967
  const sitemapEntries = [
950
968
  "/", "/blog/", "/thoughts/", "/columns/", "/about/", "/garden/",
951
969
  ...gardenDocs.map((entry) => `/garden/${entry.path}/`),
970
+ ...(gardenIndexDocEn ? ["/garden/en/"] : []),
971
+ ...gardenDocsEn.map((entry) => `/garden/en/${entry.path}/`),
952
972
  ...posts.map((entry) => `/blog/${entry.path}/`),
953
973
  ...columns.map((entry) => `/columns/${entry.column}/${columnEntryPath(entry)}/`),
954
974
  ...thoughts.map((entry) => `/thoughts/${entry.slug}/`),
@@ -1055,6 +1055,39 @@ export function checkFileFacts(filePath, content, facts, root = ROOT) {
1055
1055
  return issues;
1056
1056
  }
1057
1057
 
1058
+ /**
1059
+ * English pages mirror the Chinese ones: docs/handbook/en/<path>.md must have
1060
+ * docs/handbook/<path>.md with the same section and order. Chinese pages without
1061
+ * an English twin are fine (not translated yet).
1062
+ */
1063
+ export function checkHandbookPairs(root = ROOT) {
1064
+ const handbook = path.join(root, "docs/handbook");
1065
+ const enRoot = path.join(handbook, "en");
1066
+ const issues = [];
1067
+ const head = (text) => {
1068
+ const m = text.match(/^---\n([\s\S]*?)\n---/u);
1069
+ const field = (key) => m?.[1].match(new RegExp(`^${key}:\\s*(.+)$`, "mu"))?.[1].trim();
1070
+ return { section: field("section"), order: field("order") };
1071
+ };
1072
+ for (const file of walkFiles(enRoot, (f) => f.endsWith(".md"))) {
1073
+ const rel = path.relative(enRoot, file);
1074
+ const zhFile = path.join(handbook, rel);
1075
+ const relEn = path.relative(root, file).replace(/\\/g, "/");
1076
+ if (!fs.existsSync(zhFile)) {
1077
+ issues.push({ file: relEn, line: 1, kind: "handbook-pair", message: `no Chinese counterpart docs/handbook/${rel.replace(/\\/g, "/")}` });
1078
+ continue;
1079
+ }
1080
+ const en = head(fs.readFileSync(file, "utf8"));
1081
+ const zh = head(fs.readFileSync(zhFile, "utf8"));
1082
+ for (const key of ["section", "order"]) {
1083
+ if (en[key] !== zh[key]) {
1084
+ issues.push({ file: relEn, line: 1, kind: "handbook-pair", message: `${key} is ${en[key] ?? "missing"}, the Chinese page has ${zh[key] ?? "missing"}` });
1085
+ }
1086
+ }
1087
+ }
1088
+ return issues;
1089
+ }
1090
+
1058
1091
  /**
1059
1092
  * Scan all docs/handbook/ and app/garden/ files and return any fact-check failures.
1060
1093
  */
@@ -1069,6 +1102,7 @@ export function checkAllHandbookFacts(root = ROOT) {
1069
1102
  const content = fs.readFileSync(file, "utf8");
1070
1103
  issues.push(...checkFileFacts(file, content, facts, root));
1071
1104
  }
1105
+ issues.push(...checkHandbookPairs(root));
1072
1106
  const svgFiles = walkFiles(path.join(root, "docs/handbook"), (f) => f.endsWith(".svg"));
1073
1107
  for (const svgFile of svgFiles) {
1074
1108
  const svgContent = fs.readFileSync(svgFile, "utf8");
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /** Static export: English handbook pages get <html lang="en"> (ui/garden/html-lang.mjs). */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { handbookHtmlLang, withHtmlLang } from "../ui/garden/html-lang.mjs";
7
+
8
+ /** Rewrites every .html under root whose path is an English handbook page; returns the relative paths. */
9
+ export function setHandbookHtmlLang(root) {
10
+ const changed = [];
11
+ const visit = (directory) => {
12
+ if (!fs.existsSync(directory)) return;
13
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
14
+ const absolute = path.join(directory, entry.name);
15
+ if (entry.isDirectory()) visit(absolute);
16
+ else if (entry.name.endsWith(".html")) {
17
+ const relative = path.relative(root, absolute).split(path.sep).join("/");
18
+ const lang = handbookHtmlLang(relative);
19
+ if (!lang || relative.includes("/media/") || relative.startsWith("media/")) continue;
20
+ const html = fs.readFileSync(absolute, "utf8");
21
+ const next = withHtmlLang(html, lang);
22
+ if (next !== html) {
23
+ fs.writeFileSync(absolute, next);
24
+ changed.push(relative);
25
+ }
26
+ }
27
+ }
28
+ };
29
+ visit(root);
30
+ return changed.sort();
31
+ }
32
+
33
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
34
+ const siteRoot = process.env.GARDEN_SITE ? path.resolve(process.env.GARDEN_SITE) : process.cwd();
35
+ setHandbookHtmlLang(path.join(siteRoot, "dist", "client"));
36
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Static export with a basePath: vinext writes 404.html at dist/client/404.html.
4
+ * Copy it into the basePath folder too, so a host that serves only that folder
5
+ * (or Paths under /basePath/…) still has a 404 page next to the site.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ export function copyNotFoundIntoBasePath(clientRoot, basePath = "") {
12
+ const base = String(basePath ?? "").replace(/\/+$/u, "").replace(/^\/+/u, "");
13
+ if (!base) return null;
14
+ const source = path.join(clientRoot, "404.html");
15
+ const destination = path.join(clientRoot, base, "404.html");
16
+ if (!fs.existsSync(source) || !fs.existsSync(path.dirname(destination))) return null;
17
+ fs.copyFileSync(source, destination);
18
+ return path.relative(clientRoot, destination).split(path.sep).join("/");
19
+ }
20
+
21
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
22
+ const siteRoot = process.env.GARDEN_SITE ? path.resolve(process.env.GARDEN_SITE) : process.cwd();
23
+ const basePath = (process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "");
24
+ copyNotFoundIntoBasePath(path.join(siteRoot, "dist", "client"), basePath);
25
+ }