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,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ArrowSquareOut,
|
|
6
|
+
Heart,
|
|
7
|
+
LinkSimple,
|
|
8
|
+
Quotes,
|
|
9
|
+
Repeat,
|
|
10
|
+
} from "@phosphor-icons/react";
|
|
11
|
+
|
|
12
|
+
type WebmentionEntry = {
|
|
13
|
+
author?: {
|
|
14
|
+
name?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
photo?: string;
|
|
17
|
+
};
|
|
18
|
+
url?: string;
|
|
19
|
+
published?: string;
|
|
20
|
+
"wm-received"?: string;
|
|
21
|
+
"wm-property"?: string;
|
|
22
|
+
"wm-private"?: boolean;
|
|
23
|
+
content?: {
|
|
24
|
+
text?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type WebmentionFeed = {
|
|
29
|
+
children?: WebmentionEntry[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const reactionProperties = new Set(["like-of", "repost-of", "bookmark-of"]);
|
|
33
|
+
|
|
34
|
+
export function Webmentions({
|
|
35
|
+
target,
|
|
36
|
+
endpoint,
|
|
37
|
+
}: {
|
|
38
|
+
target: string;
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
}) {
|
|
41
|
+
const [entries, setEntries] = useState<WebmentionEntry[]>([]);
|
|
42
|
+
const [loading, setLoading] = useState(Boolean(endpoint));
|
|
43
|
+
const [failed, setFailed] = useState(false);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!endpoint) return;
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const url = new URL(endpoint);
|
|
49
|
+
url.searchParams.set("target", target);
|
|
50
|
+
|
|
51
|
+
fetch(url, {
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
headers: { Accept: "application/json" },
|
|
54
|
+
})
|
|
55
|
+
.then((response) => {
|
|
56
|
+
if (!response.ok) throw new Error("Webmentions unavailable");
|
|
57
|
+
return response.json() as Promise<WebmentionFeed>;
|
|
58
|
+
})
|
|
59
|
+
.then((feed) => {
|
|
60
|
+
setEntries(
|
|
61
|
+
(feed.children ?? []).filter(
|
|
62
|
+
(entry) => !entry["wm-private"] && entry.url,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
setFailed(false);
|
|
66
|
+
})
|
|
67
|
+
.catch((error: unknown) => {
|
|
68
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
69
|
+
setFailed(true);
|
|
70
|
+
})
|
|
71
|
+
.finally(() => setLoading(false));
|
|
72
|
+
|
|
73
|
+
return () => controller.abort();
|
|
74
|
+
}, [endpoint, target]);
|
|
75
|
+
|
|
76
|
+
const reactions = useMemo(
|
|
77
|
+
() =>
|
|
78
|
+
entries.filter((entry) =>
|
|
79
|
+
reactionProperties.has(entry["wm-property"] ?? ""),
|
|
80
|
+
),
|
|
81
|
+
[entries],
|
|
82
|
+
);
|
|
83
|
+
const responses = useMemo(
|
|
84
|
+
() =>
|
|
85
|
+
entries.filter(
|
|
86
|
+
(entry) => !reactionProperties.has(entry["wm-property"] ?? ""),
|
|
87
|
+
),
|
|
88
|
+
[entries],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<section className="webmentions" aria-labelledby="webmentions-heading">
|
|
93
|
+
<div className="webmention-title">
|
|
94
|
+
<LinkSimple size={21} />
|
|
95
|
+
<div>
|
|
96
|
+
<h2 id="webmentions-heading">来自开放网络的回应</h2>
|
|
97
|
+
<p>引用、喜欢、转发和站外回复会通过 Webmention 回到这里。</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{!endpoint && (
|
|
102
|
+
<div className="integration-notice">
|
|
103
|
+
<Quotes size={20} weight="fill" />
|
|
104
|
+
<p>接收与展示代码已经就绪,注册正式域名后即可开始收集回应。</p>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{endpoint && loading && (
|
|
109
|
+
<p className="integration-status" role="status">
|
|
110
|
+
正在读取开放网络回应…
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{endpoint && failed && (
|
|
115
|
+
<p className="integration-status" role="status">
|
|
116
|
+
暂时无法读取回应,稍后会自动恢复。
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{endpoint && !loading && !failed && entries.length === 0 && (
|
|
121
|
+
<div className="integration-notice">
|
|
122
|
+
<Quotes size={20} weight="fill" />
|
|
123
|
+
<p>还没有站外回应。任何链接到这篇文章的网站都可以向它发送 Webmention。</p>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{reactions.length > 0 && (
|
|
128
|
+
<div className="webmention-reactions">
|
|
129
|
+
<span>
|
|
130
|
+
<Heart size={16} weight="fill" />
|
|
131
|
+
<Repeat size={16} />
|
|
132
|
+
{reactions.length} 个喜欢、转发或收藏
|
|
133
|
+
</span>
|
|
134
|
+
<div className="webmention-facepile" aria-label="回应者">
|
|
135
|
+
{reactions.slice(0, 16).map((entry, index) => (
|
|
136
|
+
<a
|
|
137
|
+
href={entry.url}
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
aria-label={entry.author?.name ?? "查看站外回应"}
|
|
141
|
+
key={`${entry.url}-${index}`}
|
|
142
|
+
>
|
|
143
|
+
{entry.author?.photo ? (
|
|
144
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
145
|
+
<img
|
|
146
|
+
src={entry.author.photo}
|
|
147
|
+
alt=""
|
|
148
|
+
loading="lazy"
|
|
149
|
+
referrerPolicy="no-referrer"
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
<span>{(entry.author?.name ?? "?").slice(0, 1)}</span>
|
|
153
|
+
)}
|
|
154
|
+
</a>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{responses.length > 0 && (
|
|
161
|
+
<div className="webmention-list">
|
|
162
|
+
{responses.map((entry, index) => {
|
|
163
|
+
const received = entry.published ?? entry["wm-received"];
|
|
164
|
+
return (
|
|
165
|
+
<article key={`${entry.url}-${index}`}>
|
|
166
|
+
<header>
|
|
167
|
+
<strong>{entry.author?.name ?? "开放网络访客"}</strong>
|
|
168
|
+
{received && (
|
|
169
|
+
<time dateTime={received}>
|
|
170
|
+
{new Intl.DateTimeFormat("zh-CN", {
|
|
171
|
+
year: "numeric",
|
|
172
|
+
month: "2-digit",
|
|
173
|
+
day: "2-digit",
|
|
174
|
+
}).format(new Date(received))}
|
|
175
|
+
</time>
|
|
176
|
+
)}
|
|
177
|
+
</header>
|
|
178
|
+
{entry.content?.text && <p>{entry.content.text}</p>}
|
|
179
|
+
<a
|
|
180
|
+
href={entry.url}
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
>
|
|
184
|
+
查看原文
|
|
185
|
+
<ArrowSquareOut size={14} />
|
|
186
|
+
</a>
|
|
187
|
+
</article>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</section>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUpRight, Browser, CaretDown, FilePdf } from "@phosphor-icons/react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { parse } from "yaml";
|
|
6
|
+
|
|
7
|
+
type WidgetKind = "chart" | "html" | "pdf";
|
|
8
|
+
|
|
9
|
+
type WidgetData = {
|
|
10
|
+
src: string;
|
|
11
|
+
caption: string;
|
|
12
|
+
kind: WidgetKind;
|
|
13
|
+
height?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const widgetKinds = new Set<WidgetKind>(["chart", "html", "pdf"]);
|
|
17
|
+
|
|
18
|
+
function kindFromInput(src: string, requested: unknown): WidgetKind | null {
|
|
19
|
+
if (typeof requested === "string" && widgetKinds.has(requested.trim() as WidgetKind)) {
|
|
20
|
+
return requested.trim() as WidgetKind;
|
|
21
|
+
}
|
|
22
|
+
if (/\.pdf(?:[?#]|$)/iu.test(src)) return "pdf";
|
|
23
|
+
if (/\.html(?:[?#]|$)/iu.test(src)) return "chart";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readWidgetData(source: string): WidgetData | null {
|
|
28
|
+
try {
|
|
29
|
+
const value = parse(source.trim()) as unknown;
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
31
|
+
const input = value as Record<string, unknown>;
|
|
32
|
+
if (typeof input.src !== "string" || !input.src.trim()) return null;
|
|
33
|
+
if (typeof input.caption !== "string" || !input.caption.trim()) return null;
|
|
34
|
+
|
|
35
|
+
const src = input.src.trim();
|
|
36
|
+
if (!src.startsWith("/") || src.startsWith("//")) return null;
|
|
37
|
+
const kind = kindFromInput(src, input.kind);
|
|
38
|
+
if (!kind) return null;
|
|
39
|
+
|
|
40
|
+
const height =
|
|
41
|
+
typeof input.height === "number" && Number.isFinite(input.height) && input.height > 0
|
|
42
|
+
? Math.round(input.height)
|
|
43
|
+
: undefined;
|
|
44
|
+
return { src, caption: input.caption.trim(), kind, height };
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function themeFromDocument() {
|
|
51
|
+
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function withTheme(src: string, theme: "light" | "dark") {
|
|
55
|
+
const [path, query = ""] = src.split("?", 2);
|
|
56
|
+
const params = new URLSearchParams(query);
|
|
57
|
+
params.set("theme", theme);
|
|
58
|
+
return `${path}?${params.toString()}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fileName(src: string) {
|
|
62
|
+
const path = src.split(/[?#]/u, 1)[0] ?? src;
|
|
63
|
+
const segment = path.split("/").filter(Boolean).pop() ?? path;
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(segment);
|
|
66
|
+
} catch {
|
|
67
|
+
return segment;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function DocumentThumb({ kind }: { kind: "html" | "pdf" }) {
|
|
72
|
+
return (
|
|
73
|
+
<span className={`garden-doc-thumb garden-doc-thumb-${kind}`} aria-hidden="true">
|
|
74
|
+
<span className="garden-doc-thumb-bar" />
|
|
75
|
+
<span className="garden-doc-thumb-line" />
|
|
76
|
+
<span className="garden-doc-thumb-line" />
|
|
77
|
+
<span className="garden-doc-thumb-line garden-doc-thumb-line-short" />
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function WidgetChart({ source }: { source: string }) {
|
|
83
|
+
const data = readWidgetData(source);
|
|
84
|
+
const src = data?.src;
|
|
85
|
+
const kind = data?.kind;
|
|
86
|
+
const isDocument = kind === "html" || kind === "pdf";
|
|
87
|
+
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
88
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
89
|
+
const [height, setHeight] = useState(data?.height ?? (data?.kind === "pdf" ? 720 : 360));
|
|
90
|
+
const [expanded, setExpanded] = useState(false);
|
|
91
|
+
const [armed, setArmed] = useState(!isDocument);
|
|
92
|
+
const active = Boolean(src && kind && armed);
|
|
93
|
+
const frameSrc =
|
|
94
|
+
!active || !src
|
|
95
|
+
? undefined
|
|
96
|
+
: kind === "pdf"
|
|
97
|
+
? src
|
|
98
|
+
: withTheme(src, theme);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const root = document.documentElement;
|
|
102
|
+
const sync = () => setTheme(themeFromDocument());
|
|
103
|
+
sync();
|
|
104
|
+
const observer = new MutationObserver(sync);
|
|
105
|
+
observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
|
106
|
+
return () => observer.disconnect();
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!src || !kind || !active || kind === "pdf") return;
|
|
111
|
+
|
|
112
|
+
const onMessage = (event: MessageEvent) => {
|
|
113
|
+
const frame = frameRef.current;
|
|
114
|
+
if (!frame || event.source !== frame.contentWindow) return;
|
|
115
|
+
const payload = event.data;
|
|
116
|
+
if (
|
|
117
|
+
!payload ||
|
|
118
|
+
typeof payload !== "object" ||
|
|
119
|
+
(payload as { source?: unknown }).source !== "garden-chart" ||
|
|
120
|
+
typeof (payload as { height?: unknown }).height !== "number" ||
|
|
121
|
+
!Number.isFinite((payload as { height: number }).height)
|
|
122
|
+
) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setHeight(Math.max(160, Math.min(1600, Math.round((payload as { height: number }).height))));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
window.addEventListener("message", onMessage);
|
|
129
|
+
return () => window.removeEventListener("message", onMessage);
|
|
130
|
+
}, [src, kind, active]);
|
|
131
|
+
|
|
132
|
+
if (!data) {
|
|
133
|
+
return (
|
|
134
|
+
<p className="external-embed-error">
|
|
135
|
+
无法解析这个嵌入块。请在与 Markdown 同名的目录里放一份 .html 或 .pdf,并在围栏中写 src 与 caption。
|
|
136
|
+
</p>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const frame = (
|
|
141
|
+
<div className="garden-chart-frame" style={{ height: isDocument ? data.height : height }}>
|
|
142
|
+
<iframe
|
|
143
|
+
ref={frameRef}
|
|
144
|
+
src={frameSrc}
|
|
145
|
+
title={data.caption}
|
|
146
|
+
sandbox={data.kind === "pdf" ? undefined : "allow-scripts"}
|
|
147
|
+
referrerPolicy="no-referrer"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!isDocument) {
|
|
153
|
+
return (
|
|
154
|
+
<figure className="garden-chart">
|
|
155
|
+
{frame}
|
|
156
|
+
<figcaption>
|
|
157
|
+
<span>{data.caption}</span>
|
|
158
|
+
<a href={data.src} target="_blank" rel="noopener noreferrer">
|
|
159
|
+
打开页面
|
|
160
|
+
</a>
|
|
161
|
+
</figcaption>
|
|
162
|
+
<noscript>
|
|
163
|
+
<p>{data.caption}</p>
|
|
164
|
+
</noscript>
|
|
165
|
+
</figure>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const Icon = data.kind === "pdf" ? FilePdf : Browser;
|
|
170
|
+
const kindLabel = data.kind === "pdf" ? "PDF" : "页面";
|
|
171
|
+
const openLabel = data.kind === "pdf" ? "打开 PDF" : "打开页面";
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<figure
|
|
175
|
+
className={`garden-chart garden-doc garden-doc-${data.kind}`}
|
|
176
|
+
data-expanded={expanded ? "true" : "false"}
|
|
177
|
+
>
|
|
178
|
+
<div className="garden-doc-launch">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
className="garden-doc-toggle"
|
|
182
|
+
aria-expanded={expanded}
|
|
183
|
+
onClick={() => {
|
|
184
|
+
setExpanded((current) => {
|
|
185
|
+
const next = !current;
|
|
186
|
+
if (next) setArmed(true);
|
|
187
|
+
return next;
|
|
188
|
+
});
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<DocumentThumb kind={data.kind} />
|
|
192
|
+
<span className="garden-doc-copy">
|
|
193
|
+
<small>
|
|
194
|
+
<Icon size={15} weight="regular" />
|
|
195
|
+
{kindLabel} · {fileName(data.src)}
|
|
196
|
+
</small>
|
|
197
|
+
<strong>{data.caption}</strong>
|
|
198
|
+
</span>
|
|
199
|
+
<span className="garden-doc-action">
|
|
200
|
+
{expanded ? "收起" : "展开"}
|
|
201
|
+
<CaretDown size={14} className="garden-doc-caret" />
|
|
202
|
+
</span>
|
|
203
|
+
</button>
|
|
204
|
+
<a className="garden-doc-open" href={data.src} target="_blank" rel="noopener noreferrer">
|
|
205
|
+
{openLabel}
|
|
206
|
+
<ArrowUpRight size={14} />
|
|
207
|
+
</a>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="garden-doc-pane">
|
|
210
|
+
<div className="garden-doc-pane-inner">{armed ? frame : null}</div>
|
|
211
|
+
</div>
|
|
212
|
+
<noscript>
|
|
213
|
+
<p>
|
|
214
|
+
{data.caption}{" "}
|
|
215
|
+
<a href={data.src} target="_blank" rel="noopener noreferrer">
|
|
216
|
+
{openLabel}
|
|
217
|
+
</a>
|
|
218
|
+
</p>
|
|
219
|
+
</noscript>
|
|
220
|
+
</figure>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { ArrowRight, ArrowUpRight, GithubLogo } from "@phosphor-icons/react/ssr";
|
|
2
|
+
import { GardenAdminGuide } from "../components/GardenAdminGuide";
|
|
3
|
+
import { GardenContentGuide } from "../components/GardenContentGuide";
|
|
4
|
+
import { GardenMediaGuide } from "../components/GardenMediaGuide";
|
|
5
|
+
import { PageIntro } from "../components/PageIntro";
|
|
6
|
+
import { TableOfContents } from "../components/TableOfContents";
|
|
7
|
+
import { siteConfig } from "../site.config";
|
|
8
|
+
|
|
9
|
+
export const metadata = {
|
|
10
|
+
title: `Garden · ${siteConfig.pages.garden.title}`,
|
|
11
|
+
description:
|
|
12
|
+
"一个以 Markdown 为内容源、通过 GitHub Pages 发布的个人博客框架,支持博文、随想、专栏、搜索、RSS 与多媒体。",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const systemCapabilities = [
|
|
16
|
+
{
|
|
17
|
+
title: "Markdown 与多媒体",
|
|
18
|
+
description:
|
|
19
|
+
"兼容 Typora 常用写法,支持 GFM、代码高亮、数学公式、Mermaid,以及图片、音视频、平台嵌入和网页摘要。",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "阅读与内容发现",
|
|
23
|
+
description:
|
|
24
|
+
"自动生成文章目录、阅读时长、字数、前后篇导航、主题归档和全文搜索,搜索结果会高亮命中内容。",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "发布与订阅",
|
|
28
|
+
description:
|
|
29
|
+
"推送到 main 后由 GitHub Actions 构建并发布到 GitHub Pages,同时生成 RSS、sitemap、robots 和 Open Graph 元数据。",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "开放集成",
|
|
33
|
+
description:
|
|
34
|
+
"需要时可以接入 Giscus 评论、Webmention、无 Cookie 统计和对象存储;不配置这些服务也能正常写作和发布。",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const responsibilityBoundaries = [
|
|
39
|
+
{
|
|
40
|
+
number: "01",
|
|
41
|
+
title: "Garden 框架",
|
|
42
|
+
description:
|
|
43
|
+
"公开模板和上游代码,负责内容模型、页面生成与发布工作流。使用模板后,不需要在这里保存个人文章。",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
number: "02",
|
|
47
|
+
title: "源码仓库",
|
|
48
|
+
description:
|
|
49
|
+
"文章、随想、专栏、配置和草稿的唯一真源。你直接编辑这里,管理后台也只向这里提交内容。",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
number: "03",
|
|
53
|
+
title: "公开博客",
|
|
54
|
+
description:
|
|
55
|
+
"GitHub Actions 从源码生成静态文件,再交给 GitHub Pages。双仓模式下,公开产物仓库只保存生成结果,不手工修改。",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
number: "04",
|
|
59
|
+
title: "可选服务",
|
|
60
|
+
description:
|
|
61
|
+
"管理后台补充随想发布和媒体上传;Giscus 评论可以使用独立公开仓库。它们都不会替代源码仓库。",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const gardenHeadings = [
|
|
66
|
+
{ depth: 2, text: "仓库和服务怎么分工", id: "garden-responsibilities-title" },
|
|
67
|
+
{ depth: 2, text: "博文、随想和专栏", id: "garden-guide-title" },
|
|
68
|
+
{ depth: 3, text: "写一篇文章", id: "write-post" },
|
|
69
|
+
{ depth: 3, text: "发布一条随想", id: "write-thought" },
|
|
70
|
+
{ depth: 3, text: "创建一个专栏", id: "make-column" },
|
|
71
|
+
{ depth: 2, text: "高级嵌入", id: "garden-media-title" },
|
|
72
|
+
{ depth: 2, text: "系统能力", id: "garden-system-title" },
|
|
73
|
+
{ depth: 2, text: "管理后台", id: "garden-admin-title" },
|
|
74
|
+
{ depth: 2, text: "安装与配置", id: "garden-setup-title" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export default function GardenPage() {
|
|
78
|
+
return (
|
|
79
|
+
<main className="page-shell garden-page">
|
|
80
|
+
<div className="garden-page-toc">
|
|
81
|
+
<TableOfContents headings={gardenHeadings} label="本页目录" />
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<PageIntro
|
|
85
|
+
eyebrow="GARDEN / 博客框架"
|
|
86
|
+
title={siteConfig.pages.garden.title}
|
|
87
|
+
subtitle={siteConfig.pages.garden.subtitle}
|
|
88
|
+
>
|
|
89
|
+
<p className="garden-lede">
|
|
90
|
+
Garden 是公开的博客框架,不是你的内容仓库。使用模板后,你只在自己的源码仓库里维护
|
|
91
|
+
Markdown;推送后,GitHub Actions 会把它生成并发布为公开博客。
|
|
92
|
+
</p>
|
|
93
|
+
<div className="garden-actions">
|
|
94
|
+
<a
|
|
95
|
+
className="garden-primary-link"
|
|
96
|
+
href="https://github.com/hedon954/garden"
|
|
97
|
+
target="_blank"
|
|
98
|
+
rel="noreferrer"
|
|
99
|
+
>
|
|
100
|
+
<GithubLogo size={18} weight="fill" />
|
|
101
|
+
在 GitHub 查看源码
|
|
102
|
+
<ArrowUpRight size={16} />
|
|
103
|
+
</a>
|
|
104
|
+
<a
|
|
105
|
+
className="inline-cta"
|
|
106
|
+
href="#setup"
|
|
107
|
+
>
|
|
108
|
+
查看安装与配置
|
|
109
|
+
<ArrowRight size={16} />
|
|
110
|
+
</a>
|
|
111
|
+
</div>
|
|
112
|
+
<ul className="garden-proof-list" aria-label="Garden 核心特点">
|
|
113
|
+
<li><strong>01</strong><span>Markdown 唯一真源</span></li>
|
|
114
|
+
<li><strong>02</strong><span>GitHub 自动发布</span></li>
|
|
115
|
+
<li><strong>03</strong><span>长文、随想与专栏</span></li>
|
|
116
|
+
</ul>
|
|
117
|
+
</PageIntro>
|
|
118
|
+
|
|
119
|
+
<section
|
|
120
|
+
className="garden-responsibilities"
|
|
121
|
+
id="responsibilities"
|
|
122
|
+
aria-labelledby="garden-responsibilities-title"
|
|
123
|
+
>
|
|
124
|
+
<header className="garden-section-heading">
|
|
125
|
+
<div>
|
|
126
|
+
<p className="eyebrow">FLOW / 职责划分</p>
|
|
127
|
+
<h2 id="garden-responsibilities-title">你只需要维护源码仓库</h2>
|
|
128
|
+
</div>
|
|
129
|
+
<p>
|
|
130
|
+
日常写作从源码仓库开始,也在源码仓库结束。框架提供能力,Actions 负责构建,Pages
|
|
131
|
+
只负责展示;管理后台和评论都是可选补充。
|
|
132
|
+
</p>
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
<div className="garden-responsibility-flow" aria-label="Garden 发布链路">
|
|
136
|
+
<strong>Garden 模板</strong>
|
|
137
|
+
<ArrowRight size={16} aria-hidden="true" />
|
|
138
|
+
<strong>你的源码仓库</strong>
|
|
139
|
+
<ArrowRight size={16} aria-hidden="true" />
|
|
140
|
+
<strong>GitHub Actions</strong>
|
|
141
|
+
<ArrowRight size={16} aria-hidden="true" />
|
|
142
|
+
<strong>公开博客</strong>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="garden-responsibility-grid">
|
|
146
|
+
{responsibilityBoundaries.map((boundary) => (
|
|
147
|
+
<article key={boundary.number}>
|
|
148
|
+
<span>{boundary.number}</span>
|
|
149
|
+
<h3>{boundary.title}</h3>
|
|
150
|
+
<p>{boundary.description}</p>
|
|
151
|
+
</article>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</section>
|
|
155
|
+
|
|
156
|
+
<GardenContentGuide />
|
|
157
|
+
|
|
158
|
+
<GardenMediaGuide />
|
|
159
|
+
|
|
160
|
+
<section className="garden-system" aria-labelledby="garden-system-title">
|
|
161
|
+
<header>
|
|
162
|
+
<p className="eyebrow">SYSTEM / 系统能力</p>
|
|
163
|
+
<h2 id="garden-system-title">Markdown、搜索与自动发布</h2>
|
|
164
|
+
<p>
|
|
165
|
+
写作者只维护 Markdown、附件和专栏配置。页面生成、搜索索引和订阅文件由构建脚本统一处理。
|
|
166
|
+
</p>
|
|
167
|
+
</header>
|
|
168
|
+
<div className="garden-system-grid">
|
|
169
|
+
{systemCapabilities.map((capability, index) => (
|
|
170
|
+
<article key={capability.title}>
|
|
171
|
+
<span>{String(index + 1).padStart(2, "0")}</span>
|
|
172
|
+
<h3>{capability.title}</h3>
|
|
173
|
+
<p>{capability.description}</p>
|
|
174
|
+
</article>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
|
|
179
|
+
<GardenAdminGuide />
|
|
180
|
+
|
|
181
|
+
<section className="garden-setup" id="setup" aria-labelledby="garden-setup-title">
|
|
182
|
+
<header className="garden-section-heading">
|
|
183
|
+
<div>
|
|
184
|
+
<p className="eyebrow">SETUP / 安装与配置</p>
|
|
185
|
+
<h2 id="garden-setup-title">从模板到公开博客</h2>
|
|
186
|
+
</div>
|
|
187
|
+
<p>
|
|
188
|
+
公开博客不需要数据库和服务器。创建仓库、替换配置与内容,再推送到 main,GitHub Actions 就会完成构建和发布。
|
|
189
|
+
</p>
|
|
190
|
+
</header>
|
|
191
|
+
|
|
192
|
+
<div className="garden-setup-grid">
|
|
193
|
+
<article>
|
|
194
|
+
<span>01</span>
|
|
195
|
+
<h3>创建自己的站点</h3>
|
|
196
|
+
<p>
|
|
197
|
+
运行 <code>npx hd-garden init my-blog</code>,站点里只保留
|
|
198
|
+
<code>content/</code> 和 <code>site.config.yaml</code>。也可以继续用 GitHub 的
|
|
199
|
+
<strong>Use this template</strong>。推荐将仓库命名为
|
|
200
|
+
<code> <owner>.github.io</code>。
|
|
201
|
+
</p>
|
|
202
|
+
</article>
|
|
203
|
+
<article>
|
|
204
|
+
<span>02</span>
|
|
205
|
+
<h3>在本地启动</h3>
|
|
206
|
+
<pre>
|
|
207
|
+
<code>make dev</code>
|
|
208
|
+
</pre>
|
|
209
|
+
<p>第一次运行会自动安装依赖并创建本地配置,随后启动预览。浏览器打开终端显示的地址即可。</p>
|
|
210
|
+
</article>
|
|
211
|
+
<article>
|
|
212
|
+
<span>03</span>
|
|
213
|
+
<h3>替换配置与内容</h3>
|
|
214
|
+
<dl>
|
|
215
|
+
<div>
|
|
216
|
+
<dt>site.config.yaml</dt>
|
|
217
|
+
<dd>站点名称、作者资料和各页面标题</dd>
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<dt>content/posts/</dt>
|
|
221
|
+
<dd>长文章与附件</dd>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<dt>content/thoughts/</dt>
|
|
225
|
+
<dd>随想 Markdown</dd>
|
|
226
|
+
</div>
|
|
227
|
+
<div>
|
|
228
|
+
<dt>content/columns.yaml</dt>
|
|
229
|
+
<dd>专栏资料与文章顺序</dd>
|
|
230
|
+
</div>
|
|
231
|
+
</dl>
|
|
232
|
+
</article>
|
|
233
|
+
<article>
|
|
234
|
+
<span>04</span>
|
|
235
|
+
<h3>提交并发布</h3>
|
|
236
|
+
<pre>
|
|
237
|
+
<code>make update</code>
|
|
238
|
+
</pre>
|
|
239
|
+
<p>
|
|
240
|
+
命令会先完成检查,再提交并推送源码仓库。main 分支更新后,Actions 自动生成页面、搜索索引、RSS 和 sitemap。
|
|
241
|
+
</p>
|
|
242
|
+
</article>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="garden-setup-notes">
|
|
246
|
+
<div>
|
|
247
|
+
<strong>两种部署方式</strong>
|
|
248
|
+
<p>
|
|
249
|
+
公开源码使用单仓即可;需要隐藏草稿时,把源码放进私有仓库,并配置 <code>PAGES_REPOSITORY</code>
|
|
250
|
+
指向公开产物仓库。两种方式都只运行 <code>make update</code>。
|
|
251
|
+
</p>
|
|
252
|
+
</div>
|
|
253
|
+
<div>
|
|
254
|
+
<strong>草稿预览</strong>
|
|
255
|
+
<p>
|
|
256
|
+
本地使用 <code>CONTENT_INCLUDE_DRAFTS=1 make dev</code>,即可预览草稿和未来定时文章。
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
<div>
|
|
260
|
+
<strong>可选服务</strong>
|
|
261
|
+
<p>评论、Webmention、统计和管理后台按需配置;任何密钥都只放在部署平台的 Secret 中。</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="garden-setup-finish">
|
|
266
|
+
<div>
|
|
267
|
+
<p className="eyebrow">START / 开始使用</p>
|
|
268
|
+
<h3>先把公开博客跑起来,再决定是否接入后台。</h3>
|
|
269
|
+
</div>
|
|
270
|
+
<a
|
|
271
|
+
className="garden-primary-link"
|
|
272
|
+
href="https://github.com/hedon954/garden"
|
|
273
|
+
target="_blank"
|
|
274
|
+
rel="noreferrer"
|
|
275
|
+
>
|
|
276
|
+
<GithubLogo size={18} weight="fill" />
|
|
277
|
+
使用 Garden 模板
|
|
278
|
+
<ArrowUpRight size={16} />
|
|
279
|
+
</a>
|
|
280
|
+
</div>
|
|
281
|
+
</section>
|
|
282
|
+
</main>
|
|
283
|
+
);
|
|
284
|
+
}
|