hd-garden 0.9.3 → 0.10.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/components/SiteChrome.tsx +3 -17
- package/app/components/SiteHeader.tsx +3 -3
- package/app/garden-modules.d.ts +0 -6
- package/app/layout.tsx +1 -2
- package/app/lib/i18n.ts +2 -12
- package/app/sitemap.ts +0 -11
- package/app/styles/shell.css +0 -37
- package/docs/handbook/config.md +2 -59
- package/docs/handbook/en/config.md +2 -59
- package/package.json +1 -2
- package/scripts/build-content.mjs +1 -31
- package/scripts/handbook-facts.mjs +1 -2
- package/ui/garden/site-nav.mjs +0 -12
- package/vite.config.ts +3 -12
- package/app/bookshelf/[bookId]/page.tsx +0 -55
- package/app/bookshelf/load.mjs +0 -114
- package/app/bookshelf/load.ts +0 -16
- package/app/bookshelf/page.tsx +0 -52
- package/app/components/bookshelf/BookHub.tsx +0 -142
- package/app/components/bookshelf/Bookshelf.tsx +0 -128
- package/app/components/bookshelf/bookshelf.css +0 -1334
- package/app/components/bookshelf/catalog.mjs +0 -176
- package/app/components/bookshelf/catalog.ts +0 -27
- package/app/components/bookshelf/demo-data.ts +0 -93
- package/app/components/bookshelf/density.ts +0 -32
- package/app/components/bookshelf/engine.ts +0 -2773
- package/app/components/bookshelf/index.ts +0 -15
- package/app/components/bookshelf/model.ts +0 -16
- package/app/components/bookshelf/schema.mjs +0 -155
- package/app/components/bookshelf/schema.ts +0 -110
- package/app/components/bookshelf/types.ts +0 -89
- package/public/covers/accelerate.jpg +0 -0
- package/public/covers/clean-architecture.jpg +0 -0
- package/public/covers/clean-code.jpg +0 -0
- package/public/covers/continuous-delivery.jpg +0 -0
- package/public/covers/ddd.jpg +0 -0
- package/public/covers/ddia.jpg +0 -0
- package/public/covers/design-patterns.jpg +0 -0
- package/public/covers/eaa.jpg +0 -0
- package/public/covers/fundamentals-sa.jpg +0 -0
- package/public/covers/implementing-ddd.jpg +0 -0
- package/public/covers/lean-startup.jpg +0 -0
- package/public/covers/microservices.jpg +0 -0
- package/public/covers/mythical-man-month.jpg +0 -0
- package/public/covers/phoenix-project.jpg +0 -0
- package/public/covers/pragmatic-programmer.jpg +0 -0
- package/public/covers/refactoring.jpg +0 -0
- package/public/covers/release-it.jpg +0 -0
- package/public/covers/sre.jpg +0 -0
- package/public/covers/systems-performance.jpg +0 -0
- package/public/covers/tdd.jpg +0 -0
- package/public/covers/team-topologies.jpg +0 -0
- package/scripts/bookshelf-covers.mjs +0 -62
|
@@ -5,14 +5,7 @@ import { inAppPath, isHandbookPath, routeLang, ui } from "../lib/i18n";
|
|
|
5
5
|
import { SiteFooter } from "./SiteFooter";
|
|
6
6
|
import { SiteHeader } from "./SiteHeader";
|
|
7
7
|
|
|
8
|
-
export function SiteChrome({
|
|
9
|
-
children,
|
|
10
|
-
bookshelfSite = false,
|
|
11
|
-
}: {
|
|
12
|
-
children: React.ReactNode;
|
|
13
|
-
/** True when the site has content/bookshelf.yaml. The package demo stays chrome-less. */
|
|
14
|
-
bookshelfSite?: boolean;
|
|
15
|
-
}) {
|
|
8
|
+
export function SiteChrome({ children }: { children: React.ReactNode }) {
|
|
16
9
|
const pathname = usePathname();
|
|
17
10
|
const lang = routeLang(pathname);
|
|
18
11
|
// The skip link follows the route language too (/garden/en/ is English).
|
|
@@ -21,19 +14,12 @@ export function SiteChrome({
|
|
|
21
14
|
{ui(lang).skipToContent}
|
|
22
15
|
</a>
|
|
23
16
|
);
|
|
24
|
-
|
|
25
|
-
// A site shelf is a normal page, so it keeps the header and footer.
|
|
26
|
-
// Static export serves /bookshelf/ ; an exact "/bookshelf" check would miss it.
|
|
27
|
-
const path = inAppPath(pathname);
|
|
28
|
-
const shelfDemo = !bookshelfSite && (path === "/bookshelf" || path.startsWith("/bookshelf/"));
|
|
29
|
-
if (path.startsWith("/admin") || shelfDemo) {
|
|
30
|
-
return <>{skip}{children}</>;
|
|
31
|
-
}
|
|
17
|
+
if (inAppPath(pathname).startsWith("/admin")) return <>{skip}{children}</>;
|
|
32
18
|
|
|
33
19
|
return (
|
|
34
20
|
<>
|
|
35
21
|
{skip}
|
|
36
|
-
<SiteHeader
|
|
22
|
+
<SiteHeader />
|
|
37
23
|
<div id="main-content">{children}</div>
|
|
38
24
|
<SiteFooter docs={isHandbookPath(pathname)} lang={lang} />
|
|
39
25
|
</>
|
|
@@ -16,7 +16,7 @@ import { brand, searchFull, searchIndexUrl } from "@garden/site-config";
|
|
|
16
16
|
import { createSearch } from "../../ui/garden/search-match.mjs";
|
|
17
17
|
import { cropMatchedText, highlightParts } from "../../ui/garden/search-preview.mjs";
|
|
18
18
|
import { resolveDark, themeSwitchProps } from "../../ui/garden/theme.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { SITE_NAV, isCurrentNav } from "../../ui/garden/site-nav.mjs";
|
|
20
20
|
import { isHandbookPath, routeLang, ui } from "../lib/i18n";
|
|
21
21
|
import { HandbookLangSwitch } from "./HandbookLangSwitch";
|
|
22
22
|
import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
|
|
@@ -89,6 +89,7 @@ function loadSearchEngine() {
|
|
|
89
89
|
return engineLoad;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
const nav = SITE_NAV.map((item) => ({ ...item, key: item.href, href: siteHref(item.href) }));
|
|
92
93
|
|
|
93
94
|
function rangesFor(
|
|
94
95
|
matches: readonly SearchMatch[] | undefined,
|
|
@@ -139,8 +140,7 @@ function searchPreview(
|
|
|
139
140
|
return { prefix: "", text: item.description, suffix: "", ranges: [] };
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
export function SiteHeader(
|
|
143
|
-
const nav = siteNav({ shelf }).map((item) => ({ ...item, key: item.href, href: siteHref(item.href) }));
|
|
143
|
+
export function SiteHeader() {
|
|
144
144
|
const pathname = usePathname() || "/";
|
|
145
145
|
const t = ui(routeLang(pathname));
|
|
146
146
|
const [menuOpen, setMenuOpen] = useState(false);
|
package/app/garden-modules.d.ts
CHANGED
|
@@ -103,9 +103,3 @@ declare module "@garden/site-config" {
|
|
|
103
103
|
};
|
|
104
104
|
export const githubUrl: string;
|
|
105
105
|
}
|
|
106
|
-
|
|
107
|
-
declare module "@garden/bookshelf" {
|
|
108
|
-
import type { BookshelfMount } from "./components/bookshelf/catalog";
|
|
109
|
-
/** Site shelf from content/bookshelf.yaml, or null when that file is absent. */
|
|
110
|
-
export const bookshelf: BookshelfMount | null;
|
|
111
|
-
}
|
package/app/layout.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import "./globals.css";
|
|
|
4
4
|
import "./styles/tokens.css";
|
|
5
5
|
import "./styles/a2.css";
|
|
6
6
|
import "./styles/shell.css";
|
|
7
|
-
import { bookshelf } from "@garden/bookshelf";
|
|
8
7
|
import { SiteChrome } from "./components/SiteChrome";
|
|
9
8
|
import {
|
|
10
9
|
getSiteUrl,
|
|
@@ -95,7 +94,7 @@ export default function RootLayout({
|
|
|
95
94
|
)}
|
|
96
95
|
</head>
|
|
97
96
|
<body>
|
|
98
|
-
<SiteChrome
|
|
97
|
+
<SiteChrome>{children}</SiteChrome>
|
|
99
98
|
</body>
|
|
100
99
|
</html>
|
|
101
100
|
);
|
package/app/lib/i18n.ts
CHANGED
|
@@ -131,7 +131,7 @@ const zh = {
|
|
|
131
131
|
fuzzy: "支持模糊匹配",
|
|
132
132
|
escToClose: "关闭",
|
|
133
133
|
noResults: (q: string) => (q ? `没有找到和「${q}」相关的文章` : "还没有可以搜索的内容"),
|
|
134
|
-
navLabels: { "/blog/": "文章", "/thoughts/": "随想", "/columns/": "专栏", "/tags/": "标签", "/
|
|
134
|
+
navLabels: { "/blog/": "文章", "/thoughts/": "随想", "/columns/": "专栏", "/tags/": "标签", "/about/": "关于" } as Record<string, string>,
|
|
135
135
|
// Home page
|
|
136
136
|
homeLatestBadge: "最新文章",
|
|
137
137
|
homeRecentHeading: "最近更新",
|
|
@@ -299,11 +299,6 @@ const zh = {
|
|
|
299
299
|
adminTitlePreview: "草稿预览",
|
|
300
300
|
a2IndexTitle: "A2 对照",
|
|
301
301
|
a2IndexDeck: "对照参考文章的版式。正文没有改字。",
|
|
302
|
-
bookHubCurrent: "本书",
|
|
303
|
-
bookHubNotes: "我的笔记",
|
|
304
|
-
bookHubOverview: "总览",
|
|
305
|
-
bookHubExcerpt: "摘抄",
|
|
306
|
-
bookHubPdfNote: "(仅分享,侵权删)",
|
|
307
302
|
};
|
|
308
303
|
|
|
309
304
|
const en: typeof zh = {
|
|
@@ -419,7 +414,7 @@ const en: typeof zh = {
|
|
|
419
414
|
fuzzy: "Fuzzy matching",
|
|
420
415
|
escToClose: "to close",
|
|
421
416
|
noResults: (q: string) => (q ? `No posts match “${q}”` : "Nothing to search yet"),
|
|
422
|
-
navLabels: { "/blog/": "Posts", "/thoughts/": "Thoughts", "/columns/": "Columns", "/tags/": "Tags", "/
|
|
417
|
+
navLabels: { "/blog/": "Posts", "/thoughts/": "Thoughts", "/columns/": "Columns", "/tags/": "Tags", "/about/": "About" },
|
|
423
418
|
// Home page
|
|
424
419
|
homeLatestBadge: "Latest post",
|
|
425
420
|
homeRecentHeading: "Recent updates",
|
|
@@ -587,11 +582,6 @@ const en: typeof zh = {
|
|
|
587
582
|
adminTitlePreview: "Draft Preview",
|
|
588
583
|
a2IndexTitle: "A2 Parity",
|
|
589
584
|
a2IndexDeck: "Layout comparison against reference articles. Body text is unchanged.",
|
|
590
|
-
bookHubCurrent: "This book",
|
|
591
|
-
bookHubNotes: "My notes",
|
|
592
|
-
bookHubOverview: "Overview",
|
|
593
|
-
bookHubExcerpt: "Excerpt",
|
|
594
|
-
bookHubPdfNote: "(shared only — removed if it infringes)",
|
|
595
585
|
};
|
|
596
586
|
|
|
597
587
|
export const UI_STRINGS = { zh, en } as const;
|
package/app/sitemap.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next";
|
|
2
|
-
import { bookshelf } from "@garden/bookshelf";
|
|
3
2
|
import { columnHref, columns, postHref, posts, thoughtHref } from "./lib/content";
|
|
4
3
|
import { absoluteUrl, getSiteUrl } from "./lib/site";
|
|
5
4
|
import { getPublishedThoughts } from "./lib/public-thoughts";
|
|
6
5
|
import { siteHref } from "./lib/site-href";
|
|
7
|
-
import type { BookshelfBook, BookshelfShelf } from "./components/bookshelf/types";
|
|
8
6
|
|
|
9
7
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
10
8
|
const baseUrl = await getSiteUrl();
|
|
@@ -16,15 +14,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
16
14
|
{ path: "/columns", priority: 0.8, changeFrequency: "weekly" as const },
|
|
17
15
|
{ path: "/about", priority: 0.5, changeFrequency: "monthly" as const },
|
|
18
16
|
{ path: "/garden", priority: 0.4, changeFrequency: "monthly" as const },
|
|
19
|
-
// Only a real shelf is a page. The package demo stays out of the sitemap.
|
|
20
|
-
...(bookshelf ? [{ path: "/bookshelf", priority: 0.6, changeFrequency: "weekly" as const }] : []),
|
|
21
|
-
...(bookshelf
|
|
22
|
-
? bookshelf.catalog.flatMap((row: BookshelfShelf) =>
|
|
23
|
-
row.books
|
|
24
|
-
.filter((book: BookshelfBook) => typeof book.hub === "string")
|
|
25
|
-
.map((book: BookshelfBook) => ({ path: book.hub ?? "/bookshelf", priority: 0.55, changeFrequency: "weekly" as const })),
|
|
26
|
-
)
|
|
27
|
-
: []),
|
|
28
17
|
].map((route) => ({
|
|
29
18
|
url: absoluteUrl(siteHref(route.path), baseUrl),
|
|
30
19
|
changeFrequency: route.changeFrequency,
|
package/app/styles/shell.css
CHANGED
|
@@ -434,43 +434,6 @@ html[data-theme="dark"] .sh-repos > li.sk::after{background:linear-gradient(90de
|
|
|
434
434
|
.sh-series.no-cover h2,.sh-series.no-cover .count{grid-column:1 / -1;grid-row:auto}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
/* Book hub (本书), locked to tip book-multi-20261029c. Width is fixed; height follows the jacket. */
|
|
438
|
-
.book-hub{width:min(100%,740px);padding:40px 0 72px}
|
|
439
|
-
.book-hub-crumb{margin:0 0 28px;font-size:12px;color:var(--faint,#a09b91);letter-spacing:.06em}
|
|
440
|
-
.book-hub-crumb a{color:inherit;text-decoration:none}
|
|
441
|
-
.book-hub-crumb a:hover{color:var(--ink)}
|
|
442
|
-
.book-hub-crumb [aria-current="page"]{color:var(--faint,#a09b91)}
|
|
443
|
-
.book-hub-card{display:flex;gap:28px;align-items:flex-start;margin:0 0 40px}
|
|
444
|
-
.book-hub-cover{width:148px;flex-shrink:0;height:auto;min-height:0;aspect-ratio:auto;border-radius:0;overflow:visible;background:none;box-shadow:none;line-height:0;padding:0}
|
|
445
|
-
.book-hub-cover img{display:block;width:148px;max-width:none;height:auto;aspect-ratio:auto;object-fit:unset;background:none;border-radius:4px;box-shadow:0 10px 24px rgba(40,30,20,.10)}
|
|
446
|
-
.book-hub-meta{flex:1;min-width:0;padding-top:4px}
|
|
447
|
-
.book-hub-meta h1{margin:0 0 10px;font-family:var(--serif);font-size:clamp(26px,4vw,34px);font-weight:600;line-height:1.25;letter-spacing:-.01em;color:var(--ink)}
|
|
448
|
-
.book-hub-author{margin:0 0 18px;font-size:14px;color:var(--muted)}
|
|
449
|
-
.book-hub-sources{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
450
|
-
.book-hub-source{display:inline-flex;align-items:center;gap:2px}
|
|
451
|
-
.book-hub-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:999px;background:#f3efe6;color:var(--muted);font-size:12px;line-height:1.4;text-decoration:none;border:1px solid transparent}
|
|
452
|
-
html[data-theme="dark"] .book-hub-chip{background:var(--chip)}
|
|
453
|
-
.book-hub-chip:hover{color:var(--ink);border-color:var(--rule)}
|
|
454
|
-
.book-hub-chip .ext{font-size:11px;opacity:.7}
|
|
455
|
-
.book-hub-source-note{font-size:11px;color:var(--faint,#a09b91);letter-spacing:.02em;line-height:1.4}
|
|
456
|
-
.book-hub-notes-head{margin:0 0 14px;font:400 12px/1.4 var(--sans);color:var(--faint,#a09b91);letter-spacing:.12em}
|
|
457
|
-
.book-hub-notes{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}
|
|
458
|
-
.book-hub-note{display:block;width:100%;text-align:left;padding:18px 20px;background:#fffcf7;border:1px solid var(--rule);border-radius:10px;text-decoration:none;color:inherit}
|
|
459
|
-
html[data-theme="dark"] .book-hub-note{background:var(--card)}
|
|
460
|
-
@media (hover:hover){
|
|
461
|
-
.book-hub-note:hover{border-color:#d5d0c4;box-shadow:0 6px 16px rgba(40,30,20,.05)}
|
|
462
|
-
html[data-theme="dark"] .book-hub-note:hover{border-color:var(--rule-strong);box-shadow:none}
|
|
463
|
-
}
|
|
464
|
-
.book-hub-note-label{margin:0 0 6px;font-size:11px;color:var(--faint,#a09b91);letter-spacing:.08em}
|
|
465
|
-
.book-hub-note h3{margin:0 0 8px;font:500 16px/1.45 var(--sans);color:var(--ink)}
|
|
466
|
-
.book-hub-note-sum{margin:0 0 12px;font-size:13px;line-height:1.65;color:var(--muted)}
|
|
467
|
-
.book-hub-note-meta{margin:0;font-size:12px;color:var(--faint,#a09b91)}
|
|
468
|
-
@media (max-width:560px){
|
|
469
|
-
.book-hub{padding:28px 0 64px}
|
|
470
|
-
.book-hub-card{flex-direction:column;gap:18px}
|
|
471
|
-
.book-hub-cover,.book-hub-cover img{width:120px}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
437
|
.sh-row .incol{grid-column:2;justify-self:start;position:relative;z-index:1;margin-top:4px;font-size:var(--fs-meta);line-height:1.6;color:var(--muted);text-decoration:none}
|
|
475
438
|
@media (hover:hover){.sh-row .incol:hover{color:var(--blue);text-decoration:underline;text-underline-offset:3px}}
|
|
476
439
|
.is-compact .sh-row .incol{display:none}
|
package/docs/handbook/config.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: 站点与专栏配置
|
|
3
3
|
navTitle: 站点与专栏配置
|
|
4
|
-
description: 配置 site.config.yaml
|
|
4
|
+
description: 配置 site.config.yaml 中的站点信息、各页标题、自定义品牌图标与 content/columns.yaml 专栏。
|
|
5
5
|
section: getting-started
|
|
6
6
|
order: 3
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
站点的全局文字与外观信息集中在根目录的 `site.config.yaml`,专栏目录集中在 `content/columns.yaml
|
|
9
|
+
站点的全局文字与外观信息集中在根目录的 `site.config.yaml`,专栏目录集中在 `content/columns.yaml`。修改后运行 `npx garden sync`(或直接 `make dev`)即可生效。
|
|
10
10
|
|
|
11
11
|
## 1. `site.config.yaml` 完整结构
|
|
12
12
|
|
|
@@ -125,60 +125,3 @@ columns:
|
|
|
125
125
|
| `status` | 否 | 状态标识:`default`(或不写,显示为「持续更新」)、`ongoing`(「连载中」)、`completed`(「已完结」);旧写法 `持续更新`、`连载中`、`已完结` 仍可识别,其他文字按原样显示 |
|
|
126
126
|
| `cover` | 否 | 专栏封面图路径(相对 `content/covers/`) |
|
|
127
127
|
| `posts` | 是 | 按阅读顺序排列的文章路径列表(相对于 `content/posts/`,不带 `.md` 后缀) |
|
|
128
|
-
|
|
129
|
-
## 3. 配置读书笔记书架(`content/bookshelf.yaml`)
|
|
130
|
-
|
|
131
|
-
`/bookshelf` 在站点写了 `content/bookshelf.yaml` 时渲染这里的书,并带上站点顶栏,和其他页面一样可以被索引。没有这个文件时,`/bookshelf` 只在引擎包内的中文验收里显示内置 demo(无顶栏、不收录);正式站要真书目,就放下这份文件。
|
|
132
|
-
|
|
133
|
-
架上每一条 `books` 是一本书。`post: note/ddia` 是只有一篇笔记时的简写。一本书有多篇笔记时写 `posts`,点书脊进入 `/bookshelf/<id>`(本书),不打开第一篇,也不播放抽书动画。本书页左边是书衣(宽 148,高度跟图片走),右边是书名、作者,以及豆瓣和 PDF 两枚出处胶囊;PDF 后面有一行「(仅分享,侵权删)」。下面是「我的笔记」,点进去才是文章。单篇笔记仍走原来的抽书和合上。
|
|
134
|
-
|
|
135
|
-
`cover` 是书衣,只给书架、抽书飞页和本书页用。文件放在 `content/covers/`,构建时会复制到 `/media/covers/`,即使没有任何博文的 front matter 引用这个文件名。豆瓣封面四周的白边在放进 `content/covers/` 之前裁好,构建不会再改像素。笔记页的横幅仍是各篇 front matter 的 `cover`,不要把那张图填进书衣。yaml 不写 `cover` 时,书衣才沿用第一篇的封面。`color`(`#rgb` 或 `#rrggbb`)画书脊和单篇打开后的纸页。
|
|
136
|
-
|
|
137
|
-
```yaml
|
|
138
|
-
title: 读书笔记
|
|
139
|
-
description: 读过的书,和留下的笔记。
|
|
140
|
-
eyebrow: 花园
|
|
141
|
-
shelves:
|
|
142
|
-
- id: recent
|
|
143
|
-
name: 最近在读
|
|
144
|
-
books:
|
|
145
|
-
- id: ddia
|
|
146
|
-
title: 数据密集型应用系统设计
|
|
147
|
-
author: Martin Kleppmann
|
|
148
|
-
cover: ddia.jpg
|
|
149
|
-
color: "#8b3a2f"
|
|
150
|
-
post: note/ddia
|
|
151
|
-
- id: unit-testing
|
|
152
|
-
title: Unit Testing
|
|
153
|
-
cover: unit-testing.jpg
|
|
154
|
-
color: "#1a2744"
|
|
155
|
-
sources:
|
|
156
|
-
- label: 豆瓣
|
|
157
|
-
href: https://book.douban.com/subject/00000000/
|
|
158
|
-
- label: PDF
|
|
159
|
-
href: https://example.com/unit-testing.pdf
|
|
160
|
-
posts:
|
|
161
|
-
- path: note/note-unit-testing
|
|
162
|
-
kind: overview
|
|
163
|
-
- path: note/note-unit-testing-excerpt
|
|
164
|
-
kind: excerpt
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
| 字段 | 必填 | 说明 |
|
|
168
|
-
| :--- | :--- | :--- |
|
|
169
|
-
| `title` | 是 | 书架页标题 |
|
|
170
|
-
| `description` | 否 | 页面简介 |
|
|
171
|
-
| `eyebrow` | 否 | 架面眉题。不写时用站点名 |
|
|
172
|
-
| `shelves` | 是 | 非空数组。每一层要有 `id`、`name`,以及非空的 `books` |
|
|
173
|
-
| `books[].id` | 是 | 整架唯一 |
|
|
174
|
-
| `books[].title` | 是 | 书名,显示在左页和笔记页 |
|
|
175
|
-
| `books[].author` | 否 | 作者 |
|
|
176
|
-
| `books[].spine` | 否 | 书脊文字,默认同 `title` |
|
|
177
|
-
| `books[].size` | 否 | `thin`、`s-short`、`s-mid`、`s-tall`。普通书默认 `s-mid`,占位薄册默认 `thin` |
|
|
178
|
-
| `books[].cover` | 否 | 书衣。文件名或完整 URL。写了就只用它 |
|
|
179
|
-
| `books[].color` | 否 | 书脊和单篇打开后的纸色 |
|
|
180
|
-
| `books[].filler` | 否 | `true`、`sand` 或 `cream`,占位薄册,不能打开 |
|
|
181
|
-
| `books[].post` | 否 | 单篇笔记的简写 |
|
|
182
|
-
| `books[].posts` | 否 | 同一本书下的笔记。`kind` 为 `overview`(总览)或 `excerpt`(摘抄)。两篇及以上打开本书页 |
|
|
183
|
-
| `books[].sources` | 否 | 出处。本书页只显示「豆瓣」和「PDF」 |
|
|
184
|
-
| `books[].note` | 否 | 没有笔记路径时直接写的 Markdown |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Site & Column Config
|
|
3
3
|
navTitle: Site & Column Config
|
|
4
|
-
description: Configure site metadata, page headings, and brand icons in site.config.yaml,
|
|
4
|
+
description: Configure site metadata, page headings, and brand icons in site.config.yaml, and organize series in content/columns.yaml.
|
|
5
5
|
section: getting-started
|
|
6
6
|
order: 3
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Global site copy and appearance settings live in `site.config.yaml` at the repository root, series definitions live in `content/columns.yaml
|
|
9
|
+
Global site copy and appearance settings live in `site.config.yaml` at the repository root, while series definitions live in `content/columns.yaml`. Run `npx garden sync` (or `make dev`) after editing to apply changes.
|
|
10
10
|
|
|
11
11
|
## 1. Full `site.config.yaml` Structure
|
|
12
12
|
|
|
@@ -125,60 +125,3 @@ columns:
|
|
|
125
125
|
| `status` | No | Status value: `default` (or omitted, shown as "Active"), `ongoing` ("Ongoing"), or `completed` ("Completed"). Legacy Chinese values are still accepted, and any other text is shown verbatim |
|
|
126
126
|
| `cover` | No | Cover image path relative to `content/covers/` |
|
|
127
127
|
| `posts` | Yes | Ordered array of post paths relative to `content/posts/`, without the `.md` extension |
|
|
128
|
-
|
|
129
|
-
## 3. Configuring the bookshelf (`content/bookshelf.yaml`)
|
|
130
|
-
|
|
131
|
-
When a site has `content/bookshelf.yaml`, `/bookshelf` renders those books inside the site header and is indexed with the other pages. Without that file, `/bookshelf` is only the package's built-in demo (Chinese click-test, no header, not indexed). A production site supplies the file.
|
|
132
|
-
|
|
133
|
-
Each `books` entry is one volume. `post: note/ddia` is the one-note shorthand. Several notes use `posts`. Clicking that spine opens `/bookshelf/<id>` and does not open the first note or play the extract animation. The book page shows a 148px-wide jacket (height follows the image), the title, the author, and only the Douban and PDF chips. The PDF chip is followed by a short sharing note. My notes lists the notes; each card is a normal link to the article. A single note still uses the extract and return animation.
|
|
134
|
-
|
|
135
|
-
`cover` is the jacket for the shelf, the flyer, and the book page. The file lives in `content/covers/` and the build copies it to `/media/covers/`, even when no post front matter names that file. Crop a Douban scan's white margin before committing the file; the build does not change pixels. A note's hero stays that post's front matter `cover`. Omit the jacket and it falls back to the first note's cover. `color` (`#rgb` or `#rrggbb`) is the spine and the single-note open page.
|
|
136
|
-
|
|
137
|
-
```yaml
|
|
138
|
-
title: Reading notes
|
|
139
|
-
description: Books I have read, and the notes I kept.
|
|
140
|
-
eyebrow: Library
|
|
141
|
-
shelves:
|
|
142
|
-
- id: recent
|
|
143
|
-
name: Recent
|
|
144
|
-
books:
|
|
145
|
-
- id: ddia
|
|
146
|
-
title: Designing Data-Intensive Applications
|
|
147
|
-
author: Martin Kleppmann
|
|
148
|
-
cover: ddia.jpg
|
|
149
|
-
color: "#8b3a2f"
|
|
150
|
-
post: note/ddia
|
|
151
|
-
- id: unit-testing
|
|
152
|
-
title: Unit Testing
|
|
153
|
-
cover: unit-testing.jpg
|
|
154
|
-
color: "#1a2744"
|
|
155
|
-
sources:
|
|
156
|
-
- label: Douban
|
|
157
|
-
href: https://book.douban.com/subject/00000000/
|
|
158
|
-
- label: PDF
|
|
159
|
-
href: https://example.com/unit-testing.pdf
|
|
160
|
-
posts:
|
|
161
|
-
- path: note/note-unit-testing
|
|
162
|
-
kind: overview
|
|
163
|
-
- path: note/note-unit-testing-excerpt
|
|
164
|
-
kind: excerpt
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
| Field | Required | Description |
|
|
168
|
-
| :--- | :--- | :--- |
|
|
169
|
-
| `title` | Yes | Page title |
|
|
170
|
-
| `description` | No | Page summary |
|
|
171
|
-
| `eyebrow` | No | Small line above the title. Falls back to the site name |
|
|
172
|
-
| `shelves` | Yes | Non-empty list. Each shelf needs `id`, `name`, and a non-empty `books` list |
|
|
173
|
-
| `books[].id` | Yes | Unique across the shelf |
|
|
174
|
-
| `books[].title` | Yes | Title on the left page and the note view |
|
|
175
|
-
| `books[].author` | No | Author |
|
|
176
|
-
| `books[].spine` | No | Spine label. Defaults to `title` |
|
|
177
|
-
| `books[].size` | No | `thin`, `s-short`, `s-mid`, or `s-tall`. Real books default to `s-mid`; filler spines default to `thin` |
|
|
178
|
-
| `books[].cover` | No | Jacket. File name or full URL. When set, it is the only jacket |
|
|
179
|
-
| `books[].color` | No | Spine and single-note open-page color |
|
|
180
|
-
| `books[].filler` | No | `true`, `sand`, or `cream`. A thin spacer that does not open |
|
|
181
|
-
| `books[].post` | No | Shorthand for one note |
|
|
182
|
-
| `books[].posts` | No | Notes on this volume. `kind` is `overview` or `excerpt`. Two or more open the book page |
|
|
183
|
-
| `books[].sources` | No | Sources. The book page shows Douban and PDF only |
|
|
184
|
-
| `books[].note` | No | Markdown used only when the book names no posts |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-garden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"public/apple-touch-icon.png",
|
|
32
32
|
"public/og.png",
|
|
33
33
|
"public/images",
|
|
34
|
-
"public/covers",
|
|
35
34
|
"scripts",
|
|
36
35
|
"templates",
|
|
37
36
|
"ui",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
import { marked } from "marked";
|
|
7
7
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
@@ -994,36 +994,6 @@ const sitemapEntries = [
|
|
|
994
994
|
...thoughts.map((entry) => `/thoughts/${entry.slug}/`),
|
|
995
995
|
].map(pageAddress)),
|
|
996
996
|
];
|
|
997
|
-
// A site shelf is a public page. The package demo (no content/bookshelf.yaml) is not.
|
|
998
|
-
// The app imports this module instead of reading the yaml at request time, so the
|
|
999
|
-
// worker preview and the static export both see the same shelf.
|
|
1000
|
-
// Forked script copies (pipeline tests) do not ship app/bookshelf; a missing loader
|
|
1001
|
-
// means there is no site shelf, same as a package without the file.
|
|
1002
|
-
const bookshelfLoaderPath = path.join(gardenPackageRoot, "app", "bookshelf", "load.mjs");
|
|
1003
|
-
const loadBookshelf = fs.existsSync(bookshelfLoaderPath)
|
|
1004
|
-
? (await import(pathToFileURL(bookshelfLoaderPath).href)).loadBookshelf
|
|
1005
|
-
: () => null;
|
|
1006
|
-
const bookshelf = loadBookshelf(root, posts);
|
|
1007
|
-
fs.mkdirSync(gardenDir, { recursive: true });
|
|
1008
|
-
fs.writeFileSync(
|
|
1009
|
-
path.join(gardenDir, "bookshelf.ts"),
|
|
1010
|
-
`// Generated by garden sync. Edit content/bookshelf.yaml instead.\nexport const bookshelf = ${JSON.stringify(bookshelf)} as const;\n`,
|
|
1011
|
-
);
|
|
1012
|
-
if (bookshelf) {
|
|
1013
|
-
sitemapEntries.push(pageAddress("/bookshelf/"));
|
|
1014
|
-
for (const row of bookshelf.catalog ?? []) {
|
|
1015
|
-
for (const book of row.books ?? []) {
|
|
1016
|
-
if (typeof book.hub === "string" && book.hub.startsWith("/")) sitemapEntries.push(pageAddress(book.hub));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
const { publishBookshelfJackets } = await import(pathToFileURL(path.join(gardenPackageRoot, "scripts", "bookshelf-covers.mjs")).href);
|
|
1020
|
-
const { parseBookshelfConfig } = await import(pathToFileURL(path.join(gardenPackageRoot, "app", "components", "bookshelf", "schema.mjs")).href);
|
|
1021
|
-
const coverConfig = parseBookshelfConfig(parseYaml(fs.readFileSync(path.join(contentRoot, "bookshelf.yaml"), "utf8")));
|
|
1022
|
-
publishBookshelfJackets(coverConfig, {
|
|
1023
|
-
coversDir: path.join(contentRoot, "covers"),
|
|
1024
|
-
mediaDir: path.join(generatedMediaRoot, "covers"),
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
997
|
// Card URLs in published entries: the only pages the link-card Worker may read.
|
|
1028
998
|
fs.writeFileSync(
|
|
1029
999
|
path.join(publicRoot, "link-allowlist.json"),
|
|
@@ -743,14 +743,13 @@ export function checkFileFacts(filePath, content, facts, root = ROOT) {
|
|
|
743
743
|
const isWorkflow =
|
|
744
744
|
/\b(?:runs-on|steps|jobs|workflow_dispatch|actions\/checkout)\b/u.test(bodyText);
|
|
745
745
|
const isColumnsYaml = /^columns:\s*$/mu.test(bodyText) && /^\s*-\s+slug:/mu.test(bodyText);
|
|
746
|
-
const isBookshelfYaml = /^shelves:\s*$/mu.test(bodyText) && /^\s*-\s+id:/mu.test(bodyText) && /\bbooks:/u.test(bodyText);
|
|
747
746
|
const isFrontmatter = /^\s*---\s*$/mu.test(bodyText) || (/\bdraft:/u.test(bodyText) && /\btags:/u.test(bodyText));
|
|
748
747
|
|
|
749
748
|
if (isWorkflow) {
|
|
750
749
|
checkWorkflowRunLines(fence, relFile, facts, issues);
|
|
751
750
|
}
|
|
752
751
|
|
|
753
|
-
if (!isWorkflow && !isColumnsYaml && !
|
|
752
|
+
if (!isWorkflow && !isColumnsYaml && !isFrontmatter) {
|
|
754
753
|
try {
|
|
755
754
|
const parsed = parseYaml(bodyText);
|
|
756
755
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
package/ui/garden/site-nav.mjs
CHANGED
|
@@ -11,18 +11,6 @@ export const SITE_NAV = [
|
|
|
11
11
|
{ href: "/about/", label: "关于" },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Header links. A site with `content/bookshelf.yaml` gains 书架; the package
|
|
16
|
-
* demo (no file) does not advertise a page it only click-tests.
|
|
17
|
-
*/
|
|
18
|
-
export function siteNav({ shelf = false } = {}) {
|
|
19
|
-
if (!shelf) return SITE_NAV;
|
|
20
|
-
const entry = { href: "/bookshelf/", label: "书架" };
|
|
21
|
-
const about = SITE_NAV.findIndex((item) => item.href === "/about/");
|
|
22
|
-
if (about < 0) return [...SITE_NAV, entry];
|
|
23
|
-
return [...SITE_NAV.slice(0, about), entry, ...SITE_NAV.slice(about)];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
const trim = (value) => (value.length > 1 ? value.replace(/\/+$/u, "") : value);
|
|
27
15
|
|
|
28
16
|
/**
|
package/vite.config.ts
CHANGED
|
@@ -14,11 +14,9 @@ const { d1, r2 } = hostingConfig;
|
|
|
14
14
|
const siteRoot = gardenSiteRoot();
|
|
15
15
|
const generatedContent = path.join(siteRoot, ".garden", "generated-content.ts");
|
|
16
16
|
const generatedSiteConfig = path.join(siteRoot, ".garden", "site-config.ts");
|
|
17
|
-
const generatedBookshelf = path.join(siteRoot, ".garden", "bookshelf.ts");
|
|
18
17
|
const generatedAliases = {
|
|
19
18
|
"@garden/generated-content": generatedContent,
|
|
20
19
|
"@garden/site-config": generatedSiteConfig,
|
|
21
|
-
"@garden/bookshelf": generatedBookshelf,
|
|
22
20
|
};
|
|
23
21
|
|
|
24
22
|
// macOS Seatbelt blocks FSEvents, so Codex previews need polling for HMR.
|
|
@@ -69,11 +67,10 @@ function gardenContent() {
|
|
|
69
67
|
const normalized = id.split("?")[0];
|
|
70
68
|
if (normalized === "@garden/generated-content") return generatedContent;
|
|
71
69
|
if (normalized === "@garden/site-config") return generatedSiteConfig;
|
|
72
|
-
if (normalized === "@garden/bookshelf") return generatedBookshelf;
|
|
73
70
|
return undefined;
|
|
74
71
|
},
|
|
75
72
|
buildStart() {
|
|
76
|
-
if (!fs.existsSync(generatedContent) || !fs.existsSync(generatedSiteConfig)
|
|
73
|
+
if (!fs.existsSync(generatedContent) || !fs.existsSync(generatedSiteConfig)) {
|
|
77
74
|
sync();
|
|
78
75
|
}
|
|
79
76
|
},
|
|
@@ -81,19 +78,13 @@ function gardenContent() {
|
|
|
81
78
|
const contentDir = path.join(siteRoot, "content");
|
|
82
79
|
const configFile = path.join(siteRoot, "site.config.yaml");
|
|
83
80
|
server.watcher.add([contentDir, configFile]);
|
|
84
|
-
|
|
81
|
+
server.watcher.on("change", (file) => {
|
|
85
82
|
if (file.startsWith(contentDir) || file === configFile) {
|
|
86
83
|
// While editing, a broken ui block shows as an error card instead of stopping the server.
|
|
87
84
|
sync("preview");
|
|
88
85
|
server.ws.send({ type: "full-reload" });
|
|
89
86
|
}
|
|
90
|
-
};
|
|
91
|
-
server.watcher.on("change", resyncContent);
|
|
92
|
-
// A new or removed bookshelf.yaml is not a "change". Sync so /bookshelf
|
|
93
|
-
// switches between the site shelf and the package demo without a restart.
|
|
94
|
-
const isBookshelf = (file: string) => file.endsWith(`${path.sep}bookshelf.yaml`) || file.endsWith("/bookshelf.yaml");
|
|
95
|
-
server.watcher.on("add", (file) => { if (isBookshelf(file)) resyncContent(file); });
|
|
96
|
-
server.watcher.on("unlink", (file) => { if (isBookshelf(file)) resyncContent(file); });
|
|
87
|
+
});
|
|
97
88
|
},
|
|
98
89
|
};
|
|
99
90
|
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
import { bookshelf } from "@garden/bookshelf";
|
|
4
|
-
import { BookHub } from "../../components/bookshelf/BookHub";
|
|
5
|
-
import { pageAlternates } from "../../lib/page-alternates";
|
|
6
|
-
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
|
|
7
|
-
import type { BookshelfBook } from "../../components/bookshelf/types";
|
|
8
|
-
|
|
9
|
-
function hubBooks(): { shelfTitle: string; book: BookshelfBook }[] {
|
|
10
|
-
if (!bookshelf) return [];
|
|
11
|
-
const found: { shelfTitle: string; book: BookshelfBook }[] = [];
|
|
12
|
-
for (const row of bookshelf.catalog) {
|
|
13
|
-
for (const book of row.books) {
|
|
14
|
-
if ((book.posts?.length ?? 0) > 1) found.push({ shelfTitle: bookshelf.title, book });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return found;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function generateStaticParams() {
|
|
21
|
-
return staticParams(
|
|
22
|
-
hubBooks().map(({ book }) => ({ bookId: book.id })),
|
|
23
|
-
{ bookId: STATIC_PLACEHOLDER },
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function generateMetadata({
|
|
28
|
-
params,
|
|
29
|
-
}: {
|
|
30
|
-
params: Promise<{ bookId: string }>;
|
|
31
|
-
}): Promise<Metadata> {
|
|
32
|
-
const { bookId } = await params;
|
|
33
|
-
const found = hubBooks().find((item) => item.book.id === bookId);
|
|
34
|
-
if (!found) return {};
|
|
35
|
-
return {
|
|
36
|
-
title: found.book.title,
|
|
37
|
-
description: found.book.posts?.find((post) => post.summary)?.summary,
|
|
38
|
-
alternates: pageAlternates(`/bookshelf/${found.book.id}`),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** `/bookshelf/<id>` — one multi-note volume. Single-note books stay on the shelf. */
|
|
43
|
-
export default async function BookHubPage({
|
|
44
|
-
params,
|
|
45
|
-
}: {
|
|
46
|
-
params: Promise<{ bookId: string }>;
|
|
47
|
-
}) {
|
|
48
|
-
const { bookId } = await params;
|
|
49
|
-
const found = hubBooks().find((item) => item.book.id === bookId);
|
|
50
|
-
if (!found) {
|
|
51
|
-
if (isStaticPlaceholder(bookId)) return null;
|
|
52
|
-
notFound();
|
|
53
|
-
}
|
|
54
|
-
return <BookHub shelfTitle={found.shelfTitle} book={found.book} />;
|
|
55
|
-
}
|