hd-garden 0.8.0 → 0.8.2

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 (187) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +2 -2
  2. package/.env.en.example +67 -0
  3. package/README.en.md +2 -2
  4. package/README.md +2 -2
  5. package/app/a2/[slug]/page.tsx +4 -3
  6. package/app/a2/page.tsx +4 -3
  7. package/app/about/page.tsx +8 -9
  8. package/app/admin/page.tsx +7 -6
  9. package/app/admin/preview/[...path]/page.tsx +2 -1
  10. package/app/admin/ui/blocks/page.tsx +2 -1
  11. package/app/admin/ui/catalog/page.tsx +2 -1
  12. package/app/admin/ui/evals/page.tsx +2 -1
  13. package/app/admin/ui/page.tsx +2 -1
  14. package/app/blog/[...path]/page.tsx +7 -6
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +8 -7
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticleMeasure.tsx +6 -3
  20. package/app/components/ArticleReferences.tsx +4 -1
  21. package/app/components/ArticleTags.tsx +3 -2
  22. package/app/components/BlogArchive.tsx +20 -16
  23. package/app/components/Comments.tsx +16 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/GardenContentGuide.tsx +10 -2
  27. package/app/components/GardenDocsNav.tsx +4 -2
  28. package/app/components/GiscusComments.tsx +2 -1
  29. package/app/components/GithubLiveOverview.tsx +10 -8
  30. package/app/components/GithubProfile.tsx +31 -21
  31. package/app/components/HandbookLangSwitch.tsx +8 -4
  32. package/app/components/HandbookView.tsx +92 -9
  33. package/app/components/MarkdownArticle.tsx +31 -7
  34. package/app/components/MermaidDiagram.tsx +4 -3
  35. package/app/components/SiteChrome.tsx +3 -3
  36. package/app/components/SiteFooter.tsx +1 -1
  37. package/app/components/SiteHeader.tsx +13 -4
  38. package/app/components/ThoughtCard.tsx +16 -14
  39. package/app/components/UiBlock.tsx +3 -2
  40. package/app/components/Webmentions.tsx +11 -11
  41. package/app/components/WidgetChart.tsx +8 -6
  42. package/app/components/garden/FilePreview.tsx +2 -2
  43. package/app/components/garden/GardenBlock.tsx +7 -0
  44. package/app/components/garden/GardenFile.tsx +2 -2
  45. package/app/components/garden/RefDemo.tsx +62 -0
  46. package/app/components/garden/blocks.tsx +56 -8
  47. package/app/components/garden/patterns.tsx +497 -0
  48. package/app/garden/[...slug]/page.tsx +3 -2
  49. package/app/garden/en/page.tsx +7 -7
  50. package/app/garden/page.tsx +3 -2
  51. package/app/globals.css +49 -1
  52. package/app/lib/a2-meta.ts +26 -3
  53. package/app/lib/content.ts +2 -2
  54. package/app/lib/garden-docs-meta.ts +24 -0
  55. package/app/lib/github-id.mjs +8 -0
  56. package/app/lib/handbook-switch.mjs +12 -0
  57. package/app/lib/i18n.ts +401 -2
  58. package/app/lib/rehype-field-tables.mjs +47 -0
  59. package/app/lib/remark-a2.mjs +18 -8
  60. package/app/lib/remark-link-card.mjs +22 -0
  61. package/app/lib/remark-ref-demos.mjs +42 -0
  62. package/app/lib/tag-filter.ts +9 -2
  63. package/app/not-found.tsx +32 -4
  64. package/app/page.tsx +17 -14
  65. package/app/rss.xml/route.ts +2 -1
  66. package/app/styles/a2.css +338 -0
  67. package/app/styles/shell.css +15 -2
  68. package/app/tags/page.tsx +7 -5
  69. package/app/thoughts/[slug]/page.tsx +2 -1
  70. package/app/thoughts/page/[n]/page.tsx +1 -1
  71. package/app/thoughts/page.tsx +2 -1
  72. package/app/ui-parity/page.tsx +38 -22
  73. package/bin/garden.mjs +129 -19
  74. package/docs/handbook/ai-writing.md +2 -2
  75. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  76. package/docs/handbook/components/card.md +43 -22
  77. package/docs/handbook/components/chart.md +10 -26
  78. package/docs/handbook/components/choice.md +42 -0
  79. package/docs/handbook/components/compare.md +32 -17
  80. package/docs/handbook/components/figure.md +33 -46
  81. package/docs/handbook/components/file.md +10 -15
  82. package/docs/handbook/components/html.md +10 -30
  83. package/docs/handbook/components/interactive.md +9 -35
  84. package/docs/handbook/components/list.md +65 -0
  85. package/docs/handbook/components/relations.md +95 -0
  86. package/docs/handbook/components/stat.md +10 -28
  87. package/docs/handbook/components/timeline.md +13 -28
  88. package/docs/handbook/components.md +210 -19
  89. package/docs/handbook/config.md +5 -3
  90. package/docs/handbook/deploy.md +1 -1
  91. package/docs/handbook/design.md +20 -12
  92. package/docs/handbook/en/ai-writing.md +2 -2
  93. package/docs/handbook/en/components/card.md +42 -21
  94. package/docs/handbook/en/components/chart.md +10 -26
  95. package/docs/handbook/en/components/choice.md +42 -0
  96. package/docs/handbook/en/components/compare.md +30 -15
  97. package/docs/handbook/en/components/figure.md +29 -42
  98. package/docs/handbook/en/components/file.md +9 -14
  99. package/docs/handbook/en/components/html.md +9 -29
  100. package/docs/handbook/en/components/interactive.md +8 -34
  101. package/docs/handbook/en/components/list.md +65 -0
  102. package/docs/handbook/en/components/relations.md +95 -0
  103. package/docs/handbook/en/components/stat.md +9 -27
  104. package/docs/handbook/en/components/timeline.md +9 -24
  105. package/docs/handbook/en/components.md +211 -19
  106. package/docs/handbook/en/config.md +32 -30
  107. package/docs/handbook/en/deploy.md +12 -12
  108. package/docs/handbook/en/design.md +19 -12
  109. package/docs/handbook/en/index.md +2 -2
  110. package/docs/handbook/en/reference/card.md +85 -0
  111. package/docs/handbook/en/reference/chart.md +68 -0
  112. package/docs/handbook/en/reference/choice.md +69 -0
  113. package/docs/handbook/en/reference/compare.md +59 -0
  114. package/docs/handbook/en/reference/figure.md +84 -0
  115. package/docs/handbook/en/reference/file/checklist.md +17 -0
  116. package/docs/handbook/en/reference/file.md +50 -0
  117. package/docs/handbook/en/reference/html.md +62 -0
  118. package/docs/handbook/en/reference/interactive.md +71 -0
  119. package/docs/handbook/en/reference/list.md +72 -0
  120. package/docs/handbook/en/reference/relations.md +113 -0
  121. package/docs/handbook/en/reference/stat.md +60 -0
  122. package/docs/handbook/en/reference/timeline.md +56 -0
  123. package/docs/handbook/en/reference.md +26 -0
  124. package/docs/handbook/en/start.md +2 -2
  125. package/docs/handbook/en/workflow.md +6 -5
  126. package/docs/handbook/en/writing.md +3 -2
  127. package/docs/handbook/index.md +1 -1
  128. package/docs/handbook/reference/card.md +85 -0
  129. package/docs/handbook/reference/chart.md +68 -0
  130. package/docs/handbook/reference/choice.md +69 -0
  131. package/docs/handbook/reference/compare.md +59 -0
  132. package/docs/handbook/reference/figure.md +84 -0
  133. package/docs/handbook/reference/file/checklist.md +17 -0
  134. package/docs/handbook/reference/file.md +50 -0
  135. package/docs/handbook/reference/html.md +62 -0
  136. package/docs/handbook/reference/interactive.md +71 -0
  137. package/docs/handbook/reference/list.md +72 -0
  138. package/docs/handbook/reference/relations.md +113 -0
  139. package/docs/handbook/reference/stat.md +60 -0
  140. package/docs/handbook/reference/timeline.md +56 -0
  141. package/docs/handbook/reference.md +26 -0
  142. package/docs/handbook/start.md +1 -1
  143. package/docs/handbook/workflow.md +6 -5
  144. package/docs/handbook/writing.md +3 -2
  145. package/next.config.ts +6 -0
  146. package/package.json +2 -1
  147. package/public/images/garden/ref-card-book.svg +1 -0
  148. package/public/images/garden/ref-card-lamp.svg +1 -0
  149. package/scripts/build-content.mjs +37 -16
  150. package/scripts/favicon-ico.mjs +9 -6
  151. package/scripts/file-preview.mjs +10 -7
  152. package/scripts/handbook-allowlist.mjs +18 -0
  153. package/scripts/handbook-facts.mjs +305 -11
  154. package/scripts/link-assets.mjs +7 -0
  155. package/scripts/new-post.mjs +18 -9
  156. package/scripts/prepare-pages-output.mjs +30 -18
  157. package/scripts/redirects.mjs +17 -6
  158. package/templates/garden-blocks.md +67 -0
  159. package/templates/garden-snippets.plist +61 -0
  160. package/templates/site/Makefile.en +45 -0
  161. package/templates/site/README.en.md +15 -0
  162. package/templates/site/site.config.en.yaml +55 -0
  163. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  164. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  165. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  166. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  167. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  168. package/templates/workflows/pages.en.yml +195 -0
  169. package/templates/workflows/pages.yml +195 -0
  170. package/ui/garden/catalog.mjs +205 -4
  171. package/ui/garden/check.mjs +6 -0
  172. package/ui/garden/file-block.mjs +8 -7
  173. package/ui/garden/html-lang.mjs +73 -1
  174. package/ui/garden/markdown-links.mjs +7 -4
  175. package/ui/garden/parse.mjs +45 -0
  176. package/ui/garden/relations.mjs +252 -0
  177. package/ui/garden/search-index.mjs +6 -4
  178. package/ui/garden/source-icon.mjs +54 -0
  179. package/ui/parity-fixtures.mjs +77 -0
  180. package/ui/preview/dist/garden-ui.esm.js +41 -4
  181. package/ui/preview/dist/garden-ui.iife.js +41 -4
  182. package/ui/prompt/rules.md +28 -1
  183. package/ui/react/components/chart.tsx +1 -1
  184. package/ui/react/components/display.tsx +1 -1
  185. package/ui/react/lang.tsx +4 -0
  186. package/ui/react/parity.tsx +8 -6
  187. package/worker/index.ts +8 -2
@@ -19,6 +19,7 @@ import matter from "gray-matter";
19
19
  import { fetchFavicon, fetchImage, fetchPageMeta, isPublicUrl, linkHost } from "./link-meta-core.mjs";
20
20
  import { parse as parseYaml } from "yaml";
21
21
  import { markdownLinks } from "../ui/garden/markdown-links.mjs";
22
+ import { gardenSourceUrls } from "../ui/garden/source-icon.mjs";
22
23
  import { configuredSiteAddress, sameSiteMatcher } from "../ui/garden/same-site.mjs";
23
24
 
24
25
  export const CACHE_FILE = "link-assets.json";
@@ -76,6 +77,12 @@ export function collectSiteLinks(siteRoot, env = process.env) {
76
77
  const host = linkHost(url);
77
78
  if (host && !hosts.has(host)) hosts.set(host, url);
78
79
  }
80
+ // Card sources get the same favicon as links to that site.
81
+ for (const url of gardenSourceUrls(content)) {
82
+ if (!isPublicUrl(url) || sameSite(url)) continue;
83
+ const host = linkHost(url);
84
+ if (host && !hosts.has(host)) hosts.set(host, url);
85
+ }
79
86
  }
80
87
  return { hosts, cards, urls };
81
88
  }
@@ -3,12 +3,14 @@ import { lstat, mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { formatAuthorDate } from "./site-date.mjs";
5
5
  import { cheatSheet } from "../ui/garden/catalog.mjs";
6
+ import { siteCliLang } from "../ui/core/cli-locale.mjs";
6
7
 
7
8
  const root = path.resolve(process.env.GARDEN_SITE || process.cwd());
8
9
  const option = (name) => (process.env[`GARDEN_NEW_${name}`] ?? "").trim();
9
10
 
10
11
  async function main() {
11
- const title = option("TITLE") || "新文章";
12
+ const lang = siteCliLang(root);
13
+ const title = option("TITLE") || (lang === "en" ? "New Post" : "新文章");
12
14
  const date = formatAuthorDate();
13
15
  const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "");
14
16
  const slug = option("SLUG") || titleSlug || `post-${date.slice(0, 10)}-${randomUUID().slice(0, 8)}`;
@@ -41,16 +43,16 @@ async function main() {
41
43
  "---",
42
44
  `title: ${JSON.stringify(title)}`,
43
45
  `date: ${date}`,
44
- `description: ${JSON.stringify("请填写一句话摘要。")}`,
46
+ `description: ${JSON.stringify(lang === "en" ? "Write a one-sentence summary." : "请填写一句话摘要。")}`,
45
47
  "tags: []",
46
48
  "draft: true",
47
49
  "---",
48
50
  "",
49
- "## 起因",
51
+ lang === "en" ? "## Context" : "## 起因",
50
52
  "",
51
- "从这里开始写正文。",
53
+ lang === "en" ? "Start writing here." : "从这里开始写正文。",
52
54
  "",
53
- cheatSheet(),
55
+ cheatSheet(lang),
54
56
  "",
55
57
  ].join("\n");
56
58
  try {
@@ -62,10 +64,17 @@ async function main() {
62
64
  throw error;
63
65
  }
64
66
 
65
- console.log(`已创建草稿:${path.relative(root, destination)}`);
66
- console.log(`文章路径:/blog/${[...segments, slug].join("/")}/`);
67
- console.log("预览:CONTENT_INCLUDE_DRAFTS=1 make dev");
68
- console.log("发布前请完善摘要、标签与正文,并将 draft 改为 false,再运行 make update。");
67
+ if (lang === "en") {
68
+ console.log(`Created draft: ${path.relative(root, destination)}`);
69
+ console.log(`Post URL: /blog/${[...segments, slug].join("/")}/`);
70
+ console.log("Preview: CONTENT_INCLUDE_DRAFTS=1 make dev");
71
+ console.log("Before publishing, fill in description, tags, and body, set draft to false, then run make update.");
72
+ } else {
73
+ console.log(`已创建草稿:${path.relative(root, destination)}`);
74
+ console.log(`文章路径:/blog/${[...segments, slug].join("/")}/`);
75
+ console.log("预览:CONTENT_INCLUDE_DRAFTS=1 make dev");
76
+ console.log("发布前请完善摘要、标签与正文,并将 draft 改为 false,再运行 make update。");
77
+ }
69
78
  }
70
79
 
71
80
  main().catch((error) => {
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { langOf, siteCliLang } from "../ui/core/cli-locale.mjs";
5
6
  import { STATIC_PLACEHOLDER } from "../ui/garden/static-params.mjs";
6
7
 
7
8
  const forbiddenDirectoryNames = new Set([
@@ -30,8 +31,12 @@ const isPublishedFile = (relativePath) =>
30
31
  publishedExtensions[relativePath.split("/", 1)[0]]?.has(path.extname(relativePath)) === true;
31
32
  const deploymentVersionPlaceholder = "00000000-0000-4000-8000-000000000000";
32
33
 
34
+ const cliLang = process.env.GARDEN_LOCALE ? langOf(process.env.GARDEN_LOCALE) : siteCliLang(process.env.GARDEN_SITE ?? process.cwd());
35
+ /** Chinese or English CLI text, by the site's locale. */
36
+ const msg = (zh, en) => (cliLang === "en" ? en : zh);
37
+
33
38
  function fail(message) {
34
- throw new Error(`Pages 产物无效:${message}`);
39
+ throw new Error(msg(`Pages 产物无效:${message}`, `Invalid Pages output: ${message}`));
35
40
  }
36
41
 
37
42
  async function assertDirectory(directory, label) {
@@ -39,9 +44,9 @@ async function assertDirectory(directory, label) {
39
44
  try {
40
45
  stat = await fs.stat(directory);
41
46
  } catch {
42
- fail(`${label}不存在:${directory}`);
47
+ fail(msg(`${label}不存在:${directory}`, `${label} does not exist: ${directory}`));
43
48
  }
44
- if (!stat.isDirectory()) fail(`${label}不是目录:${directory}`);
49
+ if (!stat.isDirectory()) fail(msg(`${label}不是目录:${directory}`, `${label} is not a directory: ${directory}`));
45
50
  }
46
51
 
47
52
  async function inspectTree(root, directory = root) {
@@ -51,20 +56,20 @@ async function inspectTree(root, directory = root) {
51
56
  const relativePath = path.relative(root, absolutePath).split(path.sep).join("/");
52
57
  const stat = await fs.lstat(absolutePath);
53
58
 
54
- if (stat.isSymbolicLink()) fail(`不允许软链接:${relativePath}`);
59
+ if (stat.isSymbolicLink()) fail(msg(`不允许软链接:${relativePath}`, `Symlinks are not allowed: ${relativePath}`));
55
60
  if (forbiddenDirectoryNames.has(entry.name)) {
56
- fail(`不允许源码目录:${relativePath}`);
61
+ fail(msg(`不允许源码目录:${relativePath}`, `Source directories are not allowed: ${relativePath}`));
57
62
  }
58
- if (entry.name.startsWith(".env")) fail(`不允许环境文件:${relativePath}`);
59
- if (entry.name.startsWith(STATIC_PLACEHOLDER)) fail(`空动态路由的占位页没有删掉:${relativePath}`);
63
+ if (entry.name.startsWith(".env")) fail(msg(`不允许环境文件:${relativePath}`, `Environment files are not allowed: ${relativePath}`));
64
+ if (entry.name.startsWith(STATIC_PLACEHOLDER)) fail(msg(`空动态路由的占位页没有删掉:${relativePath}`, `Placeholder page for an empty dynamic route was not removed: ${relativePath}`));
60
65
  if (/^package(?:-lock)?\.json$/u.test(entry.name)) {
61
- fail(`不允许依赖清单:${relativePath}`);
66
+ fail(msg(`不允许依赖清单:${relativePath}`, `Dependency manifests are not allowed: ${relativePath}`));
62
67
  }
63
68
  if (forbiddenFileNames.has(entry.name)) {
64
- fail(`不允许源码配置:${relativePath}`);
69
+ fail(msg(`不允许源码配置:${relativePath}`, `Source config files are not allowed: ${relativePath}`));
65
70
  }
66
71
  if (entry.isFile() && forbiddenExtensions.has(path.extname(entry.name)) && !isPublishedFile(relativePath)) {
67
- fail(`不允许源码或 Source Map:${relativePath}`);
72
+ fail(msg(`不允许源码或 Source Map:${relativePath}`, `Source files or source maps are not allowed: ${relativePath}`));
68
73
  }
69
74
  if (entry.isDirectory()) await inspectTree(root, absolutePath);
70
75
  }
@@ -163,25 +168,25 @@ async function stabilizeDeploymentVersion(root) {
163
168
 
164
169
  export async function validateStaticOutput(sourceDirectory) {
165
170
  const source = path.resolve(sourceDirectory);
166
- await assertDirectory(source, "静态产物目录");
171
+ await assertDirectory(source, msg("静态产物目录", "Static output directory"));
167
172
  await fs.access(path.join(source, "index.html")).catch(() => {
168
- fail(`缺少入口文件:${path.join(source, "index.html")}`);
173
+ fail(msg(`缺少入口文件:${path.join(source, "index.html")}`, `Missing entry file: ${path.join(source, "index.html")}`));
169
174
  });
170
175
  await inspectTree(source);
171
176
  return source;
172
177
  }
173
178
 
174
- export async function preparePagesOutput(sourceDirectory, destinationDirectory) {
179
+ export async function preparePagesOutput(sourceDirectory, destinationDirectory, options = {}) {
175
180
  const source = await validateStaticOutput(sourceDirectory);
176
181
  const destination = path.resolve(destinationDirectory);
177
- await assertDirectory(destination, "Pages 仓库目录");
182
+ await assertDirectory(destination, msg("Pages 仓库目录", "Pages repository directory"));
178
183
  await fs.access(path.join(destination, ".git")).catch(() => {
179
- fail(`目标不是 Git 仓库:${destination}`);
184
+ fail(msg(`目标不是 Git 仓库:${destination}`, `Destination is not a Git repository: ${destination}`));
180
185
  });
181
186
 
182
187
  const relativeFromDestination = path.relative(destination, source);
183
188
  if (!relativeFromDestination.startsWith("..") && !path.isAbsolute(relativeFromDestination)) {
184
- fail("静态产物目录不能位于目标仓库内部");
189
+ fail(msg("静态产物目录不能位于目标仓库内部", "The static output directory cannot be inside the destination repository"));
185
190
  }
186
191
 
187
192
  for (const entry of await fs.readdir(destination, { withFileTypes: true })) {
@@ -191,9 +196,16 @@ export async function preparePagesOutput(sourceDirectory, destinationDirectory)
191
196
 
192
197
  await fs.cp(source, destination, { recursive: true, force: true });
193
198
  await fs.writeFile(path.join(destination, ".nojekyll"), "");
199
+ let isEn = options.lang === "en" || /^en/iu.test(String(process.env.GARDEN_LOCALE ?? ""));
200
+ if (!isEn && options.lang !== "zh") {
201
+ const indexHtml = await fs.readFile(path.join(source, "index.html"), "utf8").catch(() => "");
202
+ if (/<html\b[^>]*\slang="en(?:-US)?"/iu.test(indexHtml)) isEn = true;
203
+ }
194
204
  await fs.writeFile(
195
205
  path.join(destination, "README.md"),
196
- "# Garden 静态站点\n\n此仓库只保存 Garden 自动生成的公开静态文件,请勿手工编辑。\n",
206
+ isEn
207
+ ? "# Garden Static Site\n\nThis repository holds public static files generated automatically by Garden. Do not edit by hand.\n"
208
+ : "# Garden 静态站点\n\n此仓库只保存 Garden 自动生成的公开静态文件,请勿手工编辑。\n",
197
209
  );
198
210
  await stabilizeDeploymentVersion(destination);
199
211
  }
@@ -204,7 +216,7 @@ const invokedDirectly = process.argv[1] &&
204
216
  if (invokedDirectly) {
205
217
  const [, , sourceDirectory, destinationDirectory] = process.argv;
206
218
  if (!sourceDirectory || process.argv.length > 4) {
207
- throw new Error("用法:node scripts/prepare-pages-output.mjs <静态产物目录> [Pages 仓库目录]");
219
+ throw new Error(msg("用法:node scripts/prepare-pages-output.mjs <静态产物目录> [Pages 仓库目录]", "Usage: node scripts/prepare-pages-output.mjs <static output directory> [Pages repository directory]"));
208
220
  }
209
221
  if (destinationDirectory) {
210
222
  await preparePagesOutput(sourceDirectory, destinationDirectory);
@@ -35,20 +35,26 @@ export function redirectFile(from) {
35
35
  return /\.html?$/iu.test(clean) ? clean : path.posix.join(clean.replace(/\/+$/u, ""), "index.html");
36
36
  }
37
37
 
38
- export function redirectPage({ href, canonical, icon = "" }) {
38
+ export function redirectPage({ href, canonical, icon = "", lang = "zh" }) {
39
39
  // The site's own icon, so the browser does not go looking for /favicon.ico.
40
40
  const iconLink = icon ? `\n<link rel="icon" href="${escapeHtml(icon)}"${/\.svg(?:[?#]|$)/iu.test(icon) ? ' type="image/svg+xml"' : ""}>` : "";
41
+ const isEn = lang === "en";
42
+ const htmlLang = isEn ? "en" : "zh-CN";
43
+ const title = isEn ? "Page moved" : "页面已移动";
44
+ const bodyText = isEn
45
+ ? `<p>This page has moved to <a href="${escapeHtml(href)}">${escapeHtml(canonical)}</a>.</p>`
46
+ : `<p>页面已移到 <a href="${escapeHtml(href)}">${escapeHtml(canonical)}</a>。</p>`;
41
47
  return `<!doctype html>
42
- <html lang="zh-CN">
48
+ <html lang="${htmlLang}">
43
49
  <head>
44
50
  <meta charset="utf-8">
45
- <title>页面已移动</title>
51
+ <title>${title}</title>
46
52
  <link rel="canonical" href="${escapeHtml(canonical)}">${iconLink}
47
53
  <meta http-equiv="refresh" content="0; url=${escapeHtml(href)}">
48
54
  <script>location.replace(${JSON.stringify(href).replace(/</gu, "\\u003c")}${href.includes("#") ? "" : " + location.hash"})</script>
49
55
  </head>
50
56
  <body>
51
- <p>页面已移到 <a href="${escapeHtml(href)}">${escapeHtml(canonical)}</a>。</p>
57
+ ${bodyText}
52
58
  </body>
53
59
  </html>
54
60
  `;
@@ -67,6 +73,7 @@ export function redirectPage({ href, canonical, icon = "" }) {
67
73
  */
68
74
  export function generateRedirects({ outDir, config = {}, env = {}, basePath = "", log = () => {}, extra = [], icon = "" }) {
69
75
  const base = String(basePath ?? "").replace(/\/+$/u, "");
76
+ const lang = /^en/iu.test(String(config?.locale ?? "")) ? "en" : "zh";
70
77
  let origin = "";
71
78
  try {
72
79
  const address = configuredSiteAddress(config, env);
@@ -118,7 +125,7 @@ export function generateRedirects({ outDir, config = {}, env = {}, basePath = ""
118
125
  const href = external ? to : withTrailingSlash(`${base}${to}`);
119
126
  const canonical = external || !origin ? href : `${origin}${href}`;
120
127
  fs.mkdirSync(path.dirname(file), { recursive: true });
121
- fs.writeFileSync(file, redirectPage({ href, canonical, icon }));
128
+ fs.writeFileSync(file, redirectPage({ href, canonical, icon, lang }));
122
129
  written.push({ from, to: href, file });
123
130
  }
124
131
  return { written, expected: pairs.length, skipped };
@@ -136,7 +143,11 @@ export function writeRedirects(options) {
136
143
  * warning (a page went missing without a reason).
137
144
  */
138
145
  export function runRedirectStep({ siteRoot, env = process.env, out = process.stdout, err = process.stderr, generate = generateRedirects }) {
139
- const outDir = path.join(siteRoot, "dist", "client");
146
+ const baseDir = String(env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/^\/+|\/+$/gu, "");
147
+ // A base-path build exports pages under dist/client/<base>/, so old paths and targets live there.
148
+ const outDir = baseDir && fs.existsSync(path.join(siteRoot, "dist", "client", baseDir))
149
+ ? path.join(siteRoot, "dist", "client", baseDir)
150
+ : path.join(siteRoot, "dist", "client");
140
151
  try {
141
152
  const config = parseYaml(fs.readFileSync(path.join(siteRoot, "site.config.yaml"), "utf8")) ?? {};
142
153
  const columnsFile = path.join(siteRoot, "content", "columns.yaml");
@@ -137,3 +137,70 @@ intent: |
137
137
  src: ./post-name/spec.zip
138
138
  note: 一句话说明里面有什么
139
139
  ```
140
+
141
+ ## relations · 关系图
142
+
143
+ 三到六个关键要素,读者需要看清谁影响谁、哪些是双向的;首尾相接的循环也用它,把每一步写成一条 link。
144
+
145
+ 不该用:要素更多、有分支或先后顺序时,用 ```mermaid 流程图;只走一遍的步骤用 garden list。
146
+
147
+ ```garden relations
148
+ items:
149
+ - title: 观察
150
+ text: 记下发生了什么
151
+ - title: 假设
152
+ text: 猜一个原因
153
+ - title: 实验
154
+ text: 只改一个变量
155
+ - title: 复盘
156
+ text: 留下能复用的结论
157
+ links:
158
+ - from: 1
159
+ to: 2
160
+ - from: 2
161
+ to: 3
162
+ - from: 3
163
+ to: 4
164
+ - from: 4
165
+ to: 1
166
+ - from: 4
167
+ to: 2
168
+ style: dashed
169
+ label: 改写
170
+ caption: 复盘的结论会改写下一轮的假设。
171
+ ```
172
+
173
+ ## list · 图标列表
174
+
175
+ 几条并列的行为、信号或做法,每条配一个线条图标;带标题时每条可以写几段说明。
176
+
177
+ 不该用:条目之间有先后顺序或要编号时,用普通有序列表。
178
+
179
+ ```garden list
180
+ items:
181
+ - icon: search
182
+ text: 搜索「降噪耳机」
183
+ - icon: eye
184
+ text: 看了 5 个商品详情
185
+ ```
186
+
187
+ ## choice · 选择卡
188
+
189
+ 给读者两三个具体选项让他选一个,每个选项有图标、一个醒目的数字和一句说明。
190
+
191
+ 不该用:选项会改变后面的计算结果时,写成想法,编译成 interactive。
192
+
193
+ ```garden choice
194
+ title: 你更倾向哪一种?
195
+ items:
196
+ - icon: shield
197
+ value: ¥2,950
198
+ text: 原装,价格略高
199
+ label: 更看重可靠
200
+ - icon: wallet
201
+ value: ¥2,450
202
+ text: 换过电池,价格更低
203
+ label: 更看重价格
204
+ none:
205
+ label: 都不合适
206
+ ```
@@ -126,5 +126,66 @@ note: 一句话说明里面有什么
126
126
  <key>shortcut</key>
127
127
  <string>;gfile</string>
128
128
  </dict>
129
+ <dict>
130
+ <key>phrase</key>
131
+ <string>```garden relations
132
+ items:
133
+ - title: 观察
134
+ text: 记下发生了什么
135
+ - title: 假设
136
+ text: 猜一个原因
137
+ - title: 实验
138
+ text: 只改一个变量
139
+ - title: 复盘
140
+ text: 留下能复用的结论
141
+ links:
142
+ - from: 1
143
+ to: 2
144
+ - from: 2
145
+ to: 3
146
+ - from: 3
147
+ to: 4
148
+ - from: 4
149
+ to: 1
150
+ - from: 4
151
+ to: 2
152
+ style: dashed
153
+ label: 改写
154
+ caption: 复盘的结论会改写下一轮的假设。
155
+ ```</string>
156
+ <key>shortcut</key>
157
+ <string>;grel</string>
158
+ </dict>
159
+ <dict>
160
+ <key>phrase</key>
161
+ <string>```garden list
162
+ items:
163
+ - icon: search
164
+ text: 搜索「降噪耳机」
165
+ - icon: eye
166
+ text: 看了 5 个商品详情
167
+ ```</string>
168
+ <key>shortcut</key>
169
+ <string>;glist</string>
170
+ </dict>
171
+ <dict>
172
+ <key>phrase</key>
173
+ <string>```garden choice
174
+ title: 你更倾向哪一种?
175
+ items:
176
+ - icon: shield
177
+ value: ¥2,950
178
+ text: 原装,价格略高
179
+ label: 更看重可靠
180
+ - icon: wallet
181
+ value: ¥2,450
182
+ text: 换过电池,价格更低
183
+ label: 更看重价格
184
+ none:
185
+ label: 都不合适
186
+ ```</string>
187
+ <key>shortcut</key>
188
+ <string>;gchoice</string>
189
+ </dict>
129
190
  </array>
130
191
  </plist>
@@ -0,0 +1,45 @@
1
+ MESSAGE ?= Update blog
2
+
3
+ .DEFAULT_GOAL := help
4
+
5
+ .PHONY: help new embed skill prepare dev check build update
6
+
7
+ unexport TITLE SLUG DIR URL
8
+ export GARDEN_NEW_TITLE := $(value TITLE)
9
+ export GARDEN_NEW_SLUG := $(value SLUG)
10
+ export GARDEN_NEW_DIR := $(value DIR)
11
+ export GARDEN_EMBED_URL := $(value URL)
12
+
13
+ help:
14
+ @printf "%s\n" "Common Garden commands:" " make new Create a post draft with the engine" ' make new TITLE="Post title" SLUG=my-post DIR=writing' ' make embed URL="https://example.com" Fetch external page metadata' " make skill Install interactive chart skills" " make dev Install dependencies and preview locally" " make build Build the public site" " make update Check, commit, and push to publish"
15
+
16
+ new: prepare
17
+ npx garden new
18
+
19
+ embed:
20
+ @npx garden embed
21
+
22
+ skill: prepare
23
+ npx garden skill
24
+
25
+ prepare:
26
+ @if [ ! -d node_modules ]; then npm install; fi
27
+
28
+ dev: prepare
29
+ npx garden dev
30
+
31
+ check: prepare
32
+ npx garden sync && npx garden build
33
+
34
+ build: prepare
35
+ npx garden build
36
+
37
+ update:
38
+ @if git diff --quiet && git diff --cached --quiet && [ -z "$$(git ls-files --others --exclude-standard)" ]; then \
39
+ echo "No changes to publish"; \
40
+ else \
41
+ $(MAKE) check && \
42
+ git add -A && \
43
+ git commit -m "$(MESSAGE)" && \
44
+ git push origin HEAD; \
45
+ fi
@@ -0,0 +1,15 @@
1
+ # My Garden
2
+
3
+ This repository holds only your writing and site configuration: `content/` and `site.config.yaml`. The page engine comes from [Garden](https://github.com/hedon954/garden).
4
+
5
+ ```bash
6
+ npm install
7
+ make new TITLE="Hello, World" SLUG=hello
8
+ make dev
9
+ ```
10
+
11
+ Running `npx garden dev` for the first time fills in any missing `Makefile` or skills and creates `.env.local`. Cursor and Codex read `.agents/skills/` automatically when opening this repository. Run `npx garden skill` anytime to update the bundled skills.
12
+
13
+ Comments are disabled by default. Before publishing, open [Giscus](https://giscus.app), write the repository, repository ID, category, and category ID into `.env.local`, and set the matching `GISCUS_REPO`, `GISCUS_REPO_ID`, `GISCUS_CATEGORY`, and `GISCUS_CATEGORY_ID` under GitHub Actions Variables. Leaving them empty fails the publish workflow. To publish without comments, set `GISCUS_DISABLED` to `1`.
14
+
15
+ To upgrade the engine, update `hd-garden` in `package.json` to the new version and run `npm install`.
@@ -0,0 +1,55 @@
1
+ name: Garden
2
+ tagline: Writing · Notes · Staying curious
3
+ description: Posts, short notes, and columns.
4
+ locale: en_US
5
+ # Public address of the site. Full URLs pointing here inside posts render as internal links.
6
+ # url: https://blog.example.com
7
+ # Site icons; falls back to Garden's built-in icons when omitted.
8
+ # brand:
9
+ # logo: brand/logo.svg
10
+ # favicon: /favicon.svg
11
+ # appleTouchIcon: /apple-touch-icon.png
12
+
13
+ author:
14
+ name: Your Name
15
+ github: your-github-id
16
+ githubBio: A short bio.
17
+ githubPinned: []
18
+
19
+ pages:
20
+ home:
21
+ title: |-
22
+ Notes on what I am building,
23
+ and questions worth thinking through.
24
+ subtitle: Software, reading notes, and occasional work logs.
25
+ blog:
26
+ title: Posts
27
+ subtitle: Finished long-form essays.
28
+ thoughts:
29
+ title: Thoughts
30
+ subtitle: Short notes, images, and links.
31
+ # How many items to show per screen; the next screen loads automatically as you scroll. Default: 10.
32
+ # pageSize: 10
33
+ columns:
34
+ title: Columns
35
+ subtitle: Series of posts meant to be read in order.
36
+ about:
37
+ title: Hi, I am Your Name.
38
+ subtitle: Essays and notes live here.
39
+ heading: About
40
+ paragraphs:
41
+ - A software engineer writing code and keeping notes on things figured out along the way.
42
+ - This site holds long-form posts, short thoughts, and columns.
43
+ quote: ""
44
+ focus:
45
+ - Software
46
+ - Notes
47
+ - Daily
48
+ cta:
49
+ href: /blog
50
+ label: Browse posts
51
+ garden:
52
+ title: "Write in Markdown.\nPush to update the site."
53
+ subtitle: ""
54
+
55
+ footer: Keep writing.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: garden-interactive-chart
3
+ description: >-
4
+ Writes Garden interactive charts as self-contained HTML in a same-name
5
+ sidecar directory and embeds them with a short garden html fence. Use when adding
6
+ or editing Garden widgets, steppers, inspect grids, param benches,
7
+ compare views, traces, KV cache charts, or any interactive chart / same-name directory HTML.
8
+ ---
9
+
10
+ # Garden interactive charts
11
+
12
+ Complex interaction does not go into Markdown. Each post has a same-name directory, and every `.html` file in it is one chart that can be opened on its own. The post body only embeds a short fence.
13
+
14
+ Background: [Interactive components](https://github.com/hedon954/garden/blob/main/docs/interactive-blog-components.md). Finished examples to copy: [widget-kind-examples](https://github.com/hedon954/garden/tree/main/docs/widget-kind-examples).
15
+
16
+ ## When to draw, when to stop
17
+
18
+ Ask first: is the reader stuck because a process was frozen into a photo, a parameter is a fixed example, two dimensions were flattened, a comparison needs scrolling back and forth, or a "what if" cannot be tried? If not, write prose or Mermaid.
19
+
20
+ Five primitives. Pick one before you draw:
21
+
22
+ | What the reader should do | Primitive | Copy from |
23
+ | --- | --- | --- |
24
+ | Stop at a given beat | Stepper | [stepper.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/stepper.html), [waste.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/waste.html) |
25
+ | Look at cells and talk about them | Inspect grid | [inspect-grid.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/inspect-grid.html) |
26
+ | Turn a knob, watch numbers or curves | Param bench | [param-bench.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/param-bench.html) |
27
+ | See both sides at the same moment | Compare view | [compare.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/compare.html) |
28
+ | See the current node and current edge | Trace | [trace.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/trace.html) |
29
+
30
+ If it fits, change the data instead of adding a new type. Past about 9 nodes or 12 steps, split it into two charts.
31
+
32
+ ## File conventions
33
+
34
+ When the Markdown is at `content/posts/<dir>/<slug>.md`, charts can only live in `content/posts/<dir>/<slug>/`:
35
+
36
+ ```text
37
+ content/posts/windowsill-herbs.md
38
+ content/posts/windowsill-herbs/layers.html
39
+ ```
40
+
41
+ Body:
42
+
43
+ ````md
44
+ ```garden html
45
+ src: ./windowsill-herbs/layers.html
46
+ title: Water takes about a minute to seep from the soil surface to the drain hole.
47
+ ```
48
+ ````
49
+
50
+ Rules:
51
+
52
+ - `src` must be a local `.html` or `.pdf` in that same-name directory. It cannot point to `assets/`, another post, or `https://`
53
+ - HTML that reports its height with `garden-chart` is a chart and is shown inline. Plain HTML and PDF are folded into a preview entry by default and load when the reader opens them
54
+ - `title` must be a claim, not "see the chart below"
55
+ - One chart per HTML file. Use short kebab-case names such as `layers.html` or `diagram.html`
56
+ - The HTML is self-contained: CSS, SVG and scripts all live in that one file
57
+ - Write the HTML first, then the fence. Do not spell out step arrays or sample points in the fence
58
+
59
+ The build checks the path and copies the file to `/media/...`. A wrong path fails the build.
60
+
61
+ ## Writing the HTML
62
+
63
+ 1. Copy [references/shell.html](references/shell.html), or start from the closest example.
64
+ 2. Use only the color and font tokens in [references/style.md](references/style.md). Do not invent a dark cinematic style.
65
+ 3. Call the boot script in the shell: it reads `?theme=light|dark` and uses `ResizeObserver` to send `{ source: "garden-chart", height }` to the parent page.
66
+ 4. Respect `prefers-reduced-motion: reduce`: autoplay stops at the first step or the final state.
67
+ 5. Use native `button` and `input` controls. Bind sliders to fixed inputs. Never `eval` reader input, and never make network requests.
68
+ 6. Open the HTML in a browser and walk the main path and the empty state. Then run `CONTENT_INCLUDE_DRAFTS=1 make dev` to check the embed.
69
+
70
+ ## Don't
71
+
72
+ - Put steps, grids or formula sample points in Markdown
73
+ - Use remote scripts, remote iframes or any CDN (the system monospace stack is enough)
74
+ - Read or write the parent page's DOM or cookies, or depend on `allow-same-origin`
75
+ - Use MDX, JSX, or post-specific type names (for example `autoregressive-waste`)
76
+ - Make the interaction the only explanation: the surrounding prose must stand on its own
77
+
78
+ ## Before you hand it in
79
+
80
+ - [ ] The file is in the same-name directory, and the fence has only `src` + `title`
81
+ - [ ] The HTML works when opened on its own, and the iframe grows to its height
82
+ - [ ] Light and dark themes both follow the Garden page
83
+ - [ ] No autoplay loop when reduced motion is on
84
+ - [ ] `title` gives the conclusion to readers without JS
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Chart title</title>
7
+ <style>
8
+ :root {
9
+ --paper: #f8f8f6; --paper-deep: #eeeeeb; --ink: #202124; --ink-soft: #34383d;
10
+ --muted: #65707d; --faint: #a1a6ad; --line: #deded9; --accent: #d82d48;
11
+ --accent-soft: #f9e8eb; --new: #1f7a4d; --new-soft: #e5f4ec;
12
+ }
13
+ :root[data-theme="dark"] {
14
+ --paper: #171819; --paper-deep: #202224; --ink: #ededeb; --ink-soft: #d8d8d4;
15
+ --muted: #a6adb6; --faint: #747b84; --line: #35383b; --accent: #ff5d76;
16
+ --accent-soft: #3b242a; --new: #5ee0a0; --new-soft: #1a3a2c;
17
+ }
18
+ html, body { margin: 0; background: var(--paper); color: var(--ink); }
19
+ body {
20
+ font: 13px/1.5 ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
21
+ padding: 16px;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <!-- Chart body -->
27
+ <script>
28
+ function gardenChartBoot() {
29
+ const theme = new URLSearchParams(location.search).get("theme");
30
+ if (theme === "dark" || theme === "light") {
31
+ document.documentElement.setAttribute("data-theme", theme);
32
+ }
33
+ const report = () => {
34
+ if (window.parent === window) return;
35
+ const height = Math.ceil(Math.max(
36
+ document.body.scrollHeight,
37
+ document.body.offsetHeight,
38
+ document.documentElement.scrollHeight,
39
+ ));
40
+ window.parent.postMessage({ source: "garden-chart", height }, "*");
41
+ };
42
+ new ResizeObserver(report).observe(document.body);
43
+ window.addEventListener("load", report);
44
+ requestAnimationFrame(() => requestAnimationFrame(report));
45
+ }
46
+ gardenChartBoot();
47
+ </script>
48
+ </body>
49
+ </html>