@hutusi/amytis 1.6.0 → 1.7.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/GEMINI.md +6 -2
- package/TODO.md +11 -15
- package/bun.lock +3 -0
- package/content/about.mdx +1 -0
- package/content/about.zh.mdx +21 -0
- package/content/flows/2026/02/20.md +16 -0
- package/content/links.mdx +42 -0
- package/content/links.zh.mdx +41 -0
- package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
- package/content/posts/multimedia-showcase/index.mdx +261 -0
- package/content/privacy.mdx +32 -0
- package/content/privacy.zh.mdx +32 -0
- package/docs/ARCHITECTURE.md +8 -0
- package/package.json +2 -1
- package/scripts/copy-assets.ts +1 -1
- package/site.config.ts +126 -44
- package/src/app/[slug]/page.tsx +0 -10
- package/src/app/archive/page.tsx +38 -10
- package/src/app/books/[slug]/page.tsx +18 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +20 -3
- package/src/app/layout.tsx +47 -21
- package/src/app/page.tsx +135 -72
- package/src/app/posts/[slug]/page.tsx +6 -12
- package/src/app/series/[slug]/page.tsx +18 -0
- package/src/app/subscribe/page.tsx +17 -0
- package/src/app/tags/[tag]/page.tsx +9 -26
- package/src/app/tags/page.tsx +3 -8
- package/src/components/AuthorCard.tsx +43 -0
- package/src/components/Comments.tsx +20 -4
- package/src/components/ExternalLinks.tsx +6 -2
- package/src/components/Footer.tsx +35 -26
- package/src/components/LanguageProvider.tsx +0 -5
- package/src/components/LanguageSwitch.tsx +117 -6
- package/src/components/LocaleSwitch.tsx +33 -0
- package/src/components/Navbar.tsx +31 -8
- package/src/components/PostNavigation.tsx +55 -0
- package/src/components/PostSidebar.tsx +172 -126
- package/src/components/ReadingProgressBar.tsx +6 -21
- package/src/components/RelatedPosts.tsx +1 -1
- package/src/components/Search.tsx +24 -4
- package/src/components/SelectedBooksSection.tsx +12 -6
- package/src/components/ShareBar.tsx +115 -0
- package/src/components/SimpleLayoutHeader.tsx +5 -14
- package/src/components/SubscribePage.tsx +298 -0
- package/src/components/TagContentTabs.tsx +103 -0
- package/src/components/TagPageHeader.tsx +7 -13
- package/src/components/TagSidebar.tsx +142 -0
- package/src/components/TagsIndexClient.tsx +156 -0
- package/src/hooks/useScrollY.ts +41 -0
- package/src/i18n/translations.ts +84 -0
- package/src/layouts/PostLayout.tsx +33 -6
- package/src/layouts/SimpleLayout.tsx +53 -15
- package/src/lib/markdown.ts +71 -15
- package/src/components/TableOfContents.tsx +0 -158
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "隐私政策"
|
|
3
|
+
date: "2026-02-20"
|
|
4
|
+
excerpt: "本站如何收集和处理您的数据。"
|
|
5
|
+
layout: "simple"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 隐私政策
|
|
9
|
+
|
|
10
|
+
您的隐私对我们很重要。本页面说明您在访问本站时收集了哪些数据,以及这些数据如何被使用。
|
|
11
|
+
|
|
12
|
+
## 数据分析
|
|
13
|
+
|
|
14
|
+
本站使用 **Umami**,一个注重隐私的开源分析平台。Umami 不使用 Cookie,不跨网站追踪用户,也不收集任何个人身份信息。所收集的数据仅限于匿名页面访问统计,如来源网站、浏览器类型和所在国家,仅用于了解哪些内容最受欢迎。
|
|
15
|
+
|
|
16
|
+
数据不会被出售或与任何第三方共享。
|
|
17
|
+
|
|
18
|
+
## 评论
|
|
19
|
+
|
|
20
|
+
文章评论由 **Giscus** 提供支持,基于 GitHub Discussions 实现。留言需要使用 GitHub 账号登录。您的评论和 GitHub 用户名由 GitHub 存储,受 [GitHub 隐私政策](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement)约束。
|
|
21
|
+
|
|
22
|
+
如果您没有 GitHub 账号或不希望使用,也可以通过页脚中的电子邮件直接联系我。
|
|
23
|
+
|
|
24
|
+
## 外部链接
|
|
25
|
+
|
|
26
|
+
本站可能包含指向外部网站的链接。一旦您离开本站,这些第三方网站的隐私实践将适用,超出本站的控制范围。
|
|
27
|
+
|
|
28
|
+
## 联系我们
|
|
29
|
+
|
|
30
|
+
如果您对本隐私政策有任何疑问,请通过页脚中的电子邮件地址联系我们。
|
|
31
|
+
|
|
32
|
+
*最后更新:2026 年 2 月。*
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -50,6 +50,10 @@ src/app/
|
|
|
50
50
|
[author]/page.tsx # Posts by author
|
|
51
51
|
archive/
|
|
52
52
|
page.tsx # Chronological archive
|
|
53
|
+
subscribe/
|
|
54
|
+
page.tsx # Subscription options
|
|
55
|
+
search.json/
|
|
56
|
+
route.ts # Supplementary structured search data
|
|
53
57
|
[slug]/page.tsx # Static pages (about, etc.)
|
|
54
58
|
```
|
|
55
59
|
|
|
@@ -60,6 +64,7 @@ src/app/
|
|
|
60
64
|
- **`Footer`** - Social links, copyright, site metadata, and language switch.
|
|
61
65
|
- **`Hero`** - Homepage hero with collapsible intro.
|
|
62
66
|
- **`BookLayout`** - Dedicated layout for book chapters with sidebar navigation.
|
|
67
|
+
- **`PageHeader`** - Consistent header for simple pages (Archive, Tags, About).
|
|
63
68
|
|
|
64
69
|
### Content Display
|
|
65
70
|
- **`PostList`** - Card-based post listing.
|
|
@@ -67,6 +72,8 @@ src/app/
|
|
|
67
72
|
- **`SeriesCatalog`** - Timeline-style post listing for series.
|
|
68
73
|
- **`PostCard`** - Individual post preview card.
|
|
69
74
|
- **`CoverImage`** - Optimized image component using `next-image-export-optimizer` with dynamic desaturated gradients.
|
|
75
|
+
- **`ShareBar`** - Social sharing buttons for posts and books.
|
|
76
|
+
- **`TagContentTabs`** - Tabbed interface for filtering posts vs flows by tag.
|
|
70
77
|
|
|
71
78
|
### Content Rendering
|
|
72
79
|
- **`MarkdownRenderer`** - Core rendering component using `react-markdown` with plugins (GFM, Math, Raw HTML).
|
|
@@ -78,6 +85,7 @@ src/app/
|
|
|
78
85
|
- **`Search`** - Full-text search modal (Cmd/Ctrl+K) powered by Pagefind. Supports type filter tabs (All/Post/Flow/Book), recent searches, keyboard navigation, debounced input, focus trap, and ARIA accessibility. Search syntax: `"exact phrase"`, `word1 word2` (AND), `-exclude`.
|
|
79
86
|
- **`Pagination`** - Previous/Next page navigation.
|
|
80
87
|
- **`ReadingProgressBar`** - Top-of-page progress bar for book chapters.
|
|
88
|
+
- **`SubscribePage`** - Centralized page for all subscription options.
|
|
81
89
|
|
|
82
90
|
## Data Access Layer (`src/lib/markdown.ts`)
|
|
83
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hutusi/amytis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"next-themes": "^0.4.6",
|
|
45
45
|
"react": "19.2.4",
|
|
46
46
|
"react-dom": "19.2.4",
|
|
47
|
+
"react-icons": "^5.5.0",
|
|
47
48
|
"react-markdown": "^10.1.0",
|
|
48
49
|
"react-syntax-highlighter": "^16.1.0",
|
|
49
50
|
"rehype-katex": "^7.0.1",
|
package/scripts/copy-assets.ts
CHANGED
|
@@ -51,7 +51,7 @@ function getSlugFromFilename(filename: string): string {
|
|
|
51
51
|
const dateRegex = /^(\d{4}-\d{2}-\d{2})-(.*)$/;
|
|
52
52
|
const match = nameWithoutExt.match(dateRegex);
|
|
53
53
|
|
|
54
|
-
if (match && !siteConfig.includeDateInUrl) {
|
|
54
|
+
if (match && !siteConfig.posts?.includeDateInUrl) {
|
|
55
55
|
return match[2];
|
|
56
56
|
}
|
|
57
57
|
return nameWithoutExt;
|
package/site.config.ts
CHANGED
|
@@ -1,67 +1,140 @@
|
|
|
1
|
+
export interface NavItem {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
weight: number;
|
|
5
|
+
dropdown?: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Defined up-front so footer.connect can reference these URLs without duplication
|
|
9
|
+
const social = {
|
|
10
|
+
github: "https://github.com/hutusi/amytis",
|
|
11
|
+
twitter: "https://twitter.com/hutusi",
|
|
12
|
+
email: "mailto:huziyong@gmail.com",
|
|
13
|
+
};
|
|
14
|
+
|
|
1
15
|
export const siteConfig = {
|
|
16
|
+
|
|
17
|
+
// ── Site identity ─────────────────────────────────────────────────────────
|
|
2
18
|
title: { en: "Amytis", zh: "Amytis" },
|
|
3
19
|
description: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
|
|
4
20
|
baseUrl: "https://example.com", // Replace with your actual domain
|
|
21
|
+
ogImage: "/og-image.png", // Default OG/social preview image — place a 1200×630 PNG at public/og-image.png
|
|
5
22
|
footerText: { en: `© ${new Date().getFullYear()} Amytis. All rights reserved.`, zh: `© ${new Date().getFullYear()} Amytis. 保留所有权利。` },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{ name: "Archive", url: "/archive", weight: 2 },
|
|
12
|
-
{ name: "Tags", url: "/tags", weight: 3 },
|
|
13
|
-
{ name: "About", url: "/about", weight: 4 },
|
|
14
|
-
],
|
|
15
|
-
social: {
|
|
16
|
-
github: "https://github.com/hutusi/amytis",
|
|
17
|
-
twitter: "https://twitter.com/hutusi",
|
|
18
|
-
email: "mailto:huziyong@gmail.com",
|
|
23
|
+
|
|
24
|
+
// ── i18n ──────────────────────────────────────────────────────────────────
|
|
25
|
+
i18n: {
|
|
26
|
+
defaultLocale: 'en',
|
|
27
|
+
locales: ['en', 'zh'],
|
|
19
28
|
},
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
|
|
30
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
31
|
+
nav: [
|
|
32
|
+
{ name: "Flow", url: "/flows", weight: 1 },
|
|
33
|
+
{ name: "Posts", url: "/posts", weight: 2 },
|
|
34
|
+
{ name: "Series", url: "/series", weight: 3, dropdown: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"] },
|
|
35
|
+
{ name: "Books", url: "/books", weight: 4, dropdown: [] },
|
|
36
|
+
{ name: "About", url: "/about", weight: 5 },
|
|
37
|
+
] as NavItem[],
|
|
38
|
+
|
|
39
|
+
// ── Footer ────────────────────────────────────────────────────────────────
|
|
40
|
+
footer: {
|
|
41
|
+
explore: [
|
|
42
|
+
{ name: "Archive", url: "/archive", weight: 1 },
|
|
43
|
+
{ name: "Tags", url: "/tags", weight: 2 },
|
|
44
|
+
{ name: "Links", url: "/links", weight: 3 },
|
|
45
|
+
{ name: "About", url: "/about", weight: 4 },
|
|
46
|
+
],
|
|
47
|
+
connect: [
|
|
48
|
+
{ name: "GitHub", url: social.github, weight: 1 },
|
|
49
|
+
{ name: "Twitter", url: social.twitter, weight: 2 },
|
|
50
|
+
{ name: "RSS Feed", url: "/feed.xml", weight: 3 },
|
|
51
|
+
{ name: "Subscribe", url: "/subscribe", weight: 4 },
|
|
52
|
+
],
|
|
53
|
+
builtWith: {
|
|
54
|
+
show: true,
|
|
55
|
+
url: "https://github.com/hutusi/amytis",
|
|
56
|
+
text: { en: "Built with Amytis", zh: "基于 Amytis 构建" },
|
|
57
|
+
},
|
|
22
58
|
},
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
|
|
60
|
+
// ── Social & sharing ──────────────────────────────────────────────────────
|
|
61
|
+
social,
|
|
62
|
+
share: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
// Supported: twitter, facebook, linkedin, weibo, reddit, hackernews,
|
|
65
|
+
// telegram, bluesky, mastodon, douban, zhihu, copy
|
|
66
|
+
platforms: ['twitter', 'facebook', 'linkedin', 'weibo', 'copy'],
|
|
25
67
|
},
|
|
26
|
-
|
|
27
|
-
|
|
68
|
+
subscribe: {
|
|
69
|
+
substack: '', // Substack publication URL, e.g., 'https://yourname.substack.com'
|
|
70
|
+
telegram: '', // Telegram channel URL, e.g., 'https://t.me/yourchannel'
|
|
71
|
+
wechat: {
|
|
72
|
+
qrCode: '', // Path to QR image in public/, e.g., '/images/wechat-qr.png'
|
|
73
|
+
account: '', // WeChat official account ID/name shown below QR
|
|
74
|
+
},
|
|
75
|
+
email: '', // Newsletter/mailing list URL (distinct from social.email contact address)
|
|
28
76
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
|
|
78
|
+
// ── Features ──────────────────────────────────────────────────────────────
|
|
79
|
+
features: {
|
|
80
|
+
posts: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
name: { en: "Posts", zh: "文章" },
|
|
83
|
+
},
|
|
84
|
+
series: {
|
|
85
|
+
enabled: true,
|
|
86
|
+
name: { en: "Series", zh: "系列" },
|
|
87
|
+
},
|
|
88
|
+
books: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
name: { en: "Books", zh: "书籍" },
|
|
91
|
+
},
|
|
92
|
+
flows: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
name: { en: "Flow", zh: "随笔" },
|
|
95
|
+
},
|
|
33
96
|
},
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
showFuturePosts: false,
|
|
37
|
-
toc: true,
|
|
38
|
-
themeColor: 'default', // 'default' | 'blue' | 'rose' | 'amber'
|
|
97
|
+
|
|
98
|
+
// ── Homepage ──────────────────────────────────────────────────────────────
|
|
39
99
|
hero: {
|
|
40
100
|
tagline: { en: "Digital Garden", zh: "数字花园" },
|
|
41
101
|
title: { en: "Cultivating Digital Knowledge", zh: "培育数字知识" },
|
|
42
102
|
subtitle: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
|
|
43
103
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
homepage: {
|
|
105
|
+
sections: [
|
|
106
|
+
{ id: 'hero', enabled: true, weight: 1 },
|
|
107
|
+
{ id: 'featured-series', enabled: true, weight: 2, maxItems: 6, scrollThreshold: 2 },
|
|
108
|
+
{ id: 'featured-books', enabled: true, weight: 3, maxItems: 4, scrollThreshold: 2 },
|
|
109
|
+
{ id: 'featured-posts', enabled: true, weight: 4, maxItems: 4, scrollThreshold: 1 },
|
|
110
|
+
{ id: 'latest-posts', enabled: true, weight: 5, maxItems: 5 },
|
|
111
|
+
{ id: 'recent-flows', enabled: true, weight: 6, maxItems: 5 },
|
|
112
|
+
],
|
|
47
113
|
},
|
|
48
|
-
|
|
49
|
-
|
|
114
|
+
|
|
115
|
+
// ── Content ───────────────────────────────────────────────────────────────
|
|
116
|
+
pagination: {
|
|
117
|
+
posts: 5,
|
|
118
|
+
series: 1,
|
|
119
|
+
flows: 20,
|
|
50
120
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
maxItems: 4,
|
|
121
|
+
posts: {
|
|
122
|
+
toc: true,
|
|
123
|
+
showFuturePosts: false,
|
|
124
|
+
includeDateInUrl: false,
|
|
125
|
+
// trailingSlash is configured in next.config.ts (Next.js handles URL normalization)
|
|
126
|
+
archive: {
|
|
127
|
+
showAuthors: true,
|
|
59
128
|
},
|
|
60
129
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
locales: ['en', 'zh'],
|
|
130
|
+
flows: {
|
|
131
|
+
recentCount: 5,
|
|
64
132
|
},
|
|
133
|
+
|
|
134
|
+
// ── Appearance ────────────────────────────────────────────────────────────
|
|
135
|
+
themeColor: 'default', // 'default' | 'blue' | 'rose' | 'amber'
|
|
136
|
+
|
|
137
|
+
// ── Analytics ─────────────────────────────────────────────────────────────
|
|
65
138
|
analytics: {
|
|
66
139
|
provider: 'umami', // 'umami' | 'plausible' | 'google' | null
|
|
67
140
|
umami: {
|
|
@@ -76,6 +149,8 @@ export const siteConfig = {
|
|
|
76
149
|
measurementId: '', // G-XXXXXXXXXX
|
|
77
150
|
},
|
|
78
151
|
},
|
|
152
|
+
|
|
153
|
+
// ── Comments ──────────────────────────────────────────────────────────────
|
|
79
154
|
comments: {
|
|
80
155
|
provider: 'giscus', // 'giscus' | 'disqus' | null
|
|
81
156
|
giscus: {
|
|
@@ -88,4 +163,11 @@ export const siteConfig = {
|
|
|
88
163
|
shortname: '',
|
|
89
164
|
},
|
|
90
165
|
},
|
|
166
|
+
|
|
167
|
+
// ── Authors ───────────────────────────────────────────────────────────────
|
|
168
|
+
authors: {
|
|
169
|
+
// Map display name (as used in post frontmatter) to author profile
|
|
170
|
+
// "Author Name": { bio: "Short bio shown in author card below each post." },
|
|
171
|
+
} as Record<string, { bio?: string }>,
|
|
172
|
+
|
|
91
173
|
};
|
package/src/app/[slug]/page.tsx
CHANGED
|
@@ -53,15 +53,5 @@ export default async function Page({
|
|
|
53
53
|
return <PostLayout post={page} />;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
if (slug === 'about' && siteConfig.about) {
|
|
57
|
-
return (
|
|
58
|
-
<SimpleLayout
|
|
59
|
-
post={page}
|
|
60
|
-
titleOverride={siteConfig.about.title}
|
|
61
|
-
subtitleOverride={siteConfig.about.subtitle}
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
56
|
return <SimpleLayout post={page} />;
|
|
67
57
|
}
|
package/src/app/archive/page.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Link from 'next/link';
|
|
2
|
-
import { getAllPosts, PostData } from '@/lib/markdown';
|
|
2
|
+
import { getAllPosts, getSeriesData, PostData } from '@/lib/markdown';
|
|
3
3
|
import { siteConfig } from '../../../site.config';
|
|
4
|
-
import { resolveLocale } from '@/lib/i18n';
|
|
4
|
+
import { resolveLocale, t } from '@/lib/i18n';
|
|
5
5
|
import PageHeader from '@/components/PageHeader';
|
|
6
6
|
|
|
7
7
|
export const metadata = {
|
|
@@ -41,12 +41,20 @@ function groupPostsByDate(posts: PostData[]): GroupedPosts {
|
|
|
41
41
|
export default function ArchivePage() {
|
|
42
42
|
const posts = getAllPosts();
|
|
43
43
|
const groupedPosts = groupPostsByDate(posts);
|
|
44
|
-
const showAuthors = siteConfig.archive?.showAuthors;
|
|
44
|
+
const showAuthors = siteConfig.posts?.archive?.showAuthors;
|
|
45
45
|
|
|
46
46
|
// Sort years descending to show newest content first
|
|
47
47
|
const years = Object.keys(groupedPosts).sort((a, b) => Number(b) - Number(a));
|
|
48
48
|
const totalPosts = posts.length;
|
|
49
49
|
|
|
50
|
+
// Build series slug → title map (one lookup per unique series)
|
|
51
|
+
const seriesSlugs = [...new Set(posts.filter(p => p.series).map(p => p.series!))];
|
|
52
|
+
const seriesTitleMap: Record<string, string> = {};
|
|
53
|
+
for (const slug of seriesSlugs) {
|
|
54
|
+
const data = getSeriesData(slug);
|
|
55
|
+
if (data) seriesTitleMap[slug] = data.title;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
return (
|
|
51
59
|
<div className="layout-main">
|
|
52
60
|
<PageHeader
|
|
@@ -57,7 +65,25 @@ export default function ArchivePage() {
|
|
|
57
65
|
subtitleParams={{ count: totalPosts, years: years.length }}
|
|
58
66
|
/>
|
|
59
67
|
|
|
60
|
-
<main
|
|
68
|
+
<main>
|
|
69
|
+
{/* Year-jump navigation — mirrors content grid to align with timeline column */}
|
|
70
|
+
{years.length > 1 && (
|
|
71
|
+
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16 mb-16">
|
|
72
|
+
<div />
|
|
73
|
+
<nav aria-label="Jump to year" className="flex flex-wrap items-center gap-2">
|
|
74
|
+
{years.map(year => (
|
|
75
|
+
<a
|
|
76
|
+
key={year}
|
|
77
|
+
href={`#${year}`}
|
|
78
|
+
className="text-xs font-mono text-muted hover:text-accent border border-muted/20 hover:border-accent/40 rounded px-3 py-1 transition-all duration-200 no-underline"
|
|
79
|
+
>
|
|
80
|
+
{year}
|
|
81
|
+
</a>
|
|
82
|
+
))}
|
|
83
|
+
</nav>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
61
87
|
<div className="space-y-24">
|
|
62
88
|
{years.map((year) => {
|
|
63
89
|
// Sort months within the year in descending order (December -> January)
|
|
@@ -67,7 +93,7 @@ export default function ArchivePage() {
|
|
|
67
93
|
const yearTotal = months.reduce((total, month) => total + groupedPosts[year][month].length, 0);
|
|
68
94
|
|
|
69
95
|
return (
|
|
70
|
-
<section key={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
|
|
96
|
+
<section key={year} id={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
|
|
71
97
|
{/* Year Marker */}
|
|
72
98
|
<div className="relative">
|
|
73
99
|
<div className="sticky top-24 lg:top-32 text-left md:text-right">
|
|
@@ -75,7 +101,7 @@ export default function ArchivePage() {
|
|
|
75
101
|
{year}
|
|
76
102
|
</h2>
|
|
77
103
|
<span className="block text-xs font-bold uppercase tracking-widest text-muted mt-2">
|
|
78
|
-
{yearTotal}
|
|
104
|
+
{yearTotal} {t('posts')}
|
|
79
105
|
</span>
|
|
80
106
|
</div>
|
|
81
107
|
</div>
|
|
@@ -91,7 +117,9 @@ export default function ArchivePage() {
|
|
|
91
117
|
|
|
92
118
|
<h3 className="text-base font-sans font-bold uppercase tracking-widest text-accent mb-8">
|
|
93
119
|
{getMonthName(Number(month))}
|
|
94
|
-
<span className="ml-2 text-
|
|
120
|
+
<span className="ml-2 inline-flex items-center text-[10px] font-mono text-muted/60 bg-muted/10 rounded px-1.5 py-0.5 align-middle leading-none">
|
|
121
|
+
{monthPosts.length}
|
|
122
|
+
</span>
|
|
95
123
|
</h3>
|
|
96
124
|
|
|
97
125
|
<ul className="space-y-6">
|
|
@@ -113,10 +141,10 @@ export default function ArchivePage() {
|
|
|
113
141
|
</h4>
|
|
114
142
|
{post.series && (
|
|
115
143
|
<span
|
|
116
|
-
title={post.series}
|
|
117
|
-
className="text-[10px] font-sans font-medium uppercase tracking-wider text-accent/60 border border-accent/20 rounded px-1.5 py-0.5 shrink-0 leading-none max-w-[
|
|
144
|
+
title={seriesTitleMap[post.series] ?? post.series}
|
|
145
|
+
className="text-[10px] font-sans font-medium uppercase tracking-wider text-accent/60 border border-accent/20 rounded px-1.5 py-0.5 shrink-0 leading-none max-w-[14ch] truncate inline-block align-baseline"
|
|
118
146
|
>
|
|
119
|
-
{post.series}
|
|
147
|
+
{seriesTitleMap[post.series] ?? post.series}
|
|
120
148
|
</span>
|
|
121
149
|
)}
|
|
122
150
|
</div>
|
|
@@ -22,9 +22,27 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|
|
22
22
|
return { title: 'Book Not Found' };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const ogImage = book.coverImage && !book.coverImage.startsWith('text:') && !book.coverImage.startsWith('./')
|
|
26
|
+
? book.coverImage
|
|
27
|
+
: siteConfig.ogImage;
|
|
28
|
+
|
|
25
29
|
return {
|
|
26
30
|
title: `${book.title} | ${resolveLocale(siteConfig.title)}`,
|
|
27
31
|
description: book.excerpt,
|
|
32
|
+
openGraph: {
|
|
33
|
+
title: book.title,
|
|
34
|
+
description: book.excerpt,
|
|
35
|
+
type: 'website',
|
|
36
|
+
url: `${siteConfig.baseUrl}/books/${slug}`,
|
|
37
|
+
siteName: resolveLocale(siteConfig.title),
|
|
38
|
+
images: [{ url: ogImage, width: 1200, height: 630, alt: book.title }],
|
|
39
|
+
},
|
|
40
|
+
twitter: {
|
|
41
|
+
card: ogImage !== siteConfig.ogImage ? 'summary_large_image' : 'summary',
|
|
42
|
+
title: book.title,
|
|
43
|
+
description: book.excerpt,
|
|
44
|
+
images: [ogImage],
|
|
45
|
+
},
|
|
28
46
|
};
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
|
|
|
5
5
|
import { t, resolveLocale } from '@/lib/i18n';
|
|
6
6
|
import FlowCalendarSidebar from '@/components/FlowCalendarSidebar';
|
|
7
7
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
8
|
+
import ShareBar from '@/components/ShareBar';
|
|
8
9
|
import Link from 'next/link';
|
|
9
10
|
|
|
10
11
|
export function generateStaticParams() {
|
|
@@ -24,6 +25,19 @@ export async function generateMetadata({ params }: { params: Promise<{ year: str
|
|
|
24
25
|
return {
|
|
25
26
|
title: `${flow.title} | ${resolveLocale(siteConfig.title)}`,
|
|
26
27
|
description: flow.excerpt,
|
|
28
|
+
openGraph: {
|
|
29
|
+
title: flow.title,
|
|
30
|
+
description: flow.excerpt,
|
|
31
|
+
type: 'article',
|
|
32
|
+
publishedTime: flow.date,
|
|
33
|
+
url: `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`,
|
|
34
|
+
siteName: resolveLocale(siteConfig.title),
|
|
35
|
+
},
|
|
36
|
+
twitter: {
|
|
37
|
+
card: 'summary',
|
|
38
|
+
title: flow.title,
|
|
39
|
+
description: flow.excerpt,
|
|
40
|
+
},
|
|
27
41
|
};
|
|
28
42
|
}
|
|
29
43
|
|
|
@@ -36,6 +50,7 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
|
|
|
36
50
|
const allFlows = getAllFlows();
|
|
37
51
|
const entryDates = allFlows.map(f => f.date);
|
|
38
52
|
const { prev, next } = getAdjacentFlows(flow.slug);
|
|
53
|
+
const flowUrl = `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`;
|
|
39
54
|
|
|
40
55
|
return (
|
|
41
56
|
<div className="layout-main">
|
|
@@ -71,14 +86,16 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
|
|
|
71
86
|
<MarkdownRenderer content={flow.content} />
|
|
72
87
|
</div>
|
|
73
88
|
|
|
89
|
+
<ShareBar url={flowUrl} title={flow.title} className="mt-8 mb-2" />
|
|
90
|
+
|
|
74
91
|
{/* Prev/Next navigation */}
|
|
75
|
-
<nav className="mt-
|
|
92
|
+
<nav className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
|
|
76
93
|
{prev ? (
|
|
77
94
|
<Link
|
|
78
95
|
href={`/flows/${prev.slug}`}
|
|
79
96
|
className="group text-left no-underline"
|
|
80
97
|
>
|
|
81
|
-
<span className="text-xs text-muted">
|
|
98
|
+
<span className="text-xs text-muted">{t('older')}</span>
|
|
82
99
|
<div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
|
|
83
100
|
{prev.title}
|
|
84
101
|
</div>
|
|
@@ -90,7 +107,7 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
|
|
|
90
107
|
href={`/flows/${next.slug}`}
|
|
91
108
|
className="group text-right no-underline"
|
|
92
109
|
>
|
|
93
|
-
<span className="text-xs text-muted">
|
|
110
|
+
<span className="text-xs text-muted">{t('newer')}</span>
|
|
94
111
|
<div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
|
|
95
112
|
{next.title}
|
|
96
113
|
</div>
|
package/src/app/layout.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import Analytics from "@/components/Analytics";
|
|
|
6
6
|
import { siteConfig } from "../../site.config";
|
|
7
7
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
|
8
8
|
import { LanguageProvider } from "@/components/LanguageProvider";
|
|
9
|
-
import { getAllSeries, getAllBooks } from "@/lib/markdown";
|
|
9
|
+
import { getAllSeries, getAllBooks, getSeriesData } from "@/lib/markdown";
|
|
10
10
|
import { resolveLocale } from "@/lib/i18n";
|
|
11
11
|
import "./globals.css";
|
|
12
12
|
|
|
@@ -47,6 +47,12 @@ const baskerville = localFont({
|
|
|
47
47
|
variable: "--font-baskerville",
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
const siteTwitterHandle = (() => {
|
|
51
|
+
const url = siteConfig.social?.twitter ?? '';
|
|
52
|
+
const m = url.match(/(?:twitter\.com|x\.com)\/([^/?#]+)/);
|
|
53
|
+
return m ? `@${m[1]}` : undefined;
|
|
54
|
+
})();
|
|
55
|
+
|
|
50
56
|
export const metadata: Metadata = {
|
|
51
57
|
metadataBase: new URL(siteConfig.baseUrl),
|
|
52
58
|
title: resolveLocale(siteConfig.title),
|
|
@@ -54,6 +60,17 @@ export const metadata: Metadata = {
|
|
|
54
60
|
icons: {
|
|
55
61
|
icon: "/icon.svg",
|
|
56
62
|
},
|
|
63
|
+
openGraph: {
|
|
64
|
+
siteName: resolveLocale(siteConfig.title),
|
|
65
|
+
locale: siteConfig.i18n.defaultLocale,
|
|
66
|
+
type: 'website',
|
|
67
|
+
images: [{ url: siteConfig.ogImage, width: 1200, height: 630 }],
|
|
68
|
+
},
|
|
69
|
+
twitter: {
|
|
70
|
+
card: 'summary',
|
|
71
|
+
site: siteTwitterHandle,
|
|
72
|
+
creator: siteTwitterHandle,
|
|
73
|
+
},
|
|
57
74
|
};
|
|
58
75
|
|
|
59
76
|
export default function RootLayout({
|
|
@@ -61,30 +78,39 @@ export default function RootLayout({
|
|
|
61
78
|
}: Readonly<{
|
|
62
79
|
children: React.ReactNode;
|
|
63
80
|
}>) {
|
|
64
|
-
const
|
|
65
|
-
const featuredSeries = siteConfig.series?.navbar;
|
|
66
|
-
|
|
67
|
-
const seriesKeys = Object.keys(allSeries).sort();
|
|
68
|
-
const filteredKeys = featuredSeries
|
|
69
|
-
? seriesKeys.filter(slug => featuredSeries.includes(slug))
|
|
70
|
-
: seriesKeys.slice(0, 5);
|
|
81
|
+
const features = siteConfig.features;
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
83
|
+
// Build series list for navbar (only when series feature is enabled)
|
|
84
|
+
const seriesNavItem = siteConfig.nav.find(item => item.url === '/series');
|
|
85
|
+
const featuredSeries = seriesNavItem?.dropdown;
|
|
86
|
+
let seriesList: { name: string; slug: string }[] = [];
|
|
87
|
+
if (features?.series?.enabled !== false) {
|
|
88
|
+
const allSeries = getAllSeries();
|
|
89
|
+
const seriesKeys = Object.keys(allSeries).sort();
|
|
90
|
+
const filteredKeys = featuredSeries && featuredSeries.length > 0
|
|
91
|
+
? seriesKeys.filter(slug => featuredSeries.includes(slug))
|
|
92
|
+
: seriesKeys.slice(0, 5);
|
|
93
|
+
seriesList = filteredKeys.map(slug => ({
|
|
94
|
+
name: getSeriesData(slug)?.title || allSeries[slug][0]?.series || slug,
|
|
95
|
+
slug,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
76
98
|
|
|
77
|
-
// Build books list for navbar
|
|
78
|
-
const
|
|
79
|
-
const featuredBookSlugs =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
// Build books list for navbar (only when books feature is enabled)
|
|
100
|
+
const booksNavItem = siteConfig.nav.find(item => item.url === '/books');
|
|
101
|
+
const featuredBookSlugs = booksNavItem?.dropdown;
|
|
102
|
+
let booksList: { name: string; slug: string }[] = [];
|
|
103
|
+
if (features?.books?.enabled !== false) {
|
|
104
|
+
const allBooks = getAllBooks();
|
|
105
|
+
booksList = featuredBookSlugs && featuredBookSlugs.length > 0
|
|
106
|
+
? allBooks
|
|
107
|
+
.filter(book => featuredBookSlugs.includes(book.slug))
|
|
108
|
+
.map(book => ({ name: book.title, slug: book.slug }))
|
|
109
|
+
: allBooks.slice(0, 5).map(book => ({ name: book.title, slug: book.slug }));
|
|
110
|
+
}
|
|
85
111
|
|
|
86
112
|
return (
|
|
87
|
-
<html lang=
|
|
113
|
+
<html lang={siteConfig.i18n.defaultLocale} suppressHydrationWarning>
|
|
88
114
|
<body
|
|
89
115
|
className={`${inter.variable} ${baskerville.variable} font-sans min-h-screen transition-colors duration-300`}
|
|
90
116
|
data-palette={siteConfig.themeColor}
|