hd-garden 0.3.2 → 0.3.4
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/about/page.tsx +2 -2
- package/app/blog/[...path]/page.tsx +2 -2
- package/app/blog/page.tsx +3 -3
- package/app/columns/[column]/[...path]/page.tsx +2 -2
- package/app/columns/page.tsx +43 -32
- package/app/components/BlogArchive.tsx +1 -1
- package/app/components/GardenContentGuide.tsx +3 -5
- package/app/components/MarkdownArticle.tsx +2 -5
- package/app/components/MarkdownImage.tsx +65 -0
- package/app/garden/page.tsx +1 -1
- package/app/garden-modules.d.ts +0 -1
- package/app/globals.css +147 -31
- package/app/lib/generated-content.ts +17 -23
- package/app/lib/github-content.ts +12 -7
- package/app/page.tsx +2 -2
- package/app/rss.xml/route.ts +1 -3
- package/app/site.config.ts +3 -0
- package/app/thoughts/page.tsx +3 -3
- package/package.json +1 -1
- package/scripts/build-content.mjs +15 -8
- package/scripts/new-post.mjs +0 -1
- package/templates/site/package.json +1 -1
package/app/about/page.tsx
CHANGED
|
@@ -19,8 +19,8 @@ const focus = about.focus ?? [
|
|
|
19
19
|
const cta = about.cta ?? { href: "/blog", label: "从博文开始" };
|
|
20
20
|
|
|
21
21
|
export const metadata = {
|
|
22
|
-
title:
|
|
23
|
-
description: about.subtitle,
|
|
22
|
+
title: { absolute: siteConfig.name },
|
|
23
|
+
description: about.subtitle || siteConfig.description,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export default function AboutPage() {
|
|
@@ -48,7 +48,7 @@ export async function generateMetadata({
|
|
|
48
48
|
? new Date(post.updated).toISOString()
|
|
49
49
|
: undefined,
|
|
50
50
|
tags: post.tags,
|
|
51
|
-
images: post.cover ? [{ url: post.cover, alt: post.
|
|
51
|
+
images: post.cover ? [{ url: post.cover, alt: post.title }] : undefined,
|
|
52
52
|
},
|
|
53
53
|
twitter: {
|
|
54
54
|
card: "summary_large_image",
|
|
@@ -92,7 +92,7 @@ export default async function PostPage({
|
|
|
92
92
|
<img
|
|
93
93
|
className="u-photo"
|
|
94
94
|
src={post.cover}
|
|
95
|
-
alt={
|
|
95
|
+
alt={`${post.title} 封面`}
|
|
96
96
|
/>
|
|
97
97
|
</figure>
|
|
98
98
|
)}
|
package/app/blog/page.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
|
-
import { siteConfig } from "../site.config";
|
|
2
|
+
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
3
3
|
import { BlogArchive } from "../components/BlogArchive";
|
|
4
4
|
|
|
5
5
|
export const metadata = {
|
|
6
|
-
title:
|
|
7
|
-
description: siteConfig.pages.blog.subtitle,
|
|
6
|
+
title: siteDocumentTitle,
|
|
7
|
+
description: siteConfig.pages.blog.subtitle || siteConfig.description,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export default function BlogIndex() {
|
|
@@ -52,7 +52,7 @@ export async function generateMetadata({
|
|
|
52
52
|
? new Date(entry.updated).toISOString()
|
|
53
53
|
: undefined,
|
|
54
54
|
tags: entry.tags,
|
|
55
|
-
images: entry.cover ? [{ url: entry.cover, alt: entry.
|
|
55
|
+
images: entry.cover ? [{ url: entry.cover, alt: entry.title }] : undefined,
|
|
56
56
|
},
|
|
57
57
|
twitter: {
|
|
58
58
|
card: "summary_large_image",
|
|
@@ -115,7 +115,7 @@ export default async function ColumnEntryPage({
|
|
|
115
115
|
<img
|
|
116
116
|
className="u-photo"
|
|
117
117
|
src={entry.cover}
|
|
118
|
-
alt={
|
|
118
|
+
alt={`${entry.title} 封面`}
|
|
119
119
|
/>
|
|
120
120
|
</figure>
|
|
121
121
|
)}
|
package/app/columns/page.tsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
/* eslint-disable @next/next/no-img-element -- static GitHub Pages assets must keep their exact paths. */
|
|
1
2
|
import Link from "next/link";
|
|
2
3
|
import { ArrowRight } from "@phosphor-icons/react/ssr";
|
|
3
4
|
import { columnHref, columns } from "../lib/content";
|
|
4
5
|
import { PageIntro } from "../components/PageIntro";
|
|
5
6
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
6
|
-
import { siteConfig } from "../site.config";
|
|
7
|
+
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
7
8
|
|
|
8
9
|
export const metadata = {
|
|
9
|
-
title:
|
|
10
|
-
description: siteConfig.pages.columns.subtitle,
|
|
10
|
+
title: siteDocumentTitle,
|
|
11
|
+
description: siteConfig.pages.columns.subtitle || siteConfig.description,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export default function ColumnsPage() {
|
|
@@ -33,35 +34,45 @@ export default function ColumnsPage() {
|
|
|
33
34
|
/>
|
|
34
35
|
|
|
35
36
|
{grouped.length > 0 ? <div className="column-feature-list">
|
|
36
|
-
{grouped.map(({ slug, entries, first }, index) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
37
|
+
{grouped.map(({ slug, entries, first }, index) => {
|
|
38
|
+
const cover = first.columnCover
|
|
39
|
+
? { src: first.columnCover, alt: first.columnCoverAlt ?? `${first.columnTitle} 专栏封面` }
|
|
40
|
+
: undefined;
|
|
41
|
+
return (
|
|
42
|
+
<Link
|
|
43
|
+
href={columnHref(first)}
|
|
44
|
+
className="column-feature"
|
|
45
|
+
key={slug}
|
|
46
|
+
>
|
|
47
|
+
<div className="column-feature-copy">
|
|
48
|
+
<span>
|
|
49
|
+
{first.columnStatus ?? "持续更新"} · {entries.length} 篇 ·{" "}
|
|
50
|
+
{first.topic ?? "主题写作"}
|
|
51
|
+
</span>
|
|
52
|
+
<h2>{first.columnTitle}</h2>
|
|
53
|
+
<p>
|
|
54
|
+
{first.columnDescription ??
|
|
55
|
+
first.description ??
|
|
56
|
+
"沿着一条清晰的阅读路径,把一个主题持续写深。"}
|
|
57
|
+
</p>
|
|
58
|
+
<strong>
|
|
59
|
+
从第一篇开始
|
|
60
|
+
<ArrowRight size={17} />
|
|
61
|
+
</strong>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="column-feature-index">
|
|
64
|
+
{cover && (
|
|
65
|
+
<img className="column-feature-cover" src={cover.src} alt={cover.alt} />
|
|
66
|
+
)}
|
|
67
|
+
<div className="column-feature-index-label">
|
|
68
|
+
<span>主题专栏</span>
|
|
69
|
+
<strong>{String(index + 1).padStart(2, "0")}</strong>
|
|
70
|
+
<small>{slug.replaceAll("-", " ").toUpperCase()}</small>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</Link>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
65
76
|
</div> : <ContentEmptyState kind="columns" />}
|
|
66
77
|
</main>
|
|
67
78
|
);
|
|
@@ -26,13 +26,12 @@ function SourcePanel({ path, source, language }: SourcePanelProps) {
|
|
|
26
26
|
|
|
27
27
|
const postSource = `---
|
|
28
28
|
title: 我的第一篇文章
|
|
29
|
-
slug: first-post
|
|
30
29
|
date: 2026-08-03T09:00:00+08:00
|
|
31
30
|
description: 这篇文章记录我为什么重新开始写博客。
|
|
32
31
|
topic: 写作
|
|
33
32
|
tags: [Markdown, 博客]
|
|
34
33
|
pinned: false
|
|
35
|
-
cover:
|
|
34
|
+
cover: writing/first-post.jpg
|
|
36
35
|
---
|
|
37
36
|
|
|
38
37
|
正文从这里开始。
|
|
@@ -43,7 +42,6 @@ cover: ./assets/cover.jpg
|
|
|
43
42
|
|
|
44
43
|
const thoughtSource = `---
|
|
45
44
|
title: 雨后的散步
|
|
46
|
-
slug: walk-after-rain
|
|
47
45
|
date: 2026-08-03T18:30:00+08:00
|
|
48
46
|
tags: [生活]
|
|
49
47
|
media:
|
|
@@ -121,9 +119,9 @@ const guides: Guide[] = [
|
|
|
121
119
|
"随想适合记录几句话、一个链接或一组多媒体。它同样保存在 Git 仓库中,只需要更少的字段,也可以通过可选管理后台发布。",
|
|
122
120
|
steps: [
|
|
123
121
|
<>
|
|
124
|
-
直接创建 <code>content/thoughts/<
|
|
122
|
+
直接创建 <code>content/thoughts/<文件名>.md</code>,或者在管理后台填写内容。
|
|
125
123
|
</>,
|
|
126
|
-
|
|
124
|
+
<>文件名就是地址。front matter 填写标题和日期;需要暂存时加入草稿状态。</>,
|
|
127
125
|
<>
|
|
128
126
|
在 <code>media</code> 中按顺序添加图片、音频、视频或外部链接。
|
|
129
127
|
</>,
|
|
@@ -11,6 +11,7 @@ import rehypeSlug from "rehype-slug";
|
|
|
11
11
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
12
12
|
import rehypeHighlight from "rehype-highlight";
|
|
13
13
|
import { MermaidDiagram } from "./MermaidDiagram";
|
|
14
|
+
import { MarkdownImage } from "./MarkdownImage";
|
|
14
15
|
import { ExternalEmbed } from "./ExternalEmbed";
|
|
15
16
|
import { WidgetChart } from "./WidgetChart";
|
|
16
17
|
|
|
@@ -106,11 +107,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
106
107
|
</a>
|
|
107
108
|
);
|
|
108
109
|
},
|
|
109
|
-
img:
|
|
110
|
-
// Native images keep Typora-authored relative paths intact.
|
|
111
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
112
|
-
<img loading="lazy" alt={alt ?? ""} {...props} />
|
|
113
|
-
),
|
|
110
|
+
img: MarkdownImage,
|
|
114
111
|
code: CodeBlock,
|
|
115
112
|
pre: ({ children, ...props }) => {
|
|
116
113
|
const child = Children.only(children);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
|
|
4
|
+
|
|
5
|
+
type MarkdownImageProps = ComponentPropsWithoutRef<"img">;
|
|
6
|
+
|
|
7
|
+
export function MarkdownImage({ alt = "", src, ...props }: MarkdownImageProps) {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const description = alt || "查看原图";
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!open) return undefined;
|
|
13
|
+
|
|
14
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
15
|
+
if (event.key === "Escape") setOpen(false);
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
18
|
+
document.body.classList.add("image-preview-open");
|
|
19
|
+
|
|
20
|
+
return () => {
|
|
21
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
22
|
+
document.body.classList.remove("image-preview-open");
|
|
23
|
+
};
|
|
24
|
+
}, [open]);
|
|
25
|
+
|
|
26
|
+
if (!src) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className="markdown-image-trigger"
|
|
33
|
+
onClick={() => setOpen(true)}
|
|
34
|
+
aria-label={`放大查看:${description}`}
|
|
35
|
+
>
|
|
36
|
+
{/* Native images preserve Typora-authored relative paths. */}
|
|
37
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
38
|
+
<img loading="lazy" alt={alt} src={src} {...props} />
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
{open ? (
|
|
42
|
+
<div
|
|
43
|
+
className="markdown-image-lightbox"
|
|
44
|
+
role="dialog"
|
|
45
|
+
aria-modal="true"
|
|
46
|
+
aria-label={description}
|
|
47
|
+
onClick={() => setOpen(false)}
|
|
48
|
+
>
|
|
49
|
+
<div className="markdown-image-lightbox-content" onClick={(event) => event.stopPropagation()}>
|
|
50
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
51
|
+
<img src={src} alt={alt} {...props} />
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="markdown-image-lightbox-close"
|
|
55
|
+
onClick={() => setOpen(false)}
|
|
56
|
+
aria-label="关闭图片预览"
|
|
57
|
+
>
|
|
58
|
+
关闭
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
) : null}
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
package/app/garden/page.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { TableOfContents } from "../components/TableOfContents";
|
|
|
7
7
|
import { siteConfig } from "../site.config";
|
|
8
8
|
|
|
9
9
|
export const metadata = {
|
|
10
|
-
title:
|
|
10
|
+
title: "Garden",
|
|
11
11
|
description:
|
|
12
12
|
"一个以 Markdown 为内容源、通过 GitHub Pages 发布的个人博客框架,支持博文、随想、专栏、搜索、RSS 与多媒体。",
|
|
13
13
|
};
|
package/app/garden-modules.d.ts
CHANGED
package/app/globals.css
CHANGED
|
@@ -250,6 +250,11 @@ button {
|
|
|
250
250
|
margin-top: 72px;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
.home-thought-list {
|
|
254
|
+
display: grid;
|
|
255
|
+
gap: 24px;
|
|
256
|
+
}
|
|
257
|
+
|
|
253
258
|
.section-heading {
|
|
254
259
|
min-height: 40px;
|
|
255
260
|
margin-bottom: 21px;
|
|
@@ -1443,7 +1448,7 @@ button {
|
|
|
1443
1448
|
}
|
|
1444
1449
|
|
|
1445
1450
|
.archive-row.with-cover {
|
|
1446
|
-
grid-template-columns:
|
|
1451
|
+
grid-template-columns: 160px minmax(0, 1fr) 20px;
|
|
1447
1452
|
}
|
|
1448
1453
|
|
|
1449
1454
|
.archive-row:hover {
|
|
@@ -1469,10 +1474,9 @@ button {
|
|
|
1469
1474
|
}
|
|
1470
1475
|
|
|
1471
1476
|
.archive-cover {
|
|
1472
|
-
width:
|
|
1473
|
-
height:
|
|
1477
|
+
width: 160px;
|
|
1478
|
+
height: auto;
|
|
1474
1479
|
display: block;
|
|
1475
|
-
object-fit: cover;
|
|
1476
1480
|
border: 1px solid var(--line);
|
|
1477
1481
|
}
|
|
1478
1482
|
|
|
@@ -1752,23 +1756,54 @@ button {
|
|
|
1752
1756
|
}
|
|
1753
1757
|
|
|
1754
1758
|
.column-feature-index {
|
|
1759
|
+
position: relative;
|
|
1760
|
+
overflow: hidden;
|
|
1761
|
+
background: var(--ink);
|
|
1762
|
+
color: var(--paper);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
.column-feature-cover {
|
|
1766
|
+
width: 100%;
|
|
1767
|
+
height: 100%;
|
|
1768
|
+
display: block;
|
|
1769
|
+
object-fit: cover;
|
|
1770
|
+
object-position: center;
|
|
1771
|
+
transition: transform 280ms ease;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
.column-feature:hover .column-feature-cover,
|
|
1775
|
+
.column-feature:focus-visible .column-feature-cover {
|
|
1776
|
+
transform: scale(1.035);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
.column-feature-index::after {
|
|
1780
|
+
content: "";
|
|
1781
|
+
position: absolute;
|
|
1782
|
+
inset: 0;
|
|
1783
|
+
background: linear-gradient(180deg, rgb(16 18 23 / 20%), rgb(16 18 23 / 8%) 48%, rgb(16 18 23 / 60%));
|
|
1784
|
+
pointer-events: none;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
.column-feature-index-label {
|
|
1788
|
+
position: absolute;
|
|
1789
|
+
z-index: 1;
|
|
1790
|
+
inset: 0;
|
|
1755
1791
|
padding: 28px;
|
|
1756
1792
|
display: flex;
|
|
1757
1793
|
flex-direction: column;
|
|
1758
1794
|
justify-content: space-between;
|
|
1759
|
-
background: var(--ink);
|
|
1760
|
-
color: var(--paper);
|
|
1761
1795
|
}
|
|
1762
1796
|
|
|
1763
|
-
.column-feature-index span,
|
|
1764
|
-
.column-feature-index small {
|
|
1797
|
+
.column-feature-index-label span,
|
|
1798
|
+
.column-feature-index-label small {
|
|
1765
1799
|
color: var(--faint);
|
|
1766
1800
|
font-size: 9px;
|
|
1767
1801
|
letter-spacing: 0.08em;
|
|
1768
1802
|
}
|
|
1769
1803
|
|
|
1770
|
-
.column-feature-index strong {
|
|
1804
|
+
.column-feature-index-label strong {
|
|
1771
1805
|
align-self: center;
|
|
1806
|
+
color: white;
|
|
1772
1807
|
font-size: 82px;
|
|
1773
1808
|
line-height: 1;
|
|
1774
1809
|
}
|
|
@@ -1805,10 +1840,9 @@ button {
|
|
|
1805
1840
|
|
|
1806
1841
|
.article-hero img {
|
|
1807
1842
|
width: 100%;
|
|
1808
|
-
|
|
1843
|
+
height: auto;
|
|
1809
1844
|
display: block;
|
|
1810
|
-
|
|
1811
|
-
border: 1px solid var(--line);
|
|
1845
|
+
border: 0;
|
|
1812
1846
|
}
|
|
1813
1847
|
|
|
1814
1848
|
.post-reading-page .article-summary-row {
|
|
@@ -1854,14 +1888,6 @@ button {
|
|
|
1854
1888
|
margin: 28px 0 34px;
|
|
1855
1889
|
}
|
|
1856
1890
|
|
|
1857
|
-
.article-cover img {
|
|
1858
|
-
width: 100%;
|
|
1859
|
-
aspect-ratio: 16 / 8.5;
|
|
1860
|
-
display: block;
|
|
1861
|
-
object-fit: cover;
|
|
1862
|
-
border: 1px solid var(--line);
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
1891
|
.article-header h1 {
|
|
1866
1892
|
margin: 0;
|
|
1867
1893
|
font-size: clamp(32px, 4.2vw, 48px);
|
|
@@ -2107,6 +2133,35 @@ button {
|
|
|
2107
2133
|
padding-left: 25px;
|
|
2108
2134
|
}
|
|
2109
2135
|
|
|
2136
|
+
.markdown-body ul {
|
|
2137
|
+
list-style: disc outside;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.markdown-body ol {
|
|
2141
|
+
list-style: decimal outside;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
.markdown-body ul ul {
|
|
2145
|
+
list-style-type: circle;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
.markdown-body ul ul ul {
|
|
2149
|
+
list-style-type: square;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.markdown-body ol ol {
|
|
2153
|
+
list-style-type: lower-alpha;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
.markdown-body ol ol ol {
|
|
2157
|
+
list-style-type: lower-roman;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
.markdown-body ul.contains-task-list {
|
|
2161
|
+
padding-left: 0;
|
|
2162
|
+
list-style: none;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2110
2165
|
.markdown-body li {
|
|
2111
2166
|
margin: 7px 0;
|
|
2112
2167
|
}
|
|
@@ -2498,7 +2553,68 @@ button {
|
|
|
2498
2553
|
padding-block: 0;
|
|
2499
2554
|
}
|
|
2500
2555
|
|
|
2501
|
-
.markdown-
|
|
2556
|
+
.markdown-image-trigger {
|
|
2557
|
+
display: block;
|
|
2558
|
+
max-width: 100%;
|
|
2559
|
+
margin: 34px auto;
|
|
2560
|
+
padding: 0;
|
|
2561
|
+
border: 0;
|
|
2562
|
+
background: transparent;
|
|
2563
|
+
cursor: zoom-in;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
.markdown-image-trigger img {
|
|
2567
|
+
display: block;
|
|
2568
|
+
max-width: 100%;
|
|
2569
|
+
height: auto;
|
|
2570
|
+
margin: 0;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
.markdown-image-lightbox {
|
|
2574
|
+
position: fixed;
|
|
2575
|
+
z-index: 100;
|
|
2576
|
+
inset: 0;
|
|
2577
|
+
display: grid;
|
|
2578
|
+
place-items: center;
|
|
2579
|
+
padding: 32px;
|
|
2580
|
+
background: color-mix(in srgb, var(--ink) 82%, transparent);
|
|
2581
|
+
cursor: zoom-out;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
.markdown-image-lightbox-content {
|
|
2585
|
+
position: relative;
|
|
2586
|
+
max-width: min(1440px, 100%);
|
|
2587
|
+
max-height: 100%;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.markdown-image-lightbox-content img {
|
|
2591
|
+
display: block;
|
|
2592
|
+
max-width: 100%;
|
|
2593
|
+
max-height: calc(100vh - 64px);
|
|
2594
|
+
width: auto;
|
|
2595
|
+
height: auto;
|
|
2596
|
+
margin: 0;
|
|
2597
|
+
object-fit: contain;
|
|
2598
|
+
cursor: default;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
.markdown-image-lightbox-close {
|
|
2602
|
+
position: absolute;
|
|
2603
|
+
top: 12px;
|
|
2604
|
+
right: 12px;
|
|
2605
|
+
padding: 7px 10px;
|
|
2606
|
+
border: 1px solid color-mix(in srgb, white 60%, transparent);
|
|
2607
|
+
background: color-mix(in srgb, var(--ink) 72%, transparent);
|
|
2608
|
+
color: white;
|
|
2609
|
+
font-size: 12px;
|
|
2610
|
+
cursor: pointer;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
.image-preview-open {
|
|
2614
|
+
overflow: hidden;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
.markdown-body > img {
|
|
2502
2618
|
max-width: 100%;
|
|
2503
2619
|
height: auto;
|
|
2504
2620
|
margin: 34px auto;
|
|
@@ -3975,12 +4091,12 @@ button {
|
|
|
3975
4091
|
}
|
|
3976
4092
|
|
|
3977
4093
|
.archive-row.with-cover {
|
|
3978
|
-
grid-template-columns:
|
|
4094
|
+
grid-template-columns: 112px minmax(0, 1fr) 16px;
|
|
3979
4095
|
}
|
|
3980
4096
|
|
|
3981
4097
|
.archive-cover {
|
|
3982
|
-
width:
|
|
3983
|
-
height:
|
|
4098
|
+
width: 112px;
|
|
4099
|
+
height: auto;
|
|
3984
4100
|
}
|
|
3985
4101
|
|
|
3986
4102
|
.archive-meta {
|
|
@@ -4023,15 +4139,15 @@ button {
|
|
|
4023
4139
|
}
|
|
4024
4140
|
|
|
4025
4141
|
.column-feature-index {
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
grid-template-columns: 1fr auto;
|
|
4029
|
-
align-items: center;
|
|
4142
|
+
aspect-ratio: 4 / 5;
|
|
4143
|
+
min-height: 0;
|
|
4030
4144
|
}
|
|
4031
4145
|
|
|
4032
|
-
.column-feature-index
|
|
4033
|
-
|
|
4034
|
-
|
|
4146
|
+
.column-feature-index-label {
|
|
4147
|
+
padding: 20px;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
.column-feature-index-label strong {
|
|
4035
4151
|
font-size: 58px;
|
|
4036
4152
|
}
|
|
4037
4153
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by
|
|
1
|
+
// Generated by garden sync. Edit files in content/ instead.
|
|
2
2
|
export type MediaItem = {
|
|
3
3
|
type: "image" | "audio" | "video" | "link";
|
|
4
4
|
src: string;
|
|
@@ -33,7 +33,6 @@ export type ContentEntry = {
|
|
|
33
33
|
columnCoverAlt?: string;
|
|
34
34
|
order?: number;
|
|
35
35
|
cover?: string;
|
|
36
|
-
coverAlt?: string;
|
|
37
36
|
mediaType?: "image" | "audio" | "video" | "link" | "text";
|
|
38
37
|
media?: string | MediaItem[];
|
|
39
38
|
mediaAlt?: string;
|
|
@@ -42,11 +41,10 @@ export type ContentEntry = {
|
|
|
42
41
|
linkDescription?: string;
|
|
43
42
|
};
|
|
44
43
|
|
|
45
|
-
export const contentHash = "
|
|
44
|
+
export const contentHash = "86dad25b6dba";
|
|
46
45
|
export const posts = [
|
|
47
46
|
{
|
|
48
47
|
"title": "Markdown 复杂语法实验场",
|
|
49
|
-
"slug": "markdown-lab",
|
|
50
48
|
"description": "用一篇真实文章验证 Typora 常用语法、数学公式、Mermaid、脚注、表格与原生 HTML。",
|
|
51
49
|
"date": "2026-07-25T00:00:00.000Z",
|
|
52
50
|
"topic": "写作系统",
|
|
@@ -59,7 +57,7 @@ export const posts = [
|
|
|
59
57
|
"pinned": false,
|
|
60
58
|
"readingTime": "10 分钟",
|
|
61
59
|
"cover": "https://images.unsplash.com/photo-1761414500824-e280de5a1b37?auto=format&fit=crop&w=1600&q=84",
|
|
62
|
-
"
|
|
60
|
+
"slug": "markdown-lab",
|
|
63
61
|
"path": "markdown-lab",
|
|
64
62
|
"kind": "post",
|
|
65
63
|
"draft": false,
|
|
@@ -68,7 +66,6 @@ export const posts = [
|
|
|
68
66
|
},
|
|
69
67
|
{
|
|
70
68
|
"title": "注意力也是一种界面",
|
|
71
|
-
"slug": "attention-is-an-interface",
|
|
72
69
|
"description": "当工具越来越聪明,真正稀缺的不是功能,而是我们把注意力放在哪里。",
|
|
73
70
|
"date": "2026-07-21T00:00:00.000Z",
|
|
74
71
|
"updated": "2026-07-23T00:00:00.000Z",
|
|
@@ -80,7 +77,7 @@ export const posts = [
|
|
|
80
77
|
"pinned": true,
|
|
81
78
|
"readingTime": "6 分钟",
|
|
82
79
|
"cover": "https://images.unsplash.com/photo-1630234674857-527eb5534837?auto=format&fit=crop&w=1600&q=84",
|
|
83
|
-
"
|
|
80
|
+
"slug": "attention-is-an-interface",
|
|
84
81
|
"path": "attention-is-an-interface",
|
|
85
82
|
"kind": "post",
|
|
86
83
|
"draft": false,
|
|
@@ -89,7 +86,6 @@ export const posts = [
|
|
|
89
86
|
},
|
|
90
87
|
{
|
|
91
88
|
"title": "03 · 带着证据发布",
|
|
92
|
-
"slug": "ship-with-evidence",
|
|
93
89
|
"description": "“我改完了”不是完成,“用户可以确认它好了”才是。",
|
|
94
90
|
"date": "2026-07-18T00:00:00.000Z",
|
|
95
91
|
"topic": "独立开发",
|
|
@@ -98,6 +94,7 @@ export const posts = [
|
|
|
98
94
|
"发布"
|
|
99
95
|
],
|
|
100
96
|
"readingTime": "5 分钟",
|
|
97
|
+
"slug": "ship-with-evidence",
|
|
101
98
|
"path": "building-in-public/ship-with-evidence",
|
|
102
99
|
"kind": "post",
|
|
103
100
|
"draft": false,
|
|
@@ -106,7 +103,6 @@ export const posts = [
|
|
|
106
103
|
},
|
|
107
104
|
{
|
|
108
105
|
"title": "我为什么重新选择本地写作",
|
|
109
|
-
"slug": "local-first-writing",
|
|
110
106
|
"description": "Markdown 文件、可迁移的媒体资源,以及不被平台锁住的长期写作。",
|
|
111
107
|
"date": "2026-07-12T00:00:00.000Z",
|
|
112
108
|
"topic": "写作系统",
|
|
@@ -117,7 +113,7 @@ export const posts = [
|
|
|
117
113
|
"pinned": true,
|
|
118
114
|
"readingTime": "5 分钟",
|
|
119
115
|
"cover": "https://images.unsplash.com/photo-1501525771695-688643efeea4?auto=format&fit=crop&w=1600&q=84",
|
|
120
|
-
"
|
|
116
|
+
"slug": "local-first-writing",
|
|
121
117
|
"path": "local-first-writing",
|
|
122
118
|
"kind": "post",
|
|
123
119
|
"draft": false,
|
|
@@ -126,7 +122,6 @@ export const posts = [
|
|
|
126
122
|
},
|
|
127
123
|
{
|
|
128
124
|
"title": "02 · 让状态变得可靠",
|
|
129
|
-
"slug": "stable-state",
|
|
130
125
|
"description": "状态设计的关键,是明确谁拥有事实。",
|
|
131
126
|
"date": "2026-07-10T00:00:00.000Z",
|
|
132
127
|
"topic": "独立开发",
|
|
@@ -134,6 +129,7 @@ export const posts = [
|
|
|
134
129
|
"状态管理"
|
|
135
130
|
],
|
|
136
131
|
"readingTime": "6 分钟",
|
|
132
|
+
"slug": "stable-state",
|
|
137
133
|
"path": "building-in-public/stable-state",
|
|
138
134
|
"kind": "post",
|
|
139
135
|
"draft": false,
|
|
@@ -142,7 +138,6 @@ export const posts = [
|
|
|
142
138
|
},
|
|
143
139
|
{
|
|
144
140
|
"title": "01 · 从最小闭环开始",
|
|
145
|
-
"slug": "smallest-closed-loop",
|
|
146
141
|
"description": "先让最重要的一条路径真正跑通。",
|
|
147
142
|
"date": "2026-07-04T00:00:00.000Z",
|
|
148
143
|
"topic": "独立开发",
|
|
@@ -151,6 +146,7 @@ export const posts = [
|
|
|
151
146
|
"工程"
|
|
152
147
|
],
|
|
153
148
|
"readingTime": "7 分钟",
|
|
149
|
+
"slug": "smallest-closed-loop",
|
|
154
150
|
"path": "building-in-public/smallest-closed-loop",
|
|
155
151
|
"kind": "post",
|
|
156
152
|
"draft": false,
|
|
@@ -159,7 +155,6 @@ export const posts = [
|
|
|
159
155
|
},
|
|
160
156
|
{
|
|
161
157
|
"title": "做一个 PDF 阅读器之后,我重新理解了「位置」",
|
|
162
|
-
"slug": "building-a-pdf-reader",
|
|
163
158
|
"description": "阅读位置不是页码,而是一段连续体验的锚点。",
|
|
164
159
|
"date": "2026-06-28T00:00:00.000Z",
|
|
165
160
|
"topic": "构建手记",
|
|
@@ -170,7 +165,7 @@ export const posts = [
|
|
|
170
165
|
"pinned": false,
|
|
171
166
|
"readingTime": "8 分钟",
|
|
172
167
|
"cover": "https://images.unsplash.com/photo-1623276527153-fa38c1616b05?auto=format&fit=crop&w=1600&q=84",
|
|
173
|
-
"
|
|
168
|
+
"slug": "building-a-pdf-reader",
|
|
174
169
|
"path": "building-a-pdf-reader",
|
|
175
170
|
"kind": "post",
|
|
176
171
|
"draft": false,
|
|
@@ -179,7 +174,6 @@ export const posts = [
|
|
|
179
174
|
},
|
|
180
175
|
{
|
|
181
176
|
"title": "从逐词生成理解 KV Cache",
|
|
182
|
-
"slug": "understand-kv-cache",
|
|
183
177
|
"description": "不从公式开始,而是从模型下一次到底重复算了什么开始。",
|
|
184
178
|
"date": "2026-06-09T00:00:00.000Z",
|
|
185
179
|
"topic": "AI 学习",
|
|
@@ -190,7 +184,7 @@ export const posts = [
|
|
|
190
184
|
"pinned": false,
|
|
191
185
|
"readingTime": "9 分钟",
|
|
192
186
|
"cover": "https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=1600&q=84",
|
|
193
|
-
"
|
|
187
|
+
"slug": "understand-kv-cache",
|
|
194
188
|
"path": "understand-kv-cache",
|
|
195
189
|
"kind": "post",
|
|
196
190
|
"draft": false,
|
|
@@ -201,7 +195,6 @@ export const posts = [
|
|
|
201
195
|
export const columns = [
|
|
202
196
|
{
|
|
203
197
|
"title": "01 · 从最小闭环开始",
|
|
204
|
-
"slug": "smallest-closed-loop",
|
|
205
198
|
"description": "先让最重要的一条路径真正跑通。",
|
|
206
199
|
"date": "2026-07-04T00:00:00.000Z",
|
|
207
200
|
"topic": "独立开发",
|
|
@@ -210,6 +203,7 @@ export const columns = [
|
|
|
210
203
|
"工程"
|
|
211
204
|
],
|
|
212
205
|
"readingTime": "7 分钟",
|
|
206
|
+
"slug": "smallest-closed-loop",
|
|
213
207
|
"path": "building-in-public/smallest-closed-loop",
|
|
214
208
|
"kind": "column",
|
|
215
209
|
"draft": false,
|
|
@@ -223,7 +217,6 @@ export const columns = [
|
|
|
223
217
|
},
|
|
224
218
|
{
|
|
225
219
|
"title": "02 · 让状态变得可靠",
|
|
226
|
-
"slug": "stable-state",
|
|
227
220
|
"description": "状态设计的关键,是明确谁拥有事实。",
|
|
228
221
|
"date": "2026-07-10T00:00:00.000Z",
|
|
229
222
|
"topic": "独立开发",
|
|
@@ -231,6 +224,7 @@ export const columns = [
|
|
|
231
224
|
"状态管理"
|
|
232
225
|
],
|
|
233
226
|
"readingTime": "6 分钟",
|
|
227
|
+
"slug": "stable-state",
|
|
234
228
|
"path": "building-in-public/stable-state",
|
|
235
229
|
"kind": "column",
|
|
236
230
|
"draft": false,
|
|
@@ -244,7 +238,6 @@ export const columns = [
|
|
|
244
238
|
},
|
|
245
239
|
{
|
|
246
240
|
"title": "03 · 带着证据发布",
|
|
247
|
-
"slug": "ship-with-evidence",
|
|
248
241
|
"description": "“我改完了”不是完成,“用户可以确认它好了”才是。",
|
|
249
242
|
"date": "2026-07-18T00:00:00.000Z",
|
|
250
243
|
"topic": "独立开发",
|
|
@@ -253,6 +246,7 @@ export const columns = [
|
|
|
253
246
|
"发布"
|
|
254
247
|
],
|
|
255
248
|
"readingTime": "5 分钟",
|
|
249
|
+
"slug": "ship-with-evidence",
|
|
256
250
|
"path": "building-in-public/ship-with-evidence",
|
|
257
251
|
"kind": "column",
|
|
258
252
|
"draft": false,
|
|
@@ -268,7 +262,6 @@ export const columns = [
|
|
|
268
262
|
export const thoughts = [
|
|
269
263
|
{
|
|
270
264
|
"title": "雨后的城市",
|
|
271
|
-
"slug": "rainy-night",
|
|
272
265
|
"date": "2026-07-24T14:36:00.000Z",
|
|
273
266
|
"tags": [
|
|
274
267
|
"摄影"
|
|
@@ -307,6 +300,7 @@ export const thoughts = [
|
|
|
307
300
|
"description": "一条和这晚阅读有关、值得保留上下文的链接。"
|
|
308
301
|
}
|
|
309
302
|
],
|
|
303
|
+
"slug": "rainy-night",
|
|
310
304
|
"path": "rainy-night",
|
|
311
305
|
"kind": "thought",
|
|
312
306
|
"draft": false,
|
|
@@ -315,7 +309,6 @@ export const thoughts = [
|
|
|
315
309
|
},
|
|
316
310
|
{
|
|
317
311
|
"title": "今天的声音",
|
|
318
|
-
"slug": "field-recording",
|
|
319
312
|
"date": "2026-07-22T10:10:00.000Z",
|
|
320
313
|
"tags": [
|
|
321
314
|
"声音"
|
|
@@ -328,6 +321,7 @@ export const thoughts = [
|
|
|
328
321
|
"title": "今天的声音 · 循环片段"
|
|
329
322
|
}
|
|
330
323
|
],
|
|
324
|
+
"slug": "field-recording",
|
|
331
325
|
"path": "field-recording",
|
|
332
326
|
"kind": "thought",
|
|
333
327
|
"draft": false,
|
|
@@ -336,7 +330,6 @@ export const thoughts = [
|
|
|
336
330
|
},
|
|
337
331
|
{
|
|
338
332
|
"title": "慢一点看",
|
|
339
|
-
"slug": "slow-motion",
|
|
340
333
|
"date": "2026-07-19T01:20:00.000Z",
|
|
341
334
|
"tags": [
|
|
342
335
|
"影像"
|
|
@@ -349,6 +342,7 @@ export const thoughts = [
|
|
|
349
342
|
"poster": "https://images.unsplash.com/photo-1544655205-dcbeb868c648?auto=format&fit=crop&w=1400&q=82"
|
|
350
343
|
}
|
|
351
344
|
],
|
|
345
|
+
"slug": "slow-motion",
|
|
352
346
|
"path": "slow-motion",
|
|
353
347
|
"kind": "thought",
|
|
354
348
|
"draft": false,
|
|
@@ -357,7 +351,6 @@ export const thoughts = [
|
|
|
357
351
|
},
|
|
358
352
|
{
|
|
359
353
|
"title": "链接不是收藏,是路径",
|
|
360
|
-
"slug": "links-are-paths",
|
|
361
354
|
"date": "2026-07-16T04:05:00.000Z",
|
|
362
355
|
"tags": [
|
|
363
356
|
"阅读"
|
|
@@ -371,6 +364,7 @@ export const thoughts = [
|
|
|
371
364
|
"description": "让不同网站之间的回复、喜欢与引用重新连接起来。"
|
|
372
365
|
}
|
|
373
366
|
],
|
|
367
|
+
"slug": "links-are-paths",
|
|
374
368
|
"path": "links-are-paths",
|
|
375
369
|
"kind": "thought",
|
|
376
370
|
"draft": false,
|
|
@@ -72,17 +72,23 @@ function isMedia(value: unknown): value is MediaItem[] {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function
|
|
75
|
+
function fileStem(filePath: string) {
|
|
76
|
+
return (filePath.split("/").pop() ?? "").replace(/\.md$/u, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeThought(source: string, filePath: string, sha?: string): RepositoryThought | null {
|
|
76
80
|
const parsed = matter(source);
|
|
77
81
|
const data = parsed.data;
|
|
78
|
-
|
|
82
|
+
const slug = fileStem(filePath);
|
|
83
|
+
if (typeof data.title !== "string" || typeof data.date !== "string" || !slug) {
|
|
79
84
|
return null;
|
|
80
85
|
}
|
|
86
|
+
if (data.slug !== undefined && data.slug !== slug) return null;
|
|
81
87
|
const media = isMedia(data.media) ? data.media : [];
|
|
82
88
|
const status = data.draft === true ? "draft" : "published";
|
|
83
89
|
return {
|
|
84
|
-
id:
|
|
85
|
-
slug
|
|
90
|
+
id: slug,
|
|
91
|
+
slug,
|
|
86
92
|
title: data.title,
|
|
87
93
|
date: data.date,
|
|
88
94
|
tags: isStringArray(data.tags) ? data.tags : [],
|
|
@@ -90,7 +96,7 @@ function normalizeThought(source: string, path: string, sha?: string): Repositor
|
|
|
90
96
|
media,
|
|
91
97
|
kind: "thought",
|
|
92
98
|
draft: status === "draft",
|
|
93
|
-
sourcePath:
|
|
99
|
+
sourcePath: filePath,
|
|
94
100
|
content: parsed.content.trim(),
|
|
95
101
|
status,
|
|
96
102
|
sha,
|
|
@@ -108,7 +114,6 @@ function serializeThought(thought: {
|
|
|
108
114
|
}) {
|
|
109
115
|
return matter.stringify(thought.content.trim().concat("\n"), {
|
|
110
116
|
title: thought.title,
|
|
111
|
-
slug: thought.slug,
|
|
112
117
|
date: thought.date ?? new Date().toISOString(),
|
|
113
118
|
tags: thought.tags,
|
|
114
119
|
mediaType: thought.media[0]?.type ?? "text",
|
|
@@ -227,7 +232,7 @@ export async function updateRepositoryPostPinned(input: {
|
|
|
227
232
|
const existing = await readRepositoryFile(repositoryPath);
|
|
228
233
|
if (!existing) throw new Error("未找到对应博文。");
|
|
229
234
|
const parsed = matter(existing.source);
|
|
230
|
-
if (
|
|
235
|
+
if (fileStem(repositoryPath) !== input.slug) {
|
|
231
236
|
throw new Error("博文标识不匹配,已拒绝修改。");
|
|
232
237
|
}
|
|
233
238
|
const data = { ...parsed.data } as Record<string, unknown>;
|
package/app/page.tsx
CHANGED
|
@@ -9,10 +9,10 @@ import { columnHref, columns, formatDate, postHref, posts } from "./lib/content"
|
|
|
9
9
|
import { ThoughtCard } from "./components/ThoughtCard";
|
|
10
10
|
import { PageIntro } from "./components/PageIntro";
|
|
11
11
|
import { getPublishedThoughts } from "./lib/public-thoughts";
|
|
12
|
-
import { githubUrl, siteConfig } from "./site.config";
|
|
12
|
+
import { githubUrl, siteConfig, siteDocumentTitle } from "./site.config";
|
|
13
13
|
|
|
14
14
|
export const metadata = {
|
|
15
|
-
title:
|
|
15
|
+
title: siteDocumentTitle,
|
|
16
16
|
description: siteConfig.description,
|
|
17
17
|
};
|
|
18
18
|
|
package/app/rss.xml/route.ts
CHANGED
|
@@ -37,9 +37,7 @@ export async function GET() {
|
|
|
37
37
|
breaks: true,
|
|
38
38
|
});
|
|
39
39
|
const cover = entry.cover
|
|
40
|
-
? `<p><img src="${escapeXml(entry.cover)}" alt="${escapeXml(
|
|
41
|
-
entry.coverAlt ?? entry.title,
|
|
42
|
-
)}" /></p>`
|
|
40
|
+
? `<p><img src="${escapeXml(entry.cover)}" alt="${escapeXml(entry.title)}" /></p>`
|
|
43
41
|
: "";
|
|
44
42
|
return `
|
|
45
43
|
<item>
|
package/app/site.config.ts
CHANGED
|
@@ -33,3 +33,6 @@ export type SiteConfig = {
|
|
|
33
33
|
/** 站点身份来自根目录 site.config.yaml,由 garden sync 生成。 */
|
|
34
34
|
export const siteConfig = raw as SiteConfig;
|
|
35
35
|
export { githubUrl };
|
|
36
|
+
|
|
37
|
+
/** 列表页浏览器标题用站点名;页面大标题仍用 pages.*.title。 */
|
|
38
|
+
export const siteDocumentTitle = { absolute: siteConfig.name };
|
package/app/thoughts/page.tsx
CHANGED
|
@@ -2,11 +2,11 @@ import { getPublishedThoughts } from "../lib/public-thoughts";
|
|
|
2
2
|
import { ThoughtCard } from "../components/ThoughtCard";
|
|
3
3
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
4
4
|
import { PageIntro } from "../components/PageIntro";
|
|
5
|
-
import { siteConfig } from "../site.config";
|
|
5
|
+
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
6
6
|
|
|
7
7
|
export const metadata = {
|
|
8
|
-
title:
|
|
9
|
-
description: siteConfig.pages.thoughts.subtitle,
|
|
8
|
+
title: siteDocumentTitle,
|
|
9
|
+
description: siteConfig.pages.thoughts.subtitle || siteConfig.description,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export default function ThoughtsPage() {
|
package/package.json
CHANGED
|
@@ -71,12 +71,13 @@ const isExternalAsset = (value) =>
|
|
|
71
71
|
|
|
72
72
|
const assetCache = new Map();
|
|
73
73
|
|
|
74
|
-
function copyLocalAsset(value, markdownPath, sourcePath) {
|
|
74
|
+
function copyLocalAsset(value, markdownPath, sourcePath, options = {}) {
|
|
75
75
|
if (!value || isExternalAsset(value)) return value;
|
|
76
76
|
|
|
77
|
+
const baseDir = options.baseDir ?? path.dirname(markdownPath);
|
|
77
78
|
const [assetPath, suffix = ""] = value.split(/(?=[?#])/u, 2);
|
|
78
79
|
const decodedPath = decodeURIComponent(assetPath.replace(/^<|>$/g, ""));
|
|
79
|
-
const absoluteSource = path.resolve(
|
|
80
|
+
const absoluteSource = path.resolve(baseDir, decodedPath);
|
|
80
81
|
const relativeSource = path.relative(contentRoot, absoluteSource);
|
|
81
82
|
|
|
82
83
|
if (
|
|
@@ -85,7 +86,7 @@ function copyLocalAsset(value, markdownPath, sourcePath) {
|
|
|
85
86
|
!fs.existsSync(absoluteSource) ||
|
|
86
87
|
!fs.statSync(absoluteSource).isFile()
|
|
87
88
|
) {
|
|
88
|
-
fail(sourcePath, `找不到本地媒体文件 ${value}`);
|
|
89
|
+
fail(sourcePath, options.missingMessage ?? `找不到本地媒体文件 ${value}`);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if (!assetCache.has(absoluteSource)) {
|
|
@@ -254,9 +255,12 @@ function normalizeMedia(media, markdownPath, sourcePath) {
|
|
|
254
255
|
|
|
255
256
|
function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
256
257
|
const title = requireString(data, "title", sourcePath);
|
|
257
|
-
const slug =
|
|
258
|
+
const slug = path.basename(markdownPath, ".md");
|
|
258
259
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(slug)) {
|
|
259
|
-
fail(sourcePath, "
|
|
260
|
+
fail(sourcePath, "文件名只能包含小写字母、数字和连字符");
|
|
261
|
+
}
|
|
262
|
+
if (data.slug !== undefined) {
|
|
263
|
+
fail(sourcePath, "不要写 slug,文件名就是标识");
|
|
260
264
|
}
|
|
261
265
|
|
|
262
266
|
const date = normalizeDate(data.date, "date", sourcePath);
|
|
@@ -286,6 +290,7 @@ function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
|
286
290
|
|
|
287
291
|
const publishTime = Date.parse(publishAt ?? date);
|
|
288
292
|
if (!includeDrafts && (data.draft === true || publishTime > now)) return null;
|
|
293
|
+
delete data.coverAlt;
|
|
289
294
|
|
|
290
295
|
return {
|
|
291
296
|
...data,
|
|
@@ -301,7 +306,10 @@ function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
|
301
306
|
draft: Boolean(data.draft),
|
|
302
307
|
sourcePath,
|
|
303
308
|
cover: data.cover
|
|
304
|
-
? copyLocalAsset(data.cover, markdownPath, sourcePath
|
|
309
|
+
? copyLocalAsset(data.cover, markdownPath, sourcePath, {
|
|
310
|
+
baseDir: path.join(contentRoot, "covers"),
|
|
311
|
+
missingMessage: `找不到封面 ${data.cover};本地路径相对 content/covers`,
|
|
312
|
+
})
|
|
305
313
|
: undefined,
|
|
306
314
|
columnCover: data.columnCover
|
|
307
315
|
? copyLocalAsset(data.columnCover, markdownPath, sourcePath)
|
|
@@ -399,7 +407,7 @@ function readColumnReferences(posts) {
|
|
|
399
407
|
if (seenColumns.has(slug)) fail(sourcePath, `专栏 slug ${slug} 重复。`);
|
|
400
408
|
seenColumns.add(slug);
|
|
401
409
|
if (!Array.isArray(value.posts) || !value.posts.every((entry) => typeof entry === "string")) {
|
|
402
|
-
fail(sourcePath, "posts
|
|
410
|
+
fail(sourcePath, "posts 必须是博文相对路径数组。");
|
|
403
411
|
}
|
|
404
412
|
const referenced = new Set();
|
|
405
413
|
return value.posts.map((postReference, order) => {
|
|
@@ -487,7 +495,6 @@ export type ContentEntry = {
|
|
|
487
495
|
columnCoverAlt?: string;
|
|
488
496
|
order?: number;
|
|
489
497
|
cover?: string;
|
|
490
|
-
coverAlt?: string;
|
|
491
498
|
mediaType?: "image" | "audio" | "video" | "link" | "text";
|
|
492
499
|
media?: string | MediaItem[];
|
|
493
500
|
mediaAlt?: string;
|
package/scripts/new-post.mjs
CHANGED