hd-garden 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-interactive-chart/SKILL.md +84 -0
- package/.agents/skills/garden-interactive-chart/references/shell.html +49 -0
- package/.agents/skills/garden-interactive-chart/references/style.md +61 -0
- package/.env.example +56 -0
- package/.openai/hosting.json +5 -0
- package/LICENSE +21 -0
- package/Makefile +48 -0
- package/README.md +72 -0
- package/app/about/page.tsx +60 -0
- package/app/admin/page.tsx +117 -0
- package/app/api/admin/distribution/route.ts +120 -0
- package/app/api/admin/posts/pin/route.ts +32 -0
- package/app/api/admin/thoughts/[id]/route.ts +44 -0
- package/app/api/admin/thoughts/route.ts +120 -0
- package/app/api/admin/uploads/route.ts +20 -0
- package/app/api/auth/github/callback/route.ts +136 -0
- package/app/api/auth/github/route.ts +29 -0
- package/app/api/auth/logout/route.ts +13 -0
- package/app/blog/[...path]/page.tsx +135 -0
- package/app/blog/page.tsx +139 -0
- package/app/chatgpt-auth.ts +86 -0
- package/app/columns/[column]/[...path]/page.tsx +160 -0
- package/app/columns/page.tsx +68 -0
- package/app/components/AdminDashboard.tsx +410 -0
- package/app/components/Comments.tsx +55 -0
- package/app/components/ContentEmptyState.tsx +51 -0
- package/app/components/ExternalEmbed.tsx +127 -0
- package/app/components/GardenAdminGuide.tsx +116 -0
- package/app/components/GardenContentGuide.tsx +241 -0
- package/app/components/GardenMediaGuide.tsx +81 -0
- package/app/components/GiscusComments.tsx +71 -0
- package/app/components/GithubLiveOverview.tsx +118 -0
- package/app/components/GithubProfile.tsx +326 -0
- package/app/components/MarkdownArticle.tsx +144 -0
- package/app/components/MermaidDiagram.tsx +117 -0
- package/app/components/PageIntro.tsx +32 -0
- package/app/components/SiteChrome.tsx +18 -0
- package/app/components/SiteFooter.tsx +29 -0
- package/app/components/SiteHeader.tsx +420 -0
- package/app/components/TableOfContents.tsx +433 -0
- package/app/components/ThoughtCard.tsx +242 -0
- package/app/components/Webmentions.tsx +194 -0
- package/app/components/WidgetChart.tsx +222 -0
- package/app/garden/page.tsx +284 -0
- package/app/garden-modules.d.ts +62 -0
- package/app/globals.css +4454 -0
- package/app/layout.tsx +102 -0
- package/app/lib/admin-auth.ts +134 -0
- package/app/lib/content.ts +112 -0
- package/app/lib/fallback-site-config.ts +56 -0
- package/app/lib/generated-content.ts +380 -0
- package/app/lib/github-content.ts +244 -0
- package/app/lib/markdown-plugins.ts +13 -0
- package/app/lib/oss.ts +100 -0
- package/app/lib/public-thoughts.ts +9 -0
- package/app/lib/repository-path.mjs +32 -0
- package/app/lib/site.ts +51 -0
- package/app/page.tsx +119 -0
- package/app/robots.ts +15 -0
- package/app/rss.xml/route.ts +84 -0
- package/app/site.config.ts +35 -0
- package/app/sitemap.ts +46 -0
- package/app/thoughts/[slug]/page.tsx +79 -0
- package/app/thoughts/page.tsx +33 -0
- package/app/yaml.d.ts +4 -0
- package/bin/garden.mjs +348 -0
- package/build/sites-vite-plugin.ts +39 -0
- package/eslint.config.mjs +20 -0
- package/next.config.ts +13 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.svg +4 -0
- package/public/images/garden/admin-studio.png +0 -0
- package/public/og.png +0 -0
- package/scripts/build-content.mjs +582 -0
- package/scripts/fetch-embed.mjs +189 -0
- package/scripts/garden-paths.mjs +21 -0
- package/scripts/new-post.mjs +73 -0
- package/scripts/prepare-pages-output.mjs +182 -0
- package/scripts/send-webmentions.mjs +27 -0
- package/site.config.yaml +48 -0
- package/templates/site/Makefile +46 -0
- package/templates/site/README.md +13 -0
- package/templates/site/content/columns.yaml +1 -0
- package/templates/site/content/posts/.gitkeep +0 -0
- package/templates/site/content/thoughts/.gitkeep +0 -0
- package/templates/site/package.json +19 -0
- package/templates/site/site.config.yaml +46 -0
- package/tsconfig.json +34 -0
- package/vite.config.ts +130 -0
- package/worker/index.ts +53 -0
|
@@ -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 widget fence. Use when adding
|
|
6
|
+
or editing Garden widgets, steppers, inspect grids, param benches,
|
|
7
|
+
compare views, traces, KV cache charts, or any 可交互图表 / 同名目录 HTML.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Garden 可交互图表
|
|
11
|
+
|
|
12
|
+
复杂交互不写进 Markdown。一篇稿对应一个同名目录,目录里每一份 `.html` 是一张可单独打开的图表;正文只嵌一段短围栏。
|
|
13
|
+
|
|
14
|
+
设计背景:[可交互组件](https://github.com/hedon954/garden/blob/main/docs/interactive-blog-components.md)。可抄的成品:[widget-kind-examples](https://github.com/hedon954/garden/tree/main/docs/widget-kind-examples)。
|
|
15
|
+
|
|
16
|
+
## 何时画、何时停
|
|
17
|
+
|
|
18
|
+
先问:读者是不是卡在过程被拍成照片、参数是死例子、二维被压扁、对比只能上下翻、或「如果」无法试?卡不住,就写散文或 Mermaid。
|
|
19
|
+
|
|
20
|
+
五种原语,先选再画:
|
|
21
|
+
|
|
22
|
+
| 要让读者做什么 | 原语 | 抄谁 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| 停在某一拍 | 步进器 | [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
|
+
| 盯着格子说话 | 检视格 | [inspect-grid.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/inspect-grid.html) |
|
|
26
|
+
| 拧旋钮看数字/曲线 | 参数台 | [param-bench.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/param-bench.html) |
|
|
27
|
+
| 同一时刻看两边 | 对照台 | [compare.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/compare.html) |
|
|
28
|
+
| 看当前节点和当前边 | 轨迹 | [trace.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/trace.html) |
|
|
29
|
+
|
|
30
|
+
装得下就改数据,不新开类型。超过约 9 个节点 / 12 步,拆成两张图。
|
|
31
|
+
|
|
32
|
+
## 文件约定
|
|
33
|
+
|
|
34
|
+
Markdown 在 `content/posts/<dir>/<slug>.md` 时,图表只能放在 `content/posts/<dir>/<slug>/`:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
content/posts/understand-kv-cache.md
|
|
38
|
+
content/posts/understand-kv-cache/waste.html
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
正文:
|
|
42
|
+
|
|
43
|
+
````md
|
|
44
|
+
```widget
|
|
45
|
+
src: ./understand-kv-cache/waste.html
|
|
46
|
+
caption: 朴素 decode 每步重算全部过去 token;6 个 token 共 21 次计算,其中 15 次是浪费。
|
|
47
|
+
```
|
|
48
|
+
````
|
|
49
|
+
|
|
50
|
+
规则:
|
|
51
|
+
|
|
52
|
+
- `src` 必须是该同名目录里的本地 `.html` 或 `.pdf`,不能指到 `assets/`、别的文章或 `https://`
|
|
53
|
+
- 带 `garden-chart` 高度回报的 HTML 是图表,直接铺开;普通 HTML 和 PDF 默认收成示意入口,读者展开后再加载
|
|
54
|
+
- `caption` 必须是判断,不能是「见下图」
|
|
55
|
+
- 一份 HTML 一张图;文件名用 `waste.html`、`stepper.html` 这种短 kebab
|
|
56
|
+
- HTML 自包含:CSS、SVG、脚本都在这一个文件里
|
|
57
|
+
- 先写 HTML,再写围栏;围栏里不要再摊步进数组或采样点
|
|
58
|
+
|
|
59
|
+
构建会校验路径并复制到 `/media/...`。路径写错时构建直接失败。
|
|
60
|
+
|
|
61
|
+
## 怎么写 HTML
|
|
62
|
+
|
|
63
|
+
1. 复制 [references/shell.html](references/shell.html),或从最像的例子改。
|
|
64
|
+
2. 颜色和字体只用 [references/style.md](references/style.md) 里的 token,不要另起暗色电影风。
|
|
65
|
+
3. 必须调用 shell 里的启动脚本:读 `?theme=light|dark`,用 `ResizeObserver` 向父页发送 `{ source: "garden-chart", height }`。
|
|
66
|
+
4. 尊重 `prefers-reduced-motion: reduce`:自动播放停在第一步或终态。
|
|
67
|
+
5. 控件用原生 `button` / `input`。滑杆绑定写死的输入,不要 `eval` 读者输入,不要发网络请求。
|
|
68
|
+
6. 在浏览器里打开这份 HTML,走一遍主路径和空态;再 `CONTENT_INCLUDE_DRAFTS=1 make dev` 看嵌入。
|
|
69
|
+
|
|
70
|
+
## 禁止
|
|
71
|
+
|
|
72
|
+
- 把步进、格子、公式采样点写进 Markdown
|
|
73
|
+
- 远程脚本、远程 iframe、任意 CDN(系统等宽栈即可)
|
|
74
|
+
- 读写父页面 DOM / cookie;不要依赖 `allow-same-origin`
|
|
75
|
+
- MDX、JSX、文章私有类型名(例如 `autoregressive-waste`)
|
|
76
|
+
- 交互当唯一讲解:周围散文必须能单独读懂
|
|
77
|
+
|
|
78
|
+
## 交稿前
|
|
79
|
+
|
|
80
|
+
- [ ] 文件在同名目录内,围栏只有 `src` + `caption`
|
|
81
|
+
- [ ] 单独打开 HTML 就能用;iframe 能撑开高度
|
|
82
|
+
- [ ] 浅色 / 深色主题都跟 Garden 纸面
|
|
83
|
+
- [ ] 减少动态时不自动循环
|
|
84
|
+
- [ ] `caption` 让关 JS 的人也能读到结论
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<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
|
+
<!-- 图表主体 -->
|
|
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>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Garden 图表视觉
|
|
2
|
+
|
|
3
|
+
跟站点 `app/globals.css` 的 token,不要跟参考文的暗色绿光走。
|
|
4
|
+
|
|
5
|
+
## Token
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
:root {
|
|
9
|
+
--paper: #f8f8f6;
|
|
10
|
+
--paper-deep: #eeeeeb;
|
|
11
|
+
--card: #ffffff;
|
|
12
|
+
--ink: #202124;
|
|
13
|
+
--ink-soft: #34383d;
|
|
14
|
+
--muted: #65707d;
|
|
15
|
+
--faint: #a1a6ad;
|
|
16
|
+
--line: #deded9;
|
|
17
|
+
--line-strong: #c9c9c2;
|
|
18
|
+
--accent: #d82d48;
|
|
19
|
+
--accent-soft: #f9e8eb;
|
|
20
|
+
--new: #1f7a4d;
|
|
21
|
+
--new-soft: #e5f4ec;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:root[data-theme="dark"] {
|
|
25
|
+
--paper: #171819;
|
|
26
|
+
--paper-deep: #202224;
|
|
27
|
+
--card: #1e2022;
|
|
28
|
+
--ink: #ededeb;
|
|
29
|
+
--ink-soft: #d8d8d4;
|
|
30
|
+
--muted: #a6adb6;
|
|
31
|
+
--faint: #747b84;
|
|
32
|
+
--line: #35383b;
|
|
33
|
+
--line-strong: #4b4f54;
|
|
34
|
+
--accent: #ff5d76;
|
|
35
|
+
--accent-soft: #3b242a;
|
|
36
|
+
--new: #5ee0a0;
|
|
37
|
+
--new-soft: #1a3a2c;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
强调色最多用在 1–2 个焦点上。新算用 `--new`,浪费/重复用 `--accent`,缓存用 `--muted`。
|
|
42
|
+
|
|
43
|
+
## 字体与密度
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
47
|
+
font-size: 13px;
|
|
48
|
+
line-height: 1.5;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
不要引入 Google Fonts。正文栏大约 700px,按 640–680 画。控件高度、间距用 4 的倍数。不要阴影,不要大圆角(格子最多 4px)。
|
|
52
|
+
|
|
53
|
+
## 语义色
|
|
54
|
+
|
|
55
|
+
| 标记 | 用途 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `new` / `append` | 这一拍新算的格子 |
|
|
58
|
+
| `waste` | 被重复计算的过去 token |
|
|
59
|
+
| `cached` | 从缓存取出、不再投影 |
|
|
60
|
+
| `idle` | 占位,视觉上消失 |
|
|
61
|
+
| `repeat` / `unique` | 检视格里的重复 vs 独特 |
|
package/.env.example
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# 公开博客的正式地址,不要以 / 结尾。GitHub Pages 构建与独立后台都使用它。
|
|
2
|
+
SITE_URL=https://your-name.github.io
|
|
3
|
+
NEXT_PUBLIC_SITE_URL=https://your-name.github.io
|
|
4
|
+
|
|
5
|
+
# 独立管理后台的地址;公开博客静态导出时 /admin 会跳转到这里。
|
|
6
|
+
ADMIN_URL=https://admin.example.com
|
|
7
|
+
|
|
8
|
+
# 可选:反向代理或多域名后台时,显式指定 GitHub OAuth 回调地址。
|
|
9
|
+
# GITHUB_OAUTH_REDIRECT_URI=https://admin.example.com/api/auth/github/callback
|
|
10
|
+
|
|
11
|
+
# Giscus:在 https://giscus.app 配置后复制以下四项。
|
|
12
|
+
GISCUS_REPO=owner/repository
|
|
13
|
+
GISCUS_REPO_ID=R_xxxxxxxxxx
|
|
14
|
+
GISCUS_CATEGORY=Announcements
|
|
15
|
+
GISCUS_CATEGORY_ID=DIC_xxxxxxxxxx
|
|
16
|
+
|
|
17
|
+
# Webmention.io:注册正式域名后填写域名,不含 https://。
|
|
18
|
+
WEBMENTION_IO_DOMAIN=example.com
|
|
19
|
+
|
|
20
|
+
# 可选:使用自建接收端点时覆盖 Webmention.io 默认端点。
|
|
21
|
+
# WEBMENTION_ENDPOINT=https://example.com/webmention
|
|
22
|
+
|
|
23
|
+
# 可选:本地 About 页展示精确 commits、置顶仓库与贡献热点图。仅服务端使用,不会发送到浏览器。
|
|
24
|
+
# GITHUB_TOKEN=github_pat_xxx
|
|
25
|
+
|
|
26
|
+
# 可选:无 Cookie 的访问统计,例如 Plausible。
|
|
27
|
+
# NEXT_PUBLIC_ANALYTICS_DOMAIN=example.com
|
|
28
|
+
# NEXT_PUBLIC_ANALYTICS_SCRIPT=https://plausible.io/js/script.js
|
|
29
|
+
|
|
30
|
+
# 可选:本地构建时预览草稿和未来文章。
|
|
31
|
+
# CONTENT_INCLUDE_DRAFTS=1
|
|
32
|
+
|
|
33
|
+
# 管理后台:GitHub OAuth App、后台身份白名单,以及写入内容仓库的细粒度 PAT。
|
|
34
|
+
# Callback URL: https://admin.example.com/api/auth/github/callback
|
|
35
|
+
# CONTENT_REPOSITORY=owner/repository
|
|
36
|
+
# CONTENT_BRANCH=main
|
|
37
|
+
# CONTENT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx
|
|
38
|
+
# GITHUB_OAUTH_CLIENT_ID=Ov23li_xxxxxxxxxxxx
|
|
39
|
+
# GITHUB_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxx
|
|
40
|
+
# GITHUB_SESSION_SECRET=请使用至少32字符的随机字符串
|
|
41
|
+
# ADMIN_GITHUB_LOGINS=your-github-login
|
|
42
|
+
|
|
43
|
+
# 后台媒体:阿里云 OSS。AccessKey 仅保留在后台服务,不会下发浏览器。
|
|
44
|
+
# OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
45
|
+
# OSS_BUCKET=your-public-media-bucket
|
|
46
|
+
# OSS_ACCESS_KEY_ID=LTAIxxxxxxxx
|
|
47
|
+
# OSS_ACCESS_KEY_SECRET=xxxxxxxx
|
|
48
|
+
# OSS_PREFIX=blog-media
|
|
49
|
+
# OSS_PUBLIC_BASE_URL=https://media.example.com
|
|
50
|
+
# OSS_MAX_UPLOAD_MB=200
|
|
51
|
+
|
|
52
|
+
# 内容分发:X 使用拥有发帖权限的用户访问令牌;其余平台接入你自己的发布桥接服务。
|
|
53
|
+
# X_USER_ACCESS_TOKEN=xxxxxxxx
|
|
54
|
+
# CSDN_SYNC_WEBHOOK=https://publisher.example.com/csdn
|
|
55
|
+
# ZHIHU_SYNC_WEBHOOK=https://publisher.example.com/zhihu
|
|
56
|
+
# JUEJIN_SYNC_WEBHOOK=https://publisher.example.com/juejin
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Markdown Blog Kit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
MESSAGE ?= 更新博客
|
|
2
|
+
|
|
3
|
+
.DEFAULT_GOAL := help
|
|
4
|
+
|
|
5
|
+
.PHONY: help new embed skill prepare dev check build update
|
|
6
|
+
|
|
7
|
+
# Pass literal user input via environment, never interpolate it into shell code.
|
|
8
|
+
unexport TITLE SLUG DIR TOPIC URL
|
|
9
|
+
export GARDEN_NEW_TITLE := $(value TITLE)
|
|
10
|
+
export GARDEN_NEW_SLUG := $(value SLUG)
|
|
11
|
+
export GARDEN_NEW_DIR := $(value DIR)
|
|
12
|
+
export GARDEN_NEW_TOPIC := $(value TOPIC)
|
|
13
|
+
export GARDEN_EMBED_URL := $(value URL)
|
|
14
|
+
|
|
15
|
+
help:
|
|
16
|
+
@printf "%s\n" "Garden 常用命令:" " make new 创建文章草稿(无需安装 npm 依赖)" ' make new TITLE="文章标题" SLUG=my-post DIR=writing TOPIC="写作"' ' make embed URL="https://example.com" 拉取外部网页摘要,打印可粘贴的 embed 围栏' " make skill 安装可交互图表 skill" " make dev 首次准备并本地预览" " make check 检查内容与测试" " make build 构建公开站点" " make update 检查、提交并触发自动发布" " make update MESSAGE=\"新增一篇文章\" 自定义提交说明"
|
|
17
|
+
|
|
18
|
+
new:
|
|
19
|
+
@node scripts/new-post.mjs
|
|
20
|
+
|
|
21
|
+
embed:
|
|
22
|
+
@node scripts/fetch-embed.mjs
|
|
23
|
+
|
|
24
|
+
skill:
|
|
25
|
+
@node bin/garden.mjs skill
|
|
26
|
+
|
|
27
|
+
prepare:
|
|
28
|
+
@if [ ! -d node_modules ]; then npm ci; fi
|
|
29
|
+
@if [ ! -f .env.local ]; then cp .env.example .env.local; fi
|
|
30
|
+
|
|
31
|
+
dev: prepare
|
|
32
|
+
node bin/garden.mjs dev
|
|
33
|
+
|
|
34
|
+
check:
|
|
35
|
+
npm run lint && npm test
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
node bin/garden.mjs build
|
|
39
|
+
|
|
40
|
+
update:
|
|
41
|
+
@if git diff --quiet && git diff --cached --quiet && [ -z "$$(git ls-files --others --exclude-standard)" ]; then \
|
|
42
|
+
echo "没有需要发布的改动"; \
|
|
43
|
+
else \
|
|
44
|
+
$(MAKE) check && \
|
|
45
|
+
git add -A && \
|
|
46
|
+
git commit -m "$(MESSAGE)" && \
|
|
47
|
+
git push origin HEAD; \
|
|
48
|
+
fi
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Garden
|
|
2
|
+
|
|
3
|
+
用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。
|
|
4
|
+
|
|
5
|
+
站点仓库只保存稿件和身份:`content/` 与 `site.config.yaml`。页面、构建和发布工作流来自这个 Garden 包。
|
|
6
|
+
|
|
7
|
+
## 新建站点
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx hd-garden init my-blog
|
|
11
|
+
cd my-blog
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
推荐把 GitHub 仓库命名为 `<你的 GitHub 用户名>.github.io`,并在 **Settings → Pages** 把 Source 设为 **GitHub Actions**。然后修改 `site.config.yaml`,再写第一篇 Markdown。公开包名是 `hd-garden`;装进站点之后也可以用 `garden`。不要运行 `npx garden init`,npm 上的 `garden` 是别人的样式库。
|
|
16
|
+
|
|
17
|
+
也可以继续用 **Use this template**;该仓库同时是可运行的演示站。
|
|
18
|
+
|
|
19
|
+
完成后运行:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
make update
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
它会把本次改动提交并推送到源码仓库;发布工作流完成后,站点自动更新。初次使用请跟着[从零开始](docs/quick-start.md)做一遍。
|
|
26
|
+
|
|
27
|
+
## 日常写作只需要知道这些
|
|
28
|
+
|
|
29
|
+
| 你想做什么 | 从哪里开始 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 写一篇博文 | [新建文章](docs/content-authoring.md#新建一篇博文) |
|
|
32
|
+
| 添加封面、图片、代码或公式 | [丰富文章内容](docs/content-authoring.md#丰富文章内容) |
|
|
33
|
+
| 保存为草稿或置顶文章 | [草稿与置顶](docs/content-authoring.md#草稿与置顶) |
|
|
34
|
+
| 建一个主题专栏 | [创建专栏](docs/content-authoring.md#创建专栏) |
|
|
35
|
+
| 发布一条带多媒体的随想 | [发布随想](docs/content-authoring.md#发布随想) |
|
|
36
|
+
| 改网站名称、作者和一级页标题 | [站点配置](docs/site-configuration.md) |
|
|
37
|
+
| 给 AI 装可交互图表 skill | 第一次 `npx garden dev` 会自动装;更新用 `npx garden skill` |
|
|
38
|
+
| 升级 Garden 引擎 | 把 `hd-garden` 改到新版本,再 `npm install` |
|
|
39
|
+
| 发布本次改动 | `make update` |
|
|
40
|
+
|
|
41
|
+
## 常用命令
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
make new # 创建文章草稿(无需安装 npm 依赖)
|
|
45
|
+
make skill # 安装可交互图表 skill
|
|
46
|
+
make dev # 首次准备并本地预览
|
|
47
|
+
make check # 检查内容与运行测试
|
|
48
|
+
make build # 构建公开站点
|
|
49
|
+
make update # 检查、提交并触发自动发布
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
需要自定义提交说明时:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
make update MESSAGE="新增一篇文章"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`make dev` 会在首次运行时自动安装依赖并创建 `.env.local`。`make update` 会先检查,再提交并推送工作区中的所有改动。不想发布的文件请先移出仓库或加入 `.gitignore`。
|
|
59
|
+
|
|
60
|
+
## 进一步配置
|
|
61
|
+
|
|
62
|
+
首次跑通以后,再按需查看:
|
|
63
|
+
|
|
64
|
+
- [评论、Webmentions 与内容分发](docs/integrations.md)
|
|
65
|
+
- [随想管理后台](docs/admin-service.md)
|
|
66
|
+
- [GitHub Pages 与自定义域名](docs/github-pages.md)
|
|
67
|
+
- [系统怎么工作](docs/architecture.md)
|
|
68
|
+
- [全部文档](docs/README.md)
|
|
69
|
+
|
|
70
|
+
## 许可证
|
|
71
|
+
|
|
72
|
+
[MIT](LICENSE)。使用 Garden 构建的站点请保留页脚中的 Garden 署名链接。
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ArrowRight } from "@phosphor-icons/react/ssr";
|
|
3
|
+
import { GithubProfile } from "../components/GithubProfile";
|
|
4
|
+
import { PageIntro } from "../components/PageIntro";
|
|
5
|
+
import { siteConfig } from "../site.config";
|
|
6
|
+
|
|
7
|
+
const about = siteConfig.pages.about;
|
|
8
|
+
const heading = about.heading ?? "这个网站为什么存在";
|
|
9
|
+
const paragraphs = about.paragraphs ?? [
|
|
10
|
+
"我需要一个不被信息流推着走的地方。长文可以慢慢展开,随想可以轻轻放下;同一个主题也可以沿着清晰的顺序持续生长。",
|
|
11
|
+
"所有文章都从本地 Markdown 文件开始。这意味着我可以继续在 Typora 里写作,也意味着内容不会被某个编辑器或平台锁住。",
|
|
12
|
+
];
|
|
13
|
+
const quote = about.quote ?? "写作不是把已经想明白的东西记下来,而是在句子之间发现自己还没有想明白什么。";
|
|
14
|
+
const focus = about.focus ?? [
|
|
15
|
+
"将自己的关注方向写在这里",
|
|
16
|
+
"用文章积累可迁移的知识",
|
|
17
|
+
"为长期写作留出稳定的空间",
|
|
18
|
+
];
|
|
19
|
+
const cta = about.cta ?? { href: "/blog", label: "从博文开始" };
|
|
20
|
+
|
|
21
|
+
export const metadata = {
|
|
22
|
+
title: about.title,
|
|
23
|
+
description: about.subtitle,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function AboutPage() {
|
|
27
|
+
return (
|
|
28
|
+
<main className="page-shell about-page">
|
|
29
|
+
<PageIntro
|
|
30
|
+
eyebrow="ABOUT / 关于"
|
|
31
|
+
title={about.title}
|
|
32
|
+
subtitle={about.subtitle}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<div className="about-grid">
|
|
36
|
+
<section className="about-copy">
|
|
37
|
+
<h2>{heading}</h2>
|
|
38
|
+
{paragraphs.map((paragraph) => (
|
|
39
|
+
<p key={paragraph}>{paragraph}</p>
|
|
40
|
+
))}
|
|
41
|
+
<blockquote>{quote}</blockquote>
|
|
42
|
+
</section>
|
|
43
|
+
<aside className="about-facts">
|
|
44
|
+
<span>现在关注</span>
|
|
45
|
+
<ul>
|
|
46
|
+
{focus.map((item) => (
|
|
47
|
+
<li key={item}>{item}</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
<Link href={cta.href}>
|
|
51
|
+
{cta.label}
|
|
52
|
+
<ArrowRight size={16} />
|
|
53
|
+
</Link>
|
|
54
|
+
</aside>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<GithubProfile />
|
|
58
|
+
</main>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ArrowSquareOut, GithubLogo, LockKey } from "@phosphor-icons/react/ssr";
|
|
3
|
+
import { AdminDashboard } from "../components/AdminDashboard";
|
|
4
|
+
import { getAdminUser, isGitHubOAuthConfigured } from "../lib/admin-auth";
|
|
5
|
+
import { columns, posts, thoughts } from "../lib/content";
|
|
6
|
+
import { isContentRepositoryConfigured, listRepositoryThoughts } from "../lib/github-content";
|
|
7
|
+
import { getSiteUrl } from "../lib/site";
|
|
8
|
+
import { siteConfig } from "../site.config";
|
|
9
|
+
|
|
10
|
+
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
11
|
+
|
|
12
|
+
export const metadata = {
|
|
13
|
+
title: "内容管理",
|
|
14
|
+
robots: { index: false, follow: false },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default async function AdminPage({
|
|
18
|
+
searchParams,
|
|
19
|
+
}: {
|
|
20
|
+
searchParams: Promise<{ auth?: string; account?: string }>;
|
|
21
|
+
}) {
|
|
22
|
+
if (process.env.STATIC_EXPORT === "1") {
|
|
23
|
+
const adminUrl = process.env.ADMIN_URL;
|
|
24
|
+
return (
|
|
25
|
+
<main className="admin-access-page">
|
|
26
|
+
<div className="admin-access-card">
|
|
27
|
+
<LockKey size={28} weight="duotone" />
|
|
28
|
+
<p className="eyebrow">CONTENT MANAGEMENT</p>
|
|
29
|
+
<h1>内容后台独立运行。</h1>
|
|
30
|
+
<p>公开博客由 GitHub Pages 静态托管;发布和分发请前往独立后台。</p>
|
|
31
|
+
<div className="admin-access-actions">
|
|
32
|
+
{adminUrl ? (
|
|
33
|
+
<a className="admin-github-login" href={adminUrl}>
|
|
34
|
+
打开内容后台 <ArrowSquareOut size={15} />
|
|
35
|
+
</a>
|
|
36
|
+
) : (
|
|
37
|
+
<p>尚未配置 ADMIN_URL。</p>
|
|
38
|
+
)}
|
|
39
|
+
<Link href="/">返回公开博客</Link>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</main>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [user, query, publicSiteUrl] = await Promise.all([getAdminUser(), searchParams, getSiteUrl()]);
|
|
47
|
+
if (!user) {
|
|
48
|
+
const oauthReady = isGitHubOAuthConfigured();
|
|
49
|
+
return (
|
|
50
|
+
<main className="admin-access-page">
|
|
51
|
+
<div className="admin-access-card">
|
|
52
|
+
<LockKey size={28} weight="duotone" />
|
|
53
|
+
<p className="eyebrow">{siteConfig.name.toUpperCase()} / ADMIN</p>
|
|
54
|
+
<h1>用 GitHub 身份进入内容管理后台。</h1>
|
|
55
|
+
<p>
|
|
56
|
+
{query.auth === "denied"
|
|
57
|
+
? `登录账号 @${query.account ?? "未知"} 没有后台权限。`
|
|
58
|
+
: oauthReady
|
|
59
|
+
? "登录后会检查你的 GitHub 用户名是否在允许名单中。"
|
|
60
|
+
: "GitHub OAuth 尚未配置;请先设置后台所需的环境变量。"}
|
|
61
|
+
</p>
|
|
62
|
+
<div className="admin-access-actions">
|
|
63
|
+
{oauthReady && (
|
|
64
|
+
<Link className="admin-github-login" href="/api/auth/github">
|
|
65
|
+
<GithubLogo size={18} weight="fill" /> 使用 GitHub 登录
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
68
|
+
<Link href="/">
|
|
69
|
+
返回公开博客 <ArrowSquareOut size={15} />
|
|
70
|
+
</Link>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</main>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let initialThoughts: Awaited<ReturnType<typeof listRepositoryThoughts>> = [];
|
|
78
|
+
let contentWarning = "";
|
|
79
|
+
if (!isContentRepositoryConfigured()) {
|
|
80
|
+
contentWarning = "尚未配置内容仓库写入权限;请设置 CONTENT_REPOSITORY 和 CONTENT_GITHUB_TOKEN。";
|
|
81
|
+
} else {
|
|
82
|
+
try {
|
|
83
|
+
initialThoughts = await listRepositoryThoughts();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
contentWarning = error instanceof Error ? error.message : "无法读取 GitHub 内容仓库。";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const catalog = [
|
|
89
|
+
...posts.map((post) => ({
|
|
90
|
+
kind: "post" as const,
|
|
91
|
+
slug: post.slug,
|
|
92
|
+
path: post.path,
|
|
93
|
+
title: post.title,
|
|
94
|
+
topic: post.topic ?? "博文",
|
|
95
|
+
pinned: Boolean(post.pinned),
|
|
96
|
+
})),
|
|
97
|
+
...columns.map((entry) => ({
|
|
98
|
+
kind: "column" as const,
|
|
99
|
+
column: entry.column,
|
|
100
|
+
slug: entry.slug,
|
|
101
|
+
path: entry.path,
|
|
102
|
+
title: entry.title,
|
|
103
|
+
topic: entry.columnTitle ?? "专栏",
|
|
104
|
+
})),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<AdminDashboard
|
|
109
|
+
userName={`@${user.login}`}
|
|
110
|
+
initialThoughts={initialThoughts}
|
|
111
|
+
staticThoughtCount={thoughts.length}
|
|
112
|
+
catalog={catalog}
|
|
113
|
+
publicSiteUrl={publicSiteUrl}
|
|
114
|
+
contentWarning={contentWarning}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|