hd-garden 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-interactive-chart/SKILL.md +84 -0
- package/.agents/skills/garden-interactive-chart/references/shell.html +49 -0
- package/.agents/skills/garden-interactive-chart/references/style.md +61 -0
- package/.env.example +56 -0
- package/.openai/hosting.json +5 -0
- package/LICENSE +21 -0
- package/Makefile +48 -0
- package/README.md +72 -0
- package/app/about/page.tsx +60 -0
- package/app/admin/page.tsx +117 -0
- package/app/api/admin/distribution/route.ts +120 -0
- package/app/api/admin/posts/pin/route.ts +32 -0
- package/app/api/admin/thoughts/[id]/route.ts +44 -0
- package/app/api/admin/thoughts/route.ts +120 -0
- package/app/api/admin/uploads/route.ts +20 -0
- package/app/api/auth/github/callback/route.ts +136 -0
- package/app/api/auth/github/route.ts +29 -0
- package/app/api/auth/logout/route.ts +13 -0
- package/app/blog/[...path]/page.tsx +135 -0
- package/app/blog/page.tsx +139 -0
- package/app/chatgpt-auth.ts +86 -0
- package/app/columns/[column]/[...path]/page.tsx +160 -0
- package/app/columns/page.tsx +68 -0
- package/app/components/AdminDashboard.tsx +410 -0
- package/app/components/Comments.tsx +55 -0
- package/app/components/ContentEmptyState.tsx +51 -0
- package/app/components/ExternalEmbed.tsx +127 -0
- package/app/components/GardenAdminGuide.tsx +116 -0
- package/app/components/GardenContentGuide.tsx +241 -0
- package/app/components/GardenMediaGuide.tsx +81 -0
- package/app/components/GiscusComments.tsx +71 -0
- package/app/components/GithubLiveOverview.tsx +118 -0
- package/app/components/GithubProfile.tsx +326 -0
- package/app/components/MarkdownArticle.tsx +144 -0
- package/app/components/MermaidDiagram.tsx +117 -0
- package/app/components/PageIntro.tsx +32 -0
- package/app/components/SiteChrome.tsx +18 -0
- package/app/components/SiteFooter.tsx +29 -0
- package/app/components/SiteHeader.tsx +420 -0
- package/app/components/TableOfContents.tsx +433 -0
- package/app/components/ThoughtCard.tsx +242 -0
- package/app/components/Webmentions.tsx +194 -0
- package/app/components/WidgetChart.tsx +222 -0
- package/app/garden/page.tsx +284 -0
- package/app/garden-modules.d.ts +62 -0
- package/app/globals.css +4454 -0
- package/app/layout.tsx +102 -0
- package/app/lib/admin-auth.ts +134 -0
- package/app/lib/content.ts +112 -0
- package/app/lib/fallback-site-config.ts +56 -0
- package/app/lib/generated-content.ts +380 -0
- package/app/lib/github-content.ts +244 -0
- package/app/lib/markdown-plugins.ts +13 -0
- package/app/lib/oss.ts +100 -0
- package/app/lib/public-thoughts.ts +9 -0
- package/app/lib/repository-path.mjs +32 -0
- package/app/lib/site.ts +51 -0
- package/app/page.tsx +119 -0
- package/app/robots.ts +15 -0
- package/app/rss.xml/route.ts +84 -0
- package/app/site.config.ts +35 -0
- package/app/sitemap.ts +46 -0
- package/app/thoughts/[slug]/page.tsx +79 -0
- package/app/thoughts/page.tsx +33 -0
- package/app/yaml.d.ts +4 -0
- package/bin/garden.mjs +348 -0
- package/build/sites-vite-plugin.ts +39 -0
- package/eslint.config.mjs +20 -0
- package/next.config.ts +13 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.svg +4 -0
- package/public/images/garden/admin-studio.png +0 -0
- package/public/og.png +0 -0
- package/scripts/build-content.mjs +582 -0
- package/scripts/fetch-embed.mjs +189 -0
- package/scripts/garden-paths.mjs +21 -0
- package/scripts/new-post.mjs +73 -0
- package/scripts/prepare-pages-output.mjs +182 -0
- package/scripts/send-webmentions.mjs +27 -0
- package/site.config.yaml +48 -0
- package/templates/site/Makefile +46 -0
- package/templates/site/README.md +13 -0
- package/templates/site/content/columns.yaml +1 -0
- package/templates/site/content/posts/.gitkeep +0 -0
- package/templates/site/content/thoughts/.gitkeep +0 -0
- package/templates/site/package.json +19 -0
- package/templates/site/site.config.yaml +46 -0
- package/tsconfig.json +34 -0
- package/vite.config.ts +130 -0
- package/worker/index.ts +53 -0
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist_Mono } from "next/font/google";
|
|
3
|
+
import "katex/dist/katex.min.css";
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
import { SiteChrome } from "./components/SiteChrome";
|
|
6
|
+
import {
|
|
7
|
+
getSiteUrl,
|
|
8
|
+
siteDescription,
|
|
9
|
+
webmentionEndpoint,
|
|
10
|
+
} from "./lib/site";
|
|
11
|
+
import { githubUrl, siteConfig } from "./site.config";
|
|
12
|
+
|
|
13
|
+
const geistMono = Geist_Mono({
|
|
14
|
+
variable: "--font-geist-mono",
|
|
15
|
+
subsets: ["latin"],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
19
|
+
const siteUrl = await getSiteUrl();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
metadataBase: new URL(siteUrl),
|
|
23
|
+
title: {
|
|
24
|
+
default: siteConfig.name,
|
|
25
|
+
template: `%s · ${siteConfig.name}`,
|
|
26
|
+
},
|
|
27
|
+
description: siteDescription,
|
|
28
|
+
authors: [{ name: siteConfig.author.name, url: githubUrl }],
|
|
29
|
+
alternates: {
|
|
30
|
+
types: {
|
|
31
|
+
"application/rss+xml": "/rss.xml",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
icons: {
|
|
35
|
+
icon: "/favicon.svg",
|
|
36
|
+
},
|
|
37
|
+
robots: {
|
|
38
|
+
index: true,
|
|
39
|
+
follow: true,
|
|
40
|
+
googleBot: {
|
|
41
|
+
index: true,
|
|
42
|
+
follow: true,
|
|
43
|
+
"max-image-preview": "large",
|
|
44
|
+
"max-snippet": -1,
|
|
45
|
+
"max-video-preview": -1,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
openGraph: {
|
|
49
|
+
title: siteConfig.name,
|
|
50
|
+
description: siteConfig.tagline,
|
|
51
|
+
type: "website",
|
|
52
|
+
locale: siteConfig.locale,
|
|
53
|
+
images: [
|
|
54
|
+
{
|
|
55
|
+
url: "/og.png",
|
|
56
|
+
width: 1731,
|
|
57
|
+
height: 909,
|
|
58
|
+
alt: `${siteConfig.name} · ${siteConfig.pages.home.title.replace("\n", "")}`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
twitter: {
|
|
63
|
+
card: "summary_large_image",
|
|
64
|
+
title: siteConfig.name,
|
|
65
|
+
description: siteConfig.tagline,
|
|
66
|
+
images: ["/og.png"],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function RootLayout({
|
|
72
|
+
children,
|
|
73
|
+
}: Readonly<{
|
|
74
|
+
children: React.ReactNode;
|
|
75
|
+
}>) {
|
|
76
|
+
const mentionEndpoint = webmentionEndpoint();
|
|
77
|
+
const analyticsDomain = process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN;
|
|
78
|
+
const analyticsScript =
|
|
79
|
+
process.env.NEXT_PUBLIC_ANALYTICS_SCRIPT ??
|
|
80
|
+
"https://plausible.io/js/script.js";
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<html lang="zh-CN" suppressHydrationWarning>
|
|
84
|
+
<head>
|
|
85
|
+
{mentionEndpoint && <link rel="webmention" href={mentionEndpoint} />}
|
|
86
|
+
{analyticsDomain && (
|
|
87
|
+
<script
|
|
88
|
+
defer
|
|
89
|
+
data-domain={analyticsDomain}
|
|
90
|
+
src={analyticsScript}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</head>
|
|
94
|
+
<body className={geistMono.variable}>
|
|
95
|
+
<a className="skip-link" href="#main-content">
|
|
96
|
+
跳到正文
|
|
97
|
+
</a>
|
|
98
|
+
<SiteChrome>{children}</SiteChrome>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { cookies } from "next/headers";
|
|
2
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
const SESSION_COOKIE = "hedon_admin_session";
|
|
5
|
+
const SESSION_LIFETIME_SECONDS = 60 * 60 * 24 * 7;
|
|
6
|
+
|
|
7
|
+
export type GitHubAdminUser = {
|
|
8
|
+
login: string;
|
|
9
|
+
avatarUrl: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type SessionPayload = GitHubAdminUser & { expiresAt: number };
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
|
|
16
|
+
function configuredLogins() {
|
|
17
|
+
return new Set(
|
|
18
|
+
(process.env.ADMIN_GITHUB_LOGINS ?? "")
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((login) => login.trim().toLowerCase())
|
|
21
|
+
.filter(Boolean),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isGitHubLoginAllowed(login: string) {
|
|
26
|
+
return configuredLogins().has(login.trim().toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isGitHubOAuthConfigured() {
|
|
30
|
+
return Boolean(
|
|
31
|
+
process.env.GITHUB_OAUTH_CLIENT_ID &&
|
|
32
|
+
process.env.GITHUB_OAUTH_CLIENT_SECRET &&
|
|
33
|
+
process.env.GITHUB_SESSION_SECRET,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toBase64Url(value: Uint8Array | string) {
|
|
38
|
+
const bytes = typeof value === "string" ? encoder.encode(value) : value;
|
|
39
|
+
let binary = "";
|
|
40
|
+
bytes.forEach((byte) => {
|
|
41
|
+
binary += String.fromCharCode(byte);
|
|
42
|
+
});
|
|
43
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fromBase64Url(value: string) {
|
|
47
|
+
const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(
|
|
48
|
+
Math.ceil(value.length / 4) * 4,
|
|
49
|
+
"=",
|
|
50
|
+
);
|
|
51
|
+
const binary = atob(padded);
|
|
52
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sign(value: string) {
|
|
56
|
+
const secret = process.env.GITHUB_SESSION_SECRET;
|
|
57
|
+
if (!secret) return null;
|
|
58
|
+
return toBase64Url(createHmac("sha256", secret).update(value).digest());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function signaturesMatch(actual: string, expected: string) {
|
|
62
|
+
const actualBytes = encoder.encode(actual);
|
|
63
|
+
const expectedBytes = encoder.encode(expected);
|
|
64
|
+
return (
|
|
65
|
+
actualBytes.byteLength === expectedBytes.byteLength &&
|
|
66
|
+
timingSafeEqual(actualBytes, expectedBytes)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function createAdminSession(user: GitHubAdminUser) {
|
|
71
|
+
const payload: SessionPayload = {
|
|
72
|
+
...user,
|
|
73
|
+
expiresAt: Date.now() + SESSION_LIFETIME_SECONDS * 1000,
|
|
74
|
+
};
|
|
75
|
+
const body = toBase64Url(JSON.stringify(payload));
|
|
76
|
+
const signature = sign(body);
|
|
77
|
+
return signature ? `${body}.${signature}` : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readSession(value: string | undefined): Promise<GitHubAdminUser | null> {
|
|
81
|
+
if (!value) return null;
|
|
82
|
+
const [body, signature, ...rest] = value.split(".");
|
|
83
|
+
if (!body || !signature || rest.length) return null;
|
|
84
|
+
const expected = sign(body);
|
|
85
|
+
if (!expected || !signaturesMatch(signature, expected)) return null;
|
|
86
|
+
try {
|
|
87
|
+
const payload = JSON.parse(
|
|
88
|
+
new TextDecoder().decode(fromBase64Url(body)),
|
|
89
|
+
) as SessionPayload;
|
|
90
|
+
if (
|
|
91
|
+
!payload.login ||
|
|
92
|
+
!payload.avatarUrl ||
|
|
93
|
+
payload.expiresAt < Date.now() ||
|
|
94
|
+
!isGitHubLoginAllowed(payload.login)
|
|
95
|
+
) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return { login: payload.login, avatarUrl: payload.avatarUrl };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function getAdminUser() {
|
|
105
|
+
const store = await cookies();
|
|
106
|
+
return readSession(store.get(SESSION_COOKIE)?.value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sessionCookie(value: string) {
|
|
110
|
+
return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${SESSION_LIFETIME_SECONDS}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function clearSessionCookie() {
|
|
114
|
+
return `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function requireAdminApi() {
|
|
118
|
+
const user = await getAdminUser();
|
|
119
|
+
if (!user) {
|
|
120
|
+
return Response.json({ error: "请先使用 GitHub 登录管理后台。" }, { status: 401 });
|
|
121
|
+
}
|
|
122
|
+
return user;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Reject browser cross-site state changes even if a cookie policy is weakened. */
|
|
126
|
+
export async function requireAdminMutation(request: Request) {
|
|
127
|
+
const origin = request.headers.get("origin");
|
|
128
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
129
|
+
const ownOrigin = new URL(request.url).origin;
|
|
130
|
+
if ((origin && origin !== ownOrigin) || fetchSite === "cross-site") {
|
|
131
|
+
return Response.json({ error: "不接受跨站后台请求。" }, { status: 403 });
|
|
132
|
+
}
|
|
133
|
+
return requireAdminApi();
|
|
134
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { markdownPlugins } from "./markdown-plugins";
|
|
2
|
+
import {
|
|
3
|
+
columns,
|
|
4
|
+
posts,
|
|
5
|
+
thoughts,
|
|
6
|
+
type ContentEntry,
|
|
7
|
+
type MediaItem,
|
|
8
|
+
} from "@garden/generated-content";
|
|
9
|
+
import GithubSlugger from "github-slugger";
|
|
10
|
+
import { toString } from "mdast-util-to-string";
|
|
11
|
+
import remarkParse from "remark-parse";
|
|
12
|
+
import { unified } from "unified";
|
|
13
|
+
import { visit } from "unist-util-visit";
|
|
14
|
+
|
|
15
|
+
export { columns, posts, thoughts };
|
|
16
|
+
export type { ContentEntry, MediaItem };
|
|
17
|
+
|
|
18
|
+
export const postHref = (post: Pick<ContentEntry, "path">) => `/blog/${post.path}`;
|
|
19
|
+
export const columnEntryPath = (entry: Pick<ContentEntry, "column" | "path">) => {
|
|
20
|
+
const prefix = entry.column ? `${entry.column}/` : "";
|
|
21
|
+
return prefix && entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path;
|
|
22
|
+
};
|
|
23
|
+
export const columnHref = (entry: Pick<ContentEntry, "column" | "path">) =>
|
|
24
|
+
`/columns/${entry.column}/${columnEntryPath(entry)}`;
|
|
25
|
+
|
|
26
|
+
export const formatDate = (value: string, withTime = false) =>
|
|
27
|
+
new Intl.DateTimeFormat("zh-CN", {
|
|
28
|
+
year: "numeric",
|
|
29
|
+
month: "2-digit",
|
|
30
|
+
day: "2-digit",
|
|
31
|
+
...(withTime ? { hour: "2-digit", minute: "2-digit" } : {}),
|
|
32
|
+
}).format(new Date(value));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses the same Markdown heading text that ReactMarkdown sends to
|
|
36
|
+
* rehype-slug. Every heading depth advances the slugger so headings outside
|
|
37
|
+
* the H2-H5 TOC range cannot shift the IDs shown in the directory.
|
|
38
|
+
*/
|
|
39
|
+
export const extractHeadings = (markdown: string) => {
|
|
40
|
+
const slugger = new GithubSlugger();
|
|
41
|
+
const headings: Array<{ depth: number; text: string; id: string }> = [];
|
|
42
|
+
const tree = unified().use(remarkParse).use(markdownPlugins).parse(markdown);
|
|
43
|
+
|
|
44
|
+
visit(tree, "heading", (node) => {
|
|
45
|
+
const text = toString(node).trim();
|
|
46
|
+
const id = slugger.slug(text);
|
|
47
|
+
if (text && node.depth >= 2 && node.depth <= 5) {
|
|
48
|
+
headings.push({ depth: node.depth, text, id });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return headings;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const toSearchableText = (markdown: string) =>
|
|
56
|
+
markdown
|
|
57
|
+
.replace(/```[^\n]*\n([\s\S]*?)```/g, " $1 ")
|
|
58
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, " $1 ")
|
|
59
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, " $1 ")
|
|
60
|
+
.replace(/<[^>]+>/g, " ")
|
|
61
|
+
.replace(/[#>*_~`|[\]{}()]/g, " ")
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
64
|
+
|
|
65
|
+
export const estimateWordCount = (markdown: string) => {
|
|
66
|
+
const text = toSearchableText(markdown);
|
|
67
|
+
const hanCharacters = text.match(/[\u3400-\u9fff]/gu)?.length ?? 0;
|
|
68
|
+
const otherWords = text
|
|
69
|
+
.replace(/[\u3400-\u9fff]/gu, " ")
|
|
70
|
+
.match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
|
|
71
|
+
return hanCharacters + otherWords;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const searchRecords = [
|
|
75
|
+
...posts.map((item) => ({
|
|
76
|
+
title: item.title,
|
|
77
|
+
description: item.description ?? "",
|
|
78
|
+
topic: item.topic ?? "博文",
|
|
79
|
+
path: postHref(item),
|
|
80
|
+
content: toSearchableText(item.content),
|
|
81
|
+
})),
|
|
82
|
+
...columns.map((item) => ({
|
|
83
|
+
title: item.title,
|
|
84
|
+
description: item.description ?? "",
|
|
85
|
+
topic: item.columnTitle ?? "专栏",
|
|
86
|
+
path: columnHref(item),
|
|
87
|
+
content: toSearchableText(item.content),
|
|
88
|
+
})),
|
|
89
|
+
...thoughts.map((item) => ({
|
|
90
|
+
title: item.title,
|
|
91
|
+
description: toSearchableText(item.content),
|
|
92
|
+
topic: "随想",
|
|
93
|
+
path: `/thoughts/${item.slug}`,
|
|
94
|
+
content: toSearchableText(item.content),
|
|
95
|
+
})),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
export const topics = Array.from(
|
|
99
|
+
new Set(posts.map((post) => post.topic).filter(Boolean)),
|
|
100
|
+
) as string[];
|
|
101
|
+
|
|
102
|
+
export const findPost = (path: string): ContentEntry | undefined =>
|
|
103
|
+
posts.find((post) => post.path === path);
|
|
104
|
+
|
|
105
|
+
export const findThought = (slug: string): ContentEntry | undefined =>
|
|
106
|
+
thoughts.find((thought) => thought.slug === slug);
|
|
107
|
+
|
|
108
|
+
export const findColumnEntry = (
|
|
109
|
+
column: string,
|
|
110
|
+
path: string,
|
|
111
|
+
): ContentEntry | undefined =>
|
|
112
|
+
columns.find((entry) => entry.column === column && columnEntryPath(entry) === path);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const siteConfig = {
|
|
2
|
+
name: "Garden",
|
|
3
|
+
tagline: "写作 · 构建 · 保持好奇",
|
|
4
|
+
description: "一个可 fork、可长期维护的 Markdown 个人博客系统。",
|
|
5
|
+
locale: "zh_CN",
|
|
6
|
+
author: {
|
|
7
|
+
name: "Your Name",
|
|
8
|
+
github: "your-github-id",
|
|
9
|
+
githubBio: "Write a short introduction for your GitHub profile card.",
|
|
10
|
+
githubPinned: [] as string[],
|
|
11
|
+
},
|
|
12
|
+
pages: {
|
|
13
|
+
home: {
|
|
14
|
+
title: "把复杂的事,\n慢慢想明白。",
|
|
15
|
+
subtitle: "在这里记录产品、工程、学习与日常生活中值得慢慢想明白的事。",
|
|
16
|
+
},
|
|
17
|
+
blog: {
|
|
18
|
+
title: "长期写下去,偶尔回头整理。",
|
|
19
|
+
subtitle: "这里是完整文章:有明确主题、有持续论证,也允许自己在更新里修正旧判断。",
|
|
20
|
+
},
|
|
21
|
+
thoughts: {
|
|
22
|
+
title: "不够写成文章的,也值得留下。",
|
|
23
|
+
subtitle: "私人朋友圈式的轻量记录:短文字、照片、声音、影像,以及偶然遇见的链接。",
|
|
24
|
+
},
|
|
25
|
+
columns: {
|
|
26
|
+
title: "把一个主题,写到足够深入。",
|
|
27
|
+
subtitle: "专栏不是标签集合,而是一条有顺序的阅读路径。左侧章节导航会在阅读时始终陪着你。",
|
|
28
|
+
},
|
|
29
|
+
about: {
|
|
30
|
+
title: "你好,我是 Your Name。",
|
|
31
|
+
subtitle: "在这里写下你关心的问题、长期的方向,以及你希望如何与世界保持联系。",
|
|
32
|
+
heading: "这个网站为什么存在",
|
|
33
|
+
paragraphs: [
|
|
34
|
+
"我需要一个不被信息流推着走的地方。长文可以慢慢展开,随想可以轻轻放下;同一个主题也可以沿着清晰的顺序持续生长。",
|
|
35
|
+
"所有文章都从本地 Markdown 文件开始。这意味着我可以继续在 Typora 里写作,也意味着内容不会被某个编辑器或平台锁住。",
|
|
36
|
+
],
|
|
37
|
+
quote: "写作不是把已经想明白的东西记下来,而是在句子之间发现自己还没有想明白什么。",
|
|
38
|
+
focus: [
|
|
39
|
+
"将自己的关注方向写在这里",
|
|
40
|
+
"用文章积累可迁移的知识",
|
|
41
|
+
"为长期写作留出稳定的空间",
|
|
42
|
+
],
|
|
43
|
+
cta: {
|
|
44
|
+
href: "/blog",
|
|
45
|
+
label: "从博文开始",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
garden: {
|
|
49
|
+
title: "一个基于 Markdown 的个人博客框架。",
|
|
50
|
+
subtitle: "",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
footer: "保持独立,持续写作。",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const githubUrl = `https://github.com/${siteConfig.author.github}`;
|