hd-garden 0.3.11 → 0.3.13
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/app/admin/page.tsx +11 -15
- package/app/admin/ui/page.tsx +1 -1
- package/app/components/AdminDashboard.tsx +24 -34
- package/app/components/AdminUiPanels.tsx +5 -5
- package/app/components/AdminUiShell.tsx +7 -7
- package/app/components/GardenAdminGuide.tsx +38 -45
- package/app/components/GardenContentGuide.tsx +22 -106
- package/app/components/GardenMediaGuide.tsx +24 -45
- package/app/components/PageIntro.tsx +2 -2
- package/app/components/UiStudio.tsx +2 -2
- package/app/garden/page.tsx +85 -223
- package/app/globals.css +930 -76
- package/app/lib/fallback-site-config.ts +1 -1
- package/app/ui-parity/page.tsx +9 -6
- package/package.json +9 -9
- package/public/images/garden/admin-studio.png +0 -0
- package/site.config.yaml +1 -1
- package/templates/site/site.config.yaml +1 -1
- package/ui/README.md +70 -0
- package/ui/core/catalog.mjs +14 -0
- package/ui/core/static.mjs +2 -0
- package/ui/core/validate.mjs +11 -0
- package/ui/host.mjs +155 -0
- package/ui/index.mjs +80 -0
- package/ui/parity-fixtures.mjs +76 -0
- package/ui/preview/dist/garden-ui.css +54 -31
- package/ui/preview/dist/garden-ui.esm.js +172 -131
- package/ui/preview/dist/garden-ui.iife.js +172 -131
- package/ui/prompt/rules.md +1 -0
- package/ui/react/CatalogPage.tsx +37 -13
- package/ui/react/components/chart.tsx +1 -1
- package/ui/react/components/sandbox.tsx +68 -0
- package/ui/react/index.tsx +5 -3
- package/ui/react/parity.tsx +25 -0
- package/ui/react/runtime.tsx +22 -6
- package/ui/react/ui.css +54 -31
package/app/admin/page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
-
import { ArrowSquareOut, GithubLogo
|
|
2
|
+
import { ArrowSquareOut, GithubLogo } from "@phosphor-icons/react/ssr";
|
|
3
3
|
import { AdminDashboard } from "../components/AdminDashboard";
|
|
4
4
|
import { getAdminUser, isGitHubLoginAvailable } from "../lib/admin-auth";
|
|
5
5
|
import { columns, posts, thoughts } from "../lib/content";
|
|
@@ -27,17 +27,15 @@ export default async function AdminPage({
|
|
|
27
27
|
return (
|
|
28
28
|
<main className="admin-access-page">
|
|
29
29
|
<div className="admin-access-card">
|
|
30
|
-
<
|
|
31
|
-
<p
|
|
32
|
-
<h1>内容后台独立运行。</h1>
|
|
33
|
-
<p>公开博客由 GitHub Pages 静态托管;发布和分发请前往独立后台。</p>
|
|
30
|
+
<h1>后台在另一个地址。</h1>
|
|
31
|
+
<p>这个页面是公开博客。写随想、上传和转发,去后台。</p>
|
|
34
32
|
<div className="admin-access-actions">
|
|
35
33
|
{adminUrl ? (
|
|
36
34
|
<a className="admin-github-login" href={adminUrl}>
|
|
37
|
-
|
|
35
|
+
打开后台 <ArrowSquareOut size={15} />
|
|
38
36
|
</a>
|
|
39
37
|
) : (
|
|
40
|
-
<p
|
|
38
|
+
<p>还没有填写后台地址。</p>
|
|
41
39
|
)}
|
|
42
40
|
<Link href="/">返回公开博客</Link>
|
|
43
41
|
</div>
|
|
@@ -52,22 +50,20 @@ export default async function AdminPage({
|
|
|
52
50
|
return (
|
|
53
51
|
<main className="admin-access-page">
|
|
54
52
|
<div className="admin-access-card">
|
|
55
|
-
<
|
|
56
|
-
<p className="eyebrow">{siteConfig.name.toUpperCase()} / ADMIN</p>
|
|
57
|
-
<h1>用 GitHub 身份进入内容管理后台。</h1>
|
|
53
|
+
<h1>登录后台</h1>
|
|
58
54
|
<p>
|
|
59
55
|
{query.auth === "denied"
|
|
60
|
-
?
|
|
56
|
+
? `@${query.account ?? "这个账号"} 不在允许名单里。`
|
|
61
57
|
: oauthReady
|
|
62
|
-
?
|
|
58
|
+
? `用 GitHub 登录。只有 ${siteConfig.name} 允许的账号能进来。`
|
|
63
59
|
: process.env.GARDEN_DEV === "1"
|
|
64
|
-
? "
|
|
65
|
-
: "GitHub
|
|
60
|
+
? "这台机器还没登录 GitHub。在终端运行 gh auth login,然后重新启动。"
|
|
61
|
+
: "还没配好 GitHub 登录。先填后台的环境变量。"}
|
|
66
62
|
</p>
|
|
67
63
|
<div className="admin-access-actions">
|
|
68
64
|
{oauthReady && (
|
|
69
65
|
<Link className="admin-github-login" href="/api/auth/github">
|
|
70
|
-
<GithubLogo size={18} weight="fill" />
|
|
66
|
+
<GithubLogo size={18} weight="fill" /> 用 GitHub 登录
|
|
71
67
|
</Link>
|
|
72
68
|
)}
|
|
73
69
|
<Link href="/">
|
package/app/admin/ui/page.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { UiStudio } from "../../components/UiStudio";
|
|
|
3
3
|
import { modelConfig, recipeSources, remainingDrafts } from "../../lib/ui-model";
|
|
4
4
|
|
|
5
5
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
6
|
-
export const metadata = { title: "
|
|
6
|
+
export const metadata = { title: "写作台", robots: { index: false, follow: false } };
|
|
7
7
|
|
|
8
8
|
export default async function UiStudioPage() {
|
|
9
9
|
if (isStaticExport()) return <StaticAdminNotice />;
|
|
@@ -8,9 +8,6 @@ import {
|
|
|
8
8
|
ArrowUpRight,
|
|
9
9
|
CheckCircle,
|
|
10
10
|
Copy,
|
|
11
|
-
DotsThree,
|
|
12
|
-
FileText,
|
|
13
|
-
LinkSimple,
|
|
14
11
|
PaperPlaneTilt,
|
|
15
12
|
Plus,
|
|
16
13
|
Trash,
|
|
@@ -55,7 +52,7 @@ function MediaFields({
|
|
|
55
52
|
<div className="admin-field-heading">
|
|
56
53
|
<div>
|
|
57
54
|
<span>附件</span>
|
|
58
|
-
<small
|
|
55
|
+
<small>一条随想里可以混放图片、音频、视频和链接。</small>
|
|
59
56
|
</div>
|
|
60
57
|
<button type="button" className="admin-quiet-button" onClick={() => onChange([...attachments, emptyAttachment()])}>
|
|
61
58
|
<Plus size={14} /> 添加附件
|
|
@@ -153,20 +150,17 @@ function IntelligentUiCard({ ui }: { ui: UiOverview }) {
|
|
|
153
150
|
return (
|
|
154
151
|
<section className="admin-panel admin-ui-card">
|
|
155
152
|
<div className="admin-panel-heading">
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
<h2>交互块</h2>
|
|
159
|
-
</div>
|
|
160
|
-
<Link href="/admin/ui">打开工作台</Link>
|
|
153
|
+
<h2>文章里的交互</h2>
|
|
154
|
+
<Link href="/admin/ui">打开写作台</Link>
|
|
161
155
|
</div>
|
|
162
156
|
<p className="ui-studio-meta">
|
|
163
|
-
{ui.modelName ? ui.modelName : "
|
|
157
|
+
{ui.modelName ? ui.modelName : "还没接上模型"}
|
|
164
158
|
{" · "}
|
|
165
159
|
{counts
|
|
166
160
|
? <Link href="/admin/ui/blocks">{`未编译 ${counts.uncompiled} · 过期 ${counts.stale} · 有错 ${counts.error} · 正常 ${counts.ok}`}</Link>
|
|
167
|
-
: failed ? "
|
|
161
|
+
: failed ? "没读到文章里的块" : "正在查看文章…"}
|
|
168
162
|
{" · "}
|
|
169
|
-
<Link href="/admin/ui/catalog"
|
|
163
|
+
<Link href="/admin/ui/catalog">看组件</Link>
|
|
170
164
|
</p>
|
|
171
165
|
</section>
|
|
172
166
|
);
|
|
@@ -367,7 +361,7 @@ export function AdminDashboard({
|
|
|
367
361
|
return (
|
|
368
362
|
<main className="admin-shell">
|
|
369
363
|
<header className="admin-topbar">
|
|
370
|
-
<Link className="admin-brand" href="/admin">
|
|
364
|
+
<Link className="admin-brand" href="/admin">Garden<span>后台</span></Link>
|
|
371
365
|
<div>
|
|
372
366
|
<span>{userName}</span>
|
|
373
367
|
<form action="/api/auth/logout" method="post">
|
|
@@ -380,16 +374,15 @@ export function AdminDashboard({
|
|
|
380
374
|
</header>
|
|
381
375
|
|
|
382
376
|
<section className="admin-hero">
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
</dl>
|
|
377
|
+
<h1>写一条随想。</h1>
|
|
378
|
+
<p>长文仍在编辑器里改。这里写随想、上传附件、置顶,以及发到其他网站。</p>
|
|
379
|
+
<p className="admin-quiet-count">
|
|
380
|
+
{publishedCount + staticThoughtCount} 条公开随想
|
|
381
|
+
{" · "}
|
|
382
|
+
{catalogEntries.filter((entry) => entry.kind === "post").length} 篇博文
|
|
383
|
+
{" · "}
|
|
384
|
+
{catalogEntries.filter((entry) => entry.kind === "column").length} 篇专栏
|
|
385
|
+
</p>
|
|
393
386
|
</section>
|
|
394
387
|
|
|
395
388
|
<IntelligentUiCard ui={ui} />
|
|
@@ -400,12 +393,11 @@ export function AdminDashboard({
|
|
|
400
393
|
<section className="admin-grid">
|
|
401
394
|
<div className="admin-panel admin-compose-panel">
|
|
402
395
|
<div className="admin-panel-heading">
|
|
403
|
-
<
|
|
404
|
-
<PaperPlaneTilt size={22} />
|
|
396
|
+
<h2>写一条</h2>
|
|
405
397
|
</div>
|
|
406
|
-
<label>标题<input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="
|
|
407
|
-
<label
|
|
408
|
-
<label
|
|
398
|
+
<label>标题<input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="标题" /></label>
|
|
399
|
+
<label>正文<textarea value={content} onChange={(event) => setContent(event.target.value)} placeholder="写下来。可以用 Markdown。" rows={8} /></label>
|
|
400
|
+
<label>标签<input value={tags} onChange={(event) => setTags(event.target.value)} placeholder="用逗号分开,例如:夜晚, 散步" /></label>
|
|
409
401
|
<MediaFields attachments={attachments} onChange={setAttachments} onUpload={uploadMedia} uploading={uploadingCount > 0} />
|
|
410
402
|
<div className="admin-actions">
|
|
411
403
|
<button type="button" className="admin-secondary-button" disabled={isSaving || uploadingCount > 0} onClick={() => publishThought("draft")}>保存草稿</button>
|
|
@@ -415,11 +407,10 @@ export function AdminDashboard({
|
|
|
415
407
|
|
|
416
408
|
<div className="admin-panel admin-thoughts-panel">
|
|
417
409
|
<div className="admin-panel-heading">
|
|
418
|
-
<
|
|
419
|
-
<DotsThree size={23} />
|
|
410
|
+
<h2>草稿和已发布</h2>
|
|
420
411
|
</div>
|
|
421
412
|
<div className="admin-thought-list">
|
|
422
|
-
{thoughts.length === 0 ? <p className="admin-empty"
|
|
413
|
+
{thoughts.length === 0 ? <p className="admin-empty">还没有在这里写过随想。</p> : thoughts.map((thought) => (
|
|
423
414
|
<article key={thought.id}>
|
|
424
415
|
<div><span className={`admin-status ${thought.status}`}>{thought.status === "published" ? "已发布" : "草稿"}</span><h3>{thought.title}</h3><p>{thought.content.slice(0, 76)}{thought.content.length > 76 ? "…" : ""}</p></div>
|
|
425
416
|
<div className="admin-item-actions">
|
|
@@ -434,10 +425,9 @@ export function AdminDashboard({
|
|
|
434
425
|
|
|
435
426
|
<section className="admin-panel admin-distribution-panel">
|
|
436
427
|
<div className="admin-panel-heading">
|
|
437
|
-
<
|
|
438
|
-
<FileText size={22} />
|
|
428
|
+
<h2>置顶,或发到别处</h2>
|
|
439
429
|
</div>
|
|
440
|
-
<p className="admin-distribution-note"
|
|
430
|
+
<p className="admin-distribution-note">正文仍在仓库里改。这里可以置顶,发到 X,或同步到 CSDN、知乎、掘金。没配接口时,复制标题和链接。</p>
|
|
441
431
|
<div className="admin-catalog">
|
|
442
432
|
{catalogEntries.map((entry) => (
|
|
443
433
|
<article key={`${entry.kind}-${entry.column ?? ""}-${entry.path}`}>
|
|
@@ -83,9 +83,9 @@ export function AdminBlocks({ siteUrl }: { siteUrl: string }) {
|
|
|
83
83
|
return (
|
|
84
84
|
<section className="admin-panel ui-admin-blocks">
|
|
85
85
|
<div className="admin-panel-heading">
|
|
86
|
-
<
|
|
86
|
+
<h2>文章里的块</h2>
|
|
87
87
|
</div>
|
|
88
|
-
<p className="ui-studio-meta"
|
|
88
|
+
<p className="ui-studio-meta">这里是仓库里已经推送的内容,包括草稿。还没推的本地改动看不到。</p>
|
|
89
89
|
<div className="ui-studio-switch" role="group" aria-label="按状态筛选">
|
|
90
90
|
{(["todo", "uncompiled", "stale", "error", "ok", "all"] as const).map((value) => (
|
|
91
91
|
<button key={value} type="button" aria-pressed={filter === value} onClick={() => setFilter(value)}>
|
|
@@ -115,7 +115,7 @@ export function AdminBlocks({ siteUrl }: { siteUrl: string }) {
|
|
|
115
115
|
<td><span className={`ui-admin-status ui-admin-status-${block.status}`}>{STATUS_LABELS[block.status]}</span></td>
|
|
116
116
|
<td>{block.caption ?? block.intent?.split("\n")[0] ?? ""}{block.diagnostics.length ? `(${block.diagnostics.length} 条诊断)` : ""}</td>
|
|
117
117
|
<td className="ui-admin-actions">
|
|
118
|
-
<Link href={`/admin/ui?post=${encodeURIComponent(file.path)}&line=${block.startLine}`}
|
|
118
|
+
<Link href={`/admin/ui?post=${encodeURIComponent(file.path)}&line=${block.startLine}`}>在写作台打开</Link>
|
|
119
119
|
{block.status !== "ok" ? (
|
|
120
120
|
<button type="button" onClick={() => copyPrompt(file, block)}>
|
|
121
121
|
{copied === `${file.path}:${block.startLine}` ? "已复制" : "复制修复提示词"}
|
|
@@ -218,9 +218,9 @@ export function AdminEvals({ cases, configured }: { cases: EvalCase[]; configure
|
|
|
218
218
|
return (
|
|
219
219
|
<section className="admin-panel ui-admin-evals">
|
|
220
220
|
<div className="admin-panel-heading">
|
|
221
|
-
<
|
|
221
|
+
<h2>模型自检</h2>
|
|
222
222
|
</div>
|
|
223
|
-
<p className="ui-studio-meta"
|
|
223
|
+
<p className="ui-studio-meta">会逐条调用写作台,一共 {cases.length} 次,计入每小时次数。换了模型或提示词之后跑一次。</p>
|
|
224
224
|
<div className="admin-actions">
|
|
225
225
|
<button type="button" className="admin-primary-button" disabled={!configured || running} onClick={run}>{running ? `正在跑 ${results.length}/${cases.length}…` : "开始自检"}</button>
|
|
226
226
|
</div>
|
|
@@ -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
|
|
27
|
-
<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">
|
|
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">
|
|
@@ -1,35 +1,31 @@
|
|
|
1
|
-
const
|
|
1
|
+
const adminJobs = [
|
|
2
2
|
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
description:
|
|
6
|
-
"新建随想、保存草稿、正式发布、撤回或删除。每次修改都会写回 content/thoughts/,不会另存一份数据库副本。",
|
|
3
|
+
title: "写随想",
|
|
4
|
+
description: "保存草稿,或直接发布。文件写回 content/thoughts/,没有另一份数据库。",
|
|
7
5
|
},
|
|
8
6
|
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
description:
|
|
12
|
-
"图片、音频和视频可以直接上传到阿里云 OSS,随想中只保存公开地址,避免把大文件塞进 Git 历史。",
|
|
7
|
+
title: "上传附件",
|
|
8
|
+
description: "图片、音频和视频可以传到对象存储。文章里只留下地址。",
|
|
13
9
|
},
|
|
14
10
|
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
description:
|
|
18
|
-
"后台可以切换博文的首页置顶状态,并查看博文所属专栏。正文仍然使用 Markdown 编辑,专栏顺序仍由 columns.yaml 管理。",
|
|
11
|
+
title: "置顶",
|
|
12
|
+
description: "让一篇博文出现在首页前面。正文还是在编辑器里改。",
|
|
19
13
|
},
|
|
20
14
|
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
["
|
|
32
|
-
["
|
|
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
|
-
<
|
|
41
|
-
<h2 id="garden-admin-title">管理后台能做什么</h2>
|
|
36
|
+
<h2 id="garden-admin-title">后台</h2>
|
|
42
37
|
</div>
|
|
43
38
|
<p>
|
|
44
|
-
|
|
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={
|
|
55
|
-
height={
|
|
48
|
+
alt="Garden 后台:上面写一条随想,下面是草稿和已发布"
|
|
49
|
+
width={920}
|
|
50
|
+
height={1033}
|
|
56
51
|
loading="lazy"
|
|
57
52
|
/>
|
|
58
53
|
<figcaption>
|
|
59
|
-
<strong
|
|
60
|
-
<span
|
|
54
|
+
<strong>后台首页</strong>
|
|
55
|
+
<span>上面写一条随想。下面是草稿,以及可以置顶或转发的文章。</span>
|
|
61
56
|
</figcaption>
|
|
62
57
|
</figure>
|
|
63
58
|
|
|
64
59
|
<div className="garden-admin-capabilities">
|
|
65
|
-
{
|
|
66
|
-
<article key={
|
|
67
|
-
<
|
|
68
|
-
<
|
|
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
|
|
69
|
+
<strong>它不是整套编辑器</strong>
|
|
76
70
|
<p>
|
|
77
|
-
|
|
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>{
|
|
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
|
-
<
|
|
96
|
-
<h3>单独部署,再连接内容仓库</h3>
|
|
89
|
+
<h3>想用后台,再单独部署一份</h3>
|
|
97
90
|
<ol>
|
|
98
91
|
<li>把同一份 Garden 部署到 Node、Docker 或 Cloudflare Workers。</li>
|
|
99
|
-
<li
|
|
100
|
-
<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>
|
|
@@ -10,11 +10,6 @@ function SourcePanel({ path, source, language }: SourcePanelProps) {
|
|
|
10
10
|
return (
|
|
11
11
|
<div className="garden-source-panel">
|
|
12
12
|
<div className="garden-source-bar">
|
|
13
|
-
<span aria-hidden="true">
|
|
14
|
-
<i />
|
|
15
|
-
<i />
|
|
16
|
-
<i />
|
|
17
|
-
</span>
|
|
18
13
|
<code>{path}</code>
|
|
19
14
|
</div>
|
|
20
15
|
<pre>
|
|
@@ -69,12 +64,10 @@ const columnSource = `columns:
|
|
|
69
64
|
|
|
70
65
|
type Guide = {
|
|
71
66
|
id: string;
|
|
72
|
-
number: string;
|
|
73
67
|
label: string;
|
|
74
68
|
title: string;
|
|
75
69
|
introduction: string;
|
|
76
70
|
steps: ReactNode[];
|
|
77
|
-
features: string[];
|
|
78
71
|
result: string;
|
|
79
72
|
path: string;
|
|
80
73
|
source: string;
|
|
@@ -84,88 +77,45 @@ type Guide = {
|
|
|
84
77
|
const guides: Guide[] = [
|
|
85
78
|
{
|
|
86
79
|
id: "write-post",
|
|
87
|
-
number: "01",
|
|
88
80
|
label: "博文",
|
|
89
81
|
title: "写一篇文章",
|
|
90
|
-
introduction:
|
|
91
|
-
"博文保存在 content/posts/ 中,可以按照主题建立任意层级的子目录。文件路径同时决定文章地址,正文仍然是普通 Markdown。",
|
|
82
|
+
introduction: "文件放在 content/posts/。路径就是网址,正文是普通 Markdown。可以按主题分子目录。",
|
|
92
83
|
steps: [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<>在 Front Matter 中填写标题、日期、摘要、主题、标签和封面。</>,
|
|
97
|
-
<>使用 Markdown 编写正文,图片和其他附件可以使用相对路径。</>,
|
|
84
|
+
<>在 <code>content/posts/</code> 里新建一个 <code>.md</code> 文件。</>,
|
|
85
|
+
<>开头写下标题、日期、摘要、主题和标签。封面可有可无。</>,
|
|
86
|
+
<>接着写正文。图片用相对路径,和文件放在一起。</>,
|
|
98
87
|
],
|
|
99
|
-
|
|
100
|
-
"GFM 表格、任务列表与脚注",
|
|
101
|
-
"TypeScript、Go、Rust 等代码高亮",
|
|
102
|
-
"数学公式与 Mermaid 图表",
|
|
103
|
-
"图片、音视频与平台嵌入",
|
|
104
|
-
"草稿、定时发布与置顶",
|
|
105
|
-
"文章目录、上一篇与下一篇",
|
|
106
|
-
],
|
|
107
|
-
result:
|
|
108
|
-
"构建时会生成文章页面,并把标题、摘要和正文加入博文列表、主题归档、全文搜索与 RSS。",
|
|
88
|
+
result: "推送后,这篇文章会出现在列表、主题、搜索和 RSS 里。",
|
|
109
89
|
path: "content/posts/writing/first-post.md",
|
|
110
90
|
source: postSource,
|
|
111
91
|
language: "markdown",
|
|
112
92
|
},
|
|
113
93
|
{
|
|
114
94
|
id: "write-thought",
|
|
115
|
-
number: "02",
|
|
116
95
|
label: "随想",
|
|
117
|
-
title: "
|
|
118
|
-
introduction:
|
|
119
|
-
"随想适合记录几句话、一个链接或一组多媒体。它同样保存在 Git 仓库中,只需要更少的字段,也可以通过可选管理后台发布。",
|
|
96
|
+
title: "写一条随想",
|
|
97
|
+
introduction: "几句话、一张图或一个链接。字段更少。也可以在后台写,写完同样进仓库。",
|
|
120
98
|
steps: [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<>文件名就是地址。front matter 填写标题和日期;需要暂存时加入草稿状态。</>,
|
|
125
|
-
<>
|
|
126
|
-
在 <code>media</code> 中按顺序添加图片、音频、视频或外部链接。
|
|
127
|
-
</>,
|
|
128
|
-
],
|
|
129
|
-
features: [
|
|
130
|
-
"纯文字短内容",
|
|
131
|
-
"多张图片与混合图集",
|
|
132
|
-
"音频和视频",
|
|
133
|
-
"带标题的外部链接",
|
|
134
|
-
"草稿与公开状态",
|
|
135
|
-
"独立永久链接",
|
|
99
|
+
<>新建 <code>content/thoughts/文件名.md</code>,或在后台填写。</>,
|
|
100
|
+
<>文件名就是地址。标题和日期写在开头。先存着就标成草稿。</>,
|
|
101
|
+
<>图片、音频、视频和链接按顺序放进 <code>media</code>。</>,
|
|
136
102
|
],
|
|
137
|
-
result:
|
|
138
|
-
"发布后的内容会出现在随想时间线和首页最近随想中,每条随想都有可以单独分享的永久链接。",
|
|
103
|
+
result: "它会出现在随想页和首页,并且有自己的链接。",
|
|
139
104
|
path: "content/thoughts/walk-after-rain.md",
|
|
140
105
|
source: thoughtSource,
|
|
141
106
|
language: "markdown",
|
|
142
107
|
},
|
|
143
108
|
{
|
|
144
109
|
id: "make-column",
|
|
145
|
-
number: "03",
|
|
146
110
|
label: "专栏",
|
|
147
|
-
title: "
|
|
148
|
-
introduction:
|
|
149
|
-
"专栏不复制文章,而是在 content/columns.yaml 中引用已经存在的博文。posts 的排列顺序就是专栏的阅读顺序。",
|
|
111
|
+
title: "把文章收成专栏",
|
|
112
|
+
introduction: "专栏不复制文章。它只是 content/columns.yaml 里的一份阅读顺序。",
|
|
150
113
|
steps: [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<>
|
|
155
|
-
在 <code>posts</code> 中填写博文相对 <code>content/posts/</code> 的路径。
|
|
156
|
-
</>,
|
|
157
|
-
<>按照希望读者阅读的顺序排列路径;同一篇文章可以加入多个专栏。</>,
|
|
158
|
-
],
|
|
159
|
-
features: [
|
|
160
|
-
"专栏标题、说明和状态",
|
|
161
|
-
"独立专栏封面",
|
|
162
|
-
"文章顺序可控",
|
|
163
|
-
"复用同一份 Markdown",
|
|
164
|
-
"一篇文章加入多个专栏",
|
|
165
|
-
"系列文章导航",
|
|
114
|
+
<>在 <code>content/columns.yaml</code> 里写名称、介绍和封面。</>,
|
|
115
|
+
<>在 <code>posts</code> 里填文章相对 <code>content/posts/</code> 的路径。</>,
|
|
116
|
+
<>按你希望读者阅读的顺序排列。同一篇可以进多个专栏。</>,
|
|
166
117
|
],
|
|
167
|
-
result:
|
|
168
|
-
"构建后会生成独立专栏页面。读者可以按照配置顺序浏览文章,文章页也会显示它所属的系列与前后篇目。",
|
|
118
|
+
result: "读者按这个顺序读。文章页也会显示它属于哪一组,以及上一篇、下一篇。",
|
|
169
119
|
path: "content/columns.yaml",
|
|
170
120
|
source: columnSource,
|
|
171
121
|
language: "yaml",
|
|
@@ -174,56 +124,22 @@ const guides: Guide[] = [
|
|
|
174
124
|
|
|
175
125
|
export function GardenContentGuide() {
|
|
176
126
|
return (
|
|
177
|
-
<
|
|
178
|
-
<header className="garden-guide-heading">
|
|
179
|
-
<div>
|
|
180
|
-
<p className="eyebrow">CONTENT / 内容</p>
|
|
181
|
-
<h2 id="garden-guide-title">博文、随想和专栏</h2>
|
|
182
|
-
</div>
|
|
183
|
-
<p>
|
|
184
|
-
先选择内容类型,再编辑对应文件。三种内容都保存在源码仓库中,直接编辑文件即可完成全部公开内容的维护和发布。
|
|
185
|
-
</p>
|
|
186
|
-
</header>
|
|
187
|
-
|
|
188
|
-
<nav className="garden-guide-index" aria-label="内容类型">
|
|
189
|
-
{guides.map((guide) => (
|
|
190
|
-
<a href={`#${guide.id}`} key={guide.id}>
|
|
191
|
-
<span>{guide.number}</span>
|
|
192
|
-
<strong>{guide.title}</strong>
|
|
193
|
-
<small>{guide.label}</small>
|
|
194
|
-
</a>
|
|
195
|
-
))}
|
|
196
|
-
</nav>
|
|
197
|
-
|
|
127
|
+
<div className="garden-guide">
|
|
198
128
|
<div className="garden-guide-sections">
|
|
199
129
|
{guides.map((guide) => (
|
|
200
130
|
<article className="garden-content-guide" id={guide.id} key={guide.id}>
|
|
201
131
|
<div className="garden-content-copy">
|
|
202
|
-
<p className="
|
|
203
|
-
{guide.number} / {guide.label}
|
|
204
|
-
</p>
|
|
132
|
+
<p className="garden-content-label">{guide.label}</p>
|
|
205
133
|
<h2>{guide.title}</h2>
|
|
206
134
|
<p className="garden-content-introduction">{guide.introduction}</p>
|
|
207
135
|
|
|
208
136
|
<ol className="garden-content-steps">
|
|
209
137
|
{guide.steps.map((step, index) => (
|
|
210
|
-
<li key={index}>
|
|
211
|
-
<strong>{index + 1}</strong>
|
|
212
|
-
<span>{step}</span>
|
|
213
|
-
</li>
|
|
138
|
+
<li key={index}>{step}</li>
|
|
214
139
|
))}
|
|
215
140
|
</ol>
|
|
216
141
|
|
|
217
|
-
<
|
|
218
|
-
{guide.features.map((feature) => (
|
|
219
|
-
<li key={feature}>{feature}</li>
|
|
220
|
-
))}
|
|
221
|
-
</ul>
|
|
222
|
-
|
|
223
|
-
<aside className="garden-content-result">
|
|
224
|
-
<strong>发布后</strong>
|
|
225
|
-
<p>{guide.result}</p>
|
|
226
|
-
</aside>
|
|
142
|
+
<p className="garden-content-result">{guide.result}</p>
|
|
227
143
|
</div>
|
|
228
144
|
|
|
229
145
|
<SourcePanel
|
|
@@ -234,6 +150,6 @@ export function GardenContentGuide() {
|
|
|
234
150
|
</article>
|
|
235
151
|
))}
|
|
236
152
|
</div>
|
|
237
|
-
</
|
|
153
|
+
</div>
|
|
238
154
|
);
|
|
239
155
|
}
|