hd-garden 0.3.12 → 0.4.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 (72) hide show
  1. package/app/a2/[slug]/page.tsx +61 -0
  2. package/app/a2/page.tsx +27 -0
  3. package/app/admin/page.tsx +11 -15
  4. package/app/admin/ui/page.tsx +1 -1
  5. package/app/blog/[...path]/page.tsx +33 -22
  6. package/app/blog/page.tsx +6 -7
  7. package/app/columns/[column]/[...path]/page.tsx +33 -25
  8. package/app/columns/page.tsx +38 -52
  9. package/app/components/AdminDashboard.tsx +24 -34
  10. package/app/components/AdminUiPanels.tsx +5 -5
  11. package/app/components/AdminUiShell.tsx +7 -7
  12. package/app/components/ArticleMeasure.tsx +95 -0
  13. package/app/components/ArticleTitle.tsx +20 -0
  14. package/app/components/BlogArchive.tsx +106 -102
  15. package/app/components/GardenAdminGuide.tsx +38 -45
  16. package/app/components/GardenContentGuide.tsx +22 -106
  17. package/app/components/GardenMediaGuide.tsx +24 -45
  18. package/app/components/MarkdownArticle.tsx +145 -66
  19. package/app/components/MermaidDiagram.tsx +44 -71
  20. package/app/components/PageIntro.tsx +2 -2
  21. package/app/components/SiteFooter.tsx +3 -7
  22. package/app/components/SiteHeader.tsx +55 -41
  23. package/app/components/TableOfContents.tsx +3 -2
  24. package/app/components/TagIndex.tsx +97 -0
  25. package/app/components/UiStudio.tsx +2 -2
  26. package/app/components/garden/GardenBlock.tsx +45 -0
  27. package/app/components/garden/blocks.tsx +530 -0
  28. package/app/components/garden/chrome.tsx +350 -0
  29. package/app/components/garden/sandbox-message.ts +16 -0
  30. package/app/garden/page.tsx +85 -223
  31. package/app/garden-modules.d.ts +2 -0
  32. package/app/globals.css +947 -88
  33. package/app/layout.tsx +9 -0
  34. package/app/lib/a2-meta.ts +22 -0
  35. package/app/lib/a2-preview.ts +50 -0
  36. package/app/lib/content.ts +48 -5
  37. package/app/lib/fallback-site-config.ts +1 -1
  38. package/app/lib/markdown-plugins.ts +2 -0
  39. package/app/lib/remark-a2.mjs +208 -0
  40. package/app/lib/remark-garden.mjs +88 -0
  41. package/app/page.tsx +122 -93
  42. package/app/styles/a2.css +1383 -0
  43. package/app/styles/shell.css +438 -0
  44. package/app/styles/tokens.css +111 -0
  45. package/app/tags/page.tsx +28 -0
  46. package/app/ui-parity/page.tsx +3 -2
  47. package/package.json +2 -2
  48. package/public/images/garden/admin-studio.png +0 -0
  49. package/scripts/build-content.mjs +11 -1
  50. package/scripts/check-post.mjs +18 -4
  51. package/scripts/new-post.mjs +3 -0
  52. package/site.config.yaml +2 -2
  53. package/templates/garden-blocks.md +128 -0
  54. package/templates/garden-snippets.plist +121 -0
  55. package/templates/site/site.config.yaml +1 -1
  56. package/ui/garden/catalog.mjs +196 -0
  57. package/ui/garden/check.mjs +103 -0
  58. package/ui/garden/enhance.mjs +89 -0
  59. package/ui/garden/parse.mjs +198 -0
  60. package/ui/garden/snippets.mjs +109 -0
  61. package/ui/garden/title.mjs +37 -0
  62. package/ui/garden/word-count.mjs +266 -0
  63. package/ui/host.mjs +45 -28
  64. package/ui/parity-fixtures.mjs +1 -1
  65. package/ui/preview/dist/garden-ui.css +81 -77
  66. package/ui/preview/dist/garden-ui.esm.js +124 -89
  67. package/ui/preview/dist/garden-ui.iife.js +124 -89
  68. package/ui/prompt/rules.md +17 -1
  69. package/ui/react/CatalogPage.tsx +12 -0
  70. package/ui/react/components/display.tsx +1 -1
  71. package/ui/react/components/media.tsx +2 -2
  72. package/ui/react/ui.css +81 -77
@@ -5,10 +5,10 @@ import { getAdminUser } from "../lib/admin-auth";
5
5
 
6
6
  const SECTIONS = [
7
7
  { href: "/admin", label: "总览" },
8
- { href: "/admin/ui", label: "组件工作台" },
9
- { href: "/admin/ui/catalog", label: "组件清单" },
10
- { href: "/admin/ui/blocks", label: "交互块" },
11
- { href: "/admin/ui/evals", label: "模型自检" },
8
+ { href: "/admin/ui", label: "写作台" },
9
+ { href: "/admin/ui/catalog", label: "组件" },
10
+ { href: "/admin/ui/blocks", label: "文章里的块" },
11
+ { href: "/admin/ui/evals", label: "检查" },
12
12
  ];
13
13
 
14
14
  export const isStaticExport = () => process.env.STATIC_EXPORT === "1";
@@ -23,8 +23,8 @@ export function StaticAdminNotice() {
23
23
  return (
24
24
  <main className="admin-access-page">
25
25
  <div className="admin-access-card">
26
- <h1>内容后台独立运行。</h1>
27
- <p>公开博客由 GitHub Pages 静态托管;这一页只在独立后台里可用。</p>
26
+ <h1>后台在另一个地址。</h1>
27
+ <p>公开博客上看不到这一页。写随想和改交互,去单独部署的后台。</p>
28
28
  <Link href="/">返回公开博客</Link>
29
29
  </div>
30
30
  </main>
@@ -35,7 +35,7 @@ export function AdminUiShell({ active, userName, children }: { active: string; u
35
35
  return (
36
36
  <main className="admin-shell admin-ui-shell">
37
37
  <header className="admin-topbar">
38
- <Link className="admin-brand" href="/admin">HEDON <span>STUDIO</span></Link>
38
+ <Link className="admin-brand" href="/admin">Garden<span>后台</span></Link>
39
39
  <div>
40
40
  <span>{userName}</span>
41
41
  <form action="/api/auth/logout" method="post">
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { tableBreakout, tocShift } from "../../ui/garden/enhance.mjs";
5
+
6
+ function translatedX(element: Element) {
7
+ const value = new DOMMatrix(getComputedStyle(element).transform).m41;
8
+ return Number.isFinite(value) ? value : 0;
9
+ }
10
+
11
+ export function ArticleMeasure() {
12
+ useEffect(() => {
13
+ const article = document.querySelector("article");
14
+ const toc = document.querySelector<HTMLElement>(".toc");
15
+ if (!article) return undefined;
16
+ const root = document.documentElement;
17
+ let frame = 0;
18
+
19
+ const layout = () => {
20
+ const articleBox = article.getBoundingClientRect();
21
+ const viewport = root.clientWidth;
22
+ article.querySelectorAll<HTMLElement>(".table-wrap").forEach((wrap) => {
23
+ const table = wrap.querySelector("table");
24
+ wrap.classList.remove("ux-wide");
25
+ wrap.style.removeProperty("--wl");
26
+ wrap.style.removeProperty("--wr");
27
+ if (!table) return;
28
+ const margin = tableBreakout({
29
+ viewport,
30
+ articleLeft: articleBox.left,
31
+ articleRight: articleBox.right,
32
+ tableClient: wrap.clientWidth,
33
+ tableScroll: table.scrollWidth,
34
+ });
35
+ if (margin.left > 0) {
36
+ wrap.style.setProperty("--wl", `${margin.left}px`);
37
+ wrap.style.setProperty("--wr", `${margin.right}px`);
38
+ wrap.classList.add("ux-wide");
39
+ }
40
+ });
41
+
42
+ if (!toc) {
43
+ root.removeAttribute("data-toc-yield");
44
+ return;
45
+ }
46
+ const box = toc.getBoundingClientRect();
47
+ const dx = translatedX(toc);
48
+ const originalLeft = box.left - dx;
49
+ const overlapRights: number[] = [];
50
+ article.querySelectorAll<HTMLElement>(".ux-wide").forEach((element) => {
51
+ const wide = element.getBoundingClientRect();
52
+ const overlaps = wide.bottom > box.top - 12 && wide.top < box.bottom + 12 && wide.right > originalLeft - 12;
53
+ if (overlaps) overlapRights.push(wide.right);
54
+ });
55
+ const shift = tocShift({
56
+ tocLeft: originalLeft,
57
+ tocWidth: box.width,
58
+ viewport,
59
+ overlapRights,
60
+ });
61
+ if (shift.shift > 0) {
62
+ root.style.setProperty("--tocshift", `${shift.shift}px`);
63
+ root.toggleAttribute("data-toc-hide", shift.hide);
64
+ root.setAttribute("data-toc-yield", "");
65
+ } else {
66
+ root.removeAttribute("data-toc-yield");
67
+ root.removeAttribute("data-toc-hide");
68
+ }
69
+ };
70
+
71
+ const schedule = () => {
72
+ if (frame) return;
73
+ frame = window.requestAnimationFrame(() => {
74
+ frame = 0;
75
+ layout();
76
+ });
77
+ };
78
+
79
+ layout();
80
+ window.addEventListener("resize", schedule);
81
+ window.addEventListener("scroll", schedule, { passive: true });
82
+ window.addEventListener("load", schedule);
83
+ const observer = new ResizeObserver(schedule);
84
+ observer.observe(article);
85
+ article.querySelectorAll("img").forEach((image) => image.addEventListener("load", schedule));
86
+ return () => {
87
+ window.removeEventListener("resize", schedule);
88
+ window.removeEventListener("scroll", schedule);
89
+ window.removeEventListener("load", schedule);
90
+ observer.disconnect();
91
+ if (frame) window.cancelAnimationFrame(frame);
92
+ };
93
+ }, []);
94
+ return null;
95
+ }
@@ -0,0 +1,20 @@
1
+ import { splitTitle, titleLineScale } from "../../ui/garden/title.mjs";
2
+
3
+ export function ArticleTitle({ title, className = "" }: { title: string; className?: string }) {
4
+ const split = splitTitle(title);
5
+ if (!split.split) {
6
+ return <h1 className={`p-name${className ? ` ${className}` : ""}`}>{title}</h1>;
7
+ }
8
+ const scale = titleLineScale(split.lines);
9
+ return (
10
+ <h1 className={`p-name h1-br${className ? ` ${className}` : ""}`} style={{ ["--l" as string]: scale }}>
11
+ <span className="h1-line">{split.lines[0]}</span>
12
+ <span className="vh">{split.separator}</span>
13
+ <span className="h1-line">{split.lines[1]}</span>
14
+ </h1>
15
+ );
16
+ }
17
+
18
+ export function originalUrl(entry: { original?: unknown }) {
19
+ return typeof entry.original === "string" && /^https?:\/\//iu.test(entry.original) ? entry.original : "";
20
+ }
@@ -2,124 +2,128 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { useSearchParams } from "next/navigation";
5
- import { ArrowLeft, ArrowRight, PushPin } from "@phosphor-icons/react";
6
- import { formatDate, postHref, posts, topics } from "../lib/content";
5
+ import { useState } from "react";
6
+ import { columns, postHref, posts, topics } from "../lib/content";
7
7
  import { ContentEmptyState } from "./ContentEmptyState";
8
8
 
9
- const pageSize = 8;
10
-
11
- export const archiveHref = (topic?: string, page?: number) => {
9
+ export const archiveHref = (topic?: string) => {
12
10
  const params = new URLSearchParams();
13
11
  if (topic) params.set("topic", topic);
14
- if (page && page > 1) params.set("page", String(page));
15
12
  const query = params.toString();
16
13
  return query ? `/blog?${query}` : "/blog";
17
14
  };
18
15
 
16
+ function day(value: string) {
17
+ const date = new Date(value);
18
+ if (Number.isNaN(date.getTime())) return value.slice(0, 10);
19
+ return new Intl.DateTimeFormat("en-CA", {
20
+ timeZone: "Asia/Shanghai",
21
+ year: "numeric",
22
+ month: "2-digit",
23
+ day: "2-digit",
24
+ }).format(date);
25
+ }
26
+
19
27
  export function BlogArchive() {
20
28
  const searchParams = useSearchParams();
29
+ const [compact, setCompact] = useState(false);
21
30
  const requestedTopic = searchParams.get("topic") ?? "";
22
31
  const selectedTopic = topics.includes(requestedTopic) ? requestedTopic : undefined;
23
- const filteredPosts = selectedTopic
24
- ? posts.filter((post) => post.topic === selectedTopic)
25
- : posts;
26
- const pageCount = Math.max(1, Math.ceil(filteredPosts.length / pageSize));
27
- const parsedPage = Number.parseInt(searchParams.get("page") ?? "1", 10);
28
- const currentPage = Math.min(
29
- pageCount,
30
- Math.max(1, Number.isFinite(parsedPage) ? parsedPage : 1),
31
- );
32
- const visiblePosts = filteredPosts.slice(
33
- (currentPage - 1) * pageSize,
34
- currentPage * pageSize,
32
+ const filtered = (selectedTopic ? posts.filter((post) => post.topic === selectedTopic) : posts)
33
+ .slice()
34
+ .sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
35
+ const years = new Map<string, typeof filtered>();
36
+ for (const post of filtered) {
37
+ const year = day(post.date).slice(0, 4) || "未知";
38
+ const list = years.get(year);
39
+ if (list) list.push(post);
40
+ else years.set(year, [post]);
41
+ }
42
+
43
+ const series = Array.from(
44
+ columns.reduce((result, entry) => {
45
+ const key = entry.column ?? "";
46
+ const list = result.get(key);
47
+ if (list) list.push(entry);
48
+ else result.set(key, [entry]);
49
+ return result;
50
+ }, new Map<string, typeof columns>()),
35
51
  );
36
52
 
37
53
  return (
38
- <>
39
- <div className="topic-strip" aria-label="文章主题">
40
- <Link
41
- href="/blog"
42
- className={!selectedTopic ? "active" : ""}
43
- aria-current={!selectedTopic ? "page" : undefined}
44
- >
45
- 全部 · {posts.length}
46
- </Link>
47
- {topics.map((topic) => (
48
- <Link
49
- href={archiveHref(topic)}
50
- className={selectedTopic === topic ? "active" : ""}
51
- aria-current={selectedTopic === topic ? "page" : undefined}
52
- key={topic}
53
- >
54
- {topic} · {posts.filter((post) => post.topic === topic).length}
55
- </Link>
56
- ))}
57
- </div>
58
-
59
- {visiblePosts.length > 0 ? (
60
- <section className="archive-list" aria-label="全部博文">
61
- {visiblePosts.map((post) => (
62
- <Link
63
- key={post.path}
64
- href={postHref(post)}
65
- className={`archive-row${post.cover ? " with-cover" : ""}`}
66
- >
67
- {post.cover && (
68
- // eslint-disable-next-line @next/next/no-img-element
69
- <img
70
- className="archive-cover"
71
- src={post.cover}
72
- alt={`${post.title} 封面`}
73
- loading="lazy"
74
- />
75
- )}
76
- <div className="archive-copy">
77
- <div className="archive-meta">
78
- <time>{formatDate(post.date)}</time>
79
- <span className="archive-topic">
80
- {post.topic}
81
- {post.pinned && (
82
- <span className="pinned-badge">
83
- <PushPin size={12} weight="fill" />
84
- 置顶
85
- </span>
86
- )}
87
- </span>
88
- </div>
89
- <h2>{post.title}</h2>
90
- <p>{post.description}</p>
91
- </div>
92
- <ArrowRight size={19} />
54
+ <div className="sh-listgrid">
55
+ <div>
56
+ <ul className="sh-filters" aria-label="按分类筛选">
57
+ <li>
58
+ <Link className="sh-chip" href="/blog" aria-current={selectedTopic ? undefined : "true"}>
59
+ 全部<span className="n">{posts.length}</span>
93
60
  </Link>
61
+ </li>
62
+ {topics.map((topic) => (
63
+ <li key={topic}>
64
+ <Link className="sh-chip" href={archiveHref(topic)} aria-current={selectedTopic === topic ? "true" : undefined}>
65
+ {topic}<span className="n">{posts.filter((post) => post.topic === topic).length}</span>
66
+ </Link>
67
+ </li>
94
68
  ))}
95
- </section>
96
- ) : (
97
- <ContentEmptyState kind="posts" />
98
- )}
99
-
100
- {pageCount > 1 && (
101
- <nav className="archive-pagination" aria-label="博文分页">
102
- {currentPage > 1 ? (
103
- <Link href={archiveHref(selectedTopic, currentPage - 1)}>
104
- <ArrowLeft size={16} />
105
- 上一页
106
- </Link>
107
- ) : (
108
- <span />
109
- )}
110
- <span>
111
- {currentPage} / {pageCount}
112
- </span>
113
- {currentPage < pageCount ? (
114
- <Link href={archiveHref(selectedTopic, currentPage + 1)}>
115
- 下一页
116
- <ArrowRight size={16} />
117
- </Link>
118
- ) : (
119
- <span />
120
- )}
121
- </nav>
122
- )}
123
- </>
69
+ </ul>
70
+ <div className="sh-status">
71
+ <span aria-live="polite">{selectedTopic ? `${selectedTopic} ${filtered.length} 篇` : `全部 ${posts.length} 篇`}</span>
72
+ {selectedTopic ? <Link href="/blog">清除筛选</Link> : null}
73
+ <div className="sh-view" role="group" aria-label="列表密度">
74
+ <button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>摘要</button>
75
+ <button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>紧凑</button>
76
+ </div>
77
+ </div>
78
+ <div id="sh-list" className={compact ? "is-compact" : undefined}>
79
+ {filtered.length ? [...years.entries()].map(([year, entries]) => (
80
+ <section className="sh-year" id={`y${year}`} key={year} aria-labelledby={`yh${year}`}>
81
+ <h2 id={`yh${year}`}>{year}<span className="n">{entries.length} 篇</span></h2>
82
+ <ol className="sh-rows">
83
+ {entries.map((post) => (
84
+ <li className="sh-row" key={post.path}>
85
+ <time className="archive-meta" dateTime={day(post.date)}>{day(post.date).slice(5)}</time>
86
+ <div className="archive-copy">
87
+ {post.cover ? (
88
+ // eslint-disable-next-line @next/next/no-img-element
89
+ <img className="archive-cover" src={post.cover} alt="" />
90
+ ) : null}
91
+ <h3>
92
+ <Link href={postHref(post)}>{post.title}</Link>
93
+ {post.pinned ? <span className="pin">置顶</span> : null}
94
+ </h3>
95
+ {post.description ? <p>{post.description}</p> : null}
96
+ </div>
97
+ {post.topic ? <Link className="cat" href={archiveHref(post.topic)}>{post.topic}</Link> : null}
98
+ </li>
99
+ ))}
100
+ </ol>
101
+ </section>
102
+ )) : <ContentEmptyState kind="posts" />}
103
+ </div>
104
+ </div>
105
+ <aside className="sh-rail" aria-label="按年份跳转">
106
+ <h2>按年份</h2>
107
+ <ol>
108
+ {[...years.keys()].map((year) => (
109
+ <li key={year}>
110
+ <a href={`#y${year}`}><span>{year}</span><span className="n num">{years.get(year)?.length ?? 0}</span></a>
111
+ </li>
112
+ ))}
113
+ </ol>
114
+ {series.length ? (
115
+ <>
116
+ <h2>专栏</h2>
117
+ <ol>
118
+ {series.map(([slug, entries]) => (
119
+ <li key={slug}>
120
+ <Link href={`/columns#${slug}`}><span>{entries[0]?.columnTitle ?? slug}</span><span className="n num">{entries.length}</span></Link>
121
+ </li>
122
+ ))}
123
+ </ol>
124
+ </>
125
+ ) : null}
126
+ </aside>
127
+ </div>
124
128
  );
125
129
  }
@@ -1,35 +1,31 @@
1
- const adminCapabilities = [
1
+ const adminJobs = [
2
2
  {
3
- number: "01",
4
- title: "发布和管理随想",
5
- description:
6
- "新建随想、保存草稿、正式发布、撤回或删除。每次修改都会写回 content/thoughts/,不会另存一份数据库副本。",
3
+ title: "写随想",
4
+ description: "保存草稿,或直接发布。文件写回 content/thoughts/,没有另一份数据库。",
7
5
  },
8
6
  {
9
- number: "02",
10
- title: "上传多媒体",
11
- description:
12
- "图片、音频和视频可以直接上传到阿里云 OSS,随想中只保存公开地址,避免把大文件塞进 Git 历史。",
7
+ title: "上传附件",
8
+ description: "图片、音频和视频可以传到对象存储。文章里只留下地址。",
13
9
  },
14
10
  {
15
- number: "03",
16
- title: "管理博文置顶",
17
- description:
18
- "后台可以切换博文的首页置顶状态,并查看博文所属专栏。正文仍然使用 Markdown 编辑,专栏顺序仍由 columns.yaml 管理。",
11
+ title: "置顶",
12
+ description: "让一篇博文出现在首页前面。正文还是在编辑器里改。",
19
13
  },
20
14
  {
21
- number: "04",
22
- title: "分发到其他平台",
23
- description:
24
- "可以直接发布到 X,或调用自建服务同步到 CSDN、知乎和掘金;没有配置接口时,也能复制标题、栏目和公开链接。",
15
+ title: "发到别处",
16
+ description: "可以发到 X,或同步到 CSDN、知乎、掘金。没配接口时,复制标题和链接。",
17
+ },
18
+ {
19
+ title: "改文章里的交互",
20
+ description: "写作台把一句话收成可交互的块。组件页能看到每一种块长什么样。",
25
21
  },
26
22
  ];
27
23
 
28
24
  const adminFlow = [
29
- ["GitHub 登录", "只允许白名单中的账号进入后台。"],
30
- ["执行内容操作", "发布随想、上传附件、置顶或分发博文。"],
31
- ["提交到内容仓库", "后台使用服务端令牌修改 Markdown。"],
32
- ["自动更新博客", "main 分支变更后由 Actions 重新发布。"],
25
+ ["用 GitHub 登录", "只有名单里的账号能进来。"],
26
+ ["做一件事", "写随想、上传、置顶,或发到别处。"],
27
+ ["写回仓库", "后台把改动提交到你的内容仓库。"],
28
+ ["网站更新", "main 变了之后,页面会重新发布。"],
33
29
  ];
34
30
 
35
31
  export function GardenAdminGuide() {
@@ -37,11 +33,10 @@ export function GardenAdminGuide() {
37
33
  <section className="garden-admin" id="admin" aria-labelledby="garden-admin-title">
38
34
  <header className="garden-section-heading">
39
35
  <div>
40
- <p className="eyebrow">ADMIN / 管理后台</p>
41
- <h2 id="garden-admin-title">管理后台能做什么</h2>
36
+ <h2 id="garden-admin-title">后台</h2>
42
37
  </div>
43
38
  <p>
44
- 后台是一个可选的独立服务,处理需要登录和服务端密钥的工作。公开博客仍然是静态站点,内容仍然保存在 Git 仓库中。
39
+ 长文继续在编辑器里写。后台只做需要登录的事:随想、附件、置顶、转发,以及文章里的交互。
45
40
  </p>
46
41
  </header>
47
42
 
@@ -50,40 +45,39 @@ export function GardenAdminGuide() {
50
45
  {/* eslint-disable-next-line @next/next/no-img-element */}
51
46
  <img
52
47
  src="/images/garden/admin-studio.png"
53
- alt="Garden 内容管理后台,包含随想发布、草稿管理与博文分发"
54
- width={1440}
55
- height={1000}
48
+ alt="Garden 后台:上面写一条随想,下面是草稿和已发布"
49
+ width={920}
50
+ height={1033}
56
51
  loading="lazy"
57
52
  />
58
53
  <figcaption>
59
- <strong>实际后台界面</strong>
60
- <span>随想编辑、已发布与草稿管理,以及博文置顶和分发都集中在同一个工作区。</span>
54
+ <strong>后台首页</strong>
55
+ <span>上面写一条随想。下面是草稿,以及可以置顶或转发的文章。</span>
61
56
  </figcaption>
62
57
  </figure>
63
58
 
64
59
  <div className="garden-admin-capabilities">
65
- {adminCapabilities.map((capability) => (
66
- <article key={capability.number}>
67
- <span>{capability.number}</span>
68
- <h3>{capability.title}</h3>
69
- <p>{capability.description}</p>
60
+ {adminJobs.map((job) => (
61
+ <article key={job.title}>
62
+ <h3>{job.title}</h3>
63
+ <p>{job.description}</p>
70
64
  </article>
71
65
  ))}
72
66
  </div>
73
67
 
74
68
  <div className="garden-admin-boundary">
75
- <strong>后台的边界</strong>
69
+ <strong>它不是整套编辑器</strong>
76
70
  <p>
77
- 它不是一套完整 CMS。长文章继续在 Typora 或其他 Markdown 编辑器中编写,专栏继续在
71
+ 长文仍用 Typora 或其他 Markdown 编辑器。专栏顺序仍在
78
72
  <code> content/columns.yaml </code>
79
- 中编排;后台主要补上随想发布、媒体上传、博文置顶和内容分发。
73
+ 里排。
80
74
  </p>
81
75
  </div>
82
76
 
83
- <ol className="garden-admin-flow" aria-label="管理后台发布流程">
77
+ <ol className="garden-admin-flow" aria-label="从登录到网站更新">
84
78
  {adminFlow.map(([title, description], index) => (
85
79
  <li key={title}>
86
- <span>{String(index + 1).padStart(2, "0")}</span>
80
+ <span>{index + 1}</span>
87
81
  <strong>{title}</strong>
88
82
  <p>{description}</p>
89
83
  </li>
@@ -92,21 +86,20 @@ export function GardenAdminGuide() {
92
86
 
93
87
  <div className="garden-admin-enable">
94
88
  <div>
95
- <p className="eyebrow">ENABLE / 启用后台</p>
96
- <h3>单独部署,再连接内容仓库</h3>
89
+ <h3>想用后台,再单独部署一份</h3>
97
90
  <ol>
98
91
  <li>把同一份 Garden 部署到 Node、Docker 或 Cloudflare Workers。</li>
99
- <li>配置 GitHub OAuth、允许登录的账号,以及仅能写入内容仓库的令牌。</li>
100
- <li>需要上传附件或跨平台分发时,再配置 OSS 和各平台接口。</li>
92
+ <li>配好 GitHub 登录、允许的账号,以及只能改内容仓库的令牌。</li>
93
+ <li>要上传附件或转发到其他网站时,再配存储和各平台接口。</li>
101
94
  </ol>
102
95
  </div>
103
- <pre aria-label="管理后台核心配置示例">
96
+ <pre aria-label="后台需要的环境变量">
104
97
  <code>{`ADMIN_URL=https://admin.example.com
105
98
  CONTENT_REPOSITORY=owner/repository
106
99
  CONTENT_BRANCH=main
107
100
  ADMIN_GITHUB_LOGINS=your-github-login
108
101
 
109
- # 令牌与密钥只保存在后台服务中
102
+ # 令牌只放在后台服务里
110
103
  CONTENT_GITHUB_TOKEN=github_pat_xxx
111
104
  GITHUB_OAUTH_CLIENT_ID=Ov23li_xxx`}</code>
112
105
  </pre>