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.
- package/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/admin/page.tsx +11 -15
- package/app/admin/ui/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +33 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +33 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/AdminDashboard.tsx +24 -34
- package/app/components/AdminUiPanels.tsx +5 -5
- package/app/components/AdminUiShell.tsx +7 -7
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/GardenAdminGuide.tsx +38 -45
- package/app/components/GardenContentGuide.tsx +22 -106
- package/app/components/GardenMediaGuide.tsx +24 -45
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/PageIntro.tsx +2 -2
- package/app/components/SiteFooter.tsx +3 -7
- package/app/components/SiteHeader.tsx +55 -41
- package/app/components/TableOfContents.tsx +3 -2
- package/app/components/TagIndex.tsx +97 -0
- package/app/components/UiStudio.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +45 -0
- package/app/components/garden/blocks.tsx +530 -0
- package/app/components/garden/chrome.tsx +350 -0
- package/app/components/garden/sandbox-message.ts +16 -0
- package/app/garden/page.tsx +85 -223
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +947 -88
- package/app/layout.tsx +9 -0
- package/app/lib/a2-meta.ts +22 -0
- package/app/lib/a2-preview.ts +50 -0
- package/app/lib/content.ts +48 -5
- package/app/lib/fallback-site-config.ts +1 -1
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +2 -2
- package/public/images/garden/admin-studio.png +0 -0
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +2 -2
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/templates/site/site.config.yaml +1 -1
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- 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
|
|
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">
|
|
@@ -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 {
|
|
6
|
-
import {
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { columns, postHref, posts, topics } from "../lib/content";
|
|
7
7
|
import { ContentEmptyState } from "./ContentEmptyState";
|
|
8
8
|
|
|
9
|
-
const
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
</
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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>
|