@hutusi/amytis 1.7.0 → 1.9.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/.github/workflows/ci.yml +1 -1
- package/CHANGELOG.md +63 -0
- package/CLAUDE.md +9 -18
- package/GEMINI.md +6 -0
- package/README.md +44 -0
- package/TODO.md +15 -3
- package/bun.lock +5 -3
- package/content/about.mdx +64 -10
- package/content/about.zh.mdx +66 -9
- package/content/books/sample-book/index.mdx +3 -3
- package/content/flows/2026/02/05.md +0 -1
- package/content/flows/2026/02/10.mdx +2 -1
- package/content/flows/2026/02/15.md +2 -1
- package/content/flows/2026/02/18.mdx +2 -1
- package/content/flows/2026/02/20.md +0 -1
- package/content/notes/algorithms-and-data-structures.mdx +51 -0
- package/content/notes/digital-garden-philosophy.mdx +36 -0
- package/content/notes/react-server-components.mdx +49 -0
- package/content/notes/tailwind-v4.mdx +45 -0
- package/content/notes/zettelkasten-method.mdx +33 -0
- package/content/series/digital-garden/01-philosophy.mdx +25 -12
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONTRIBUTING.md +26 -0
- package/docs/DIGITAL_GARDEN.md +72 -0
- package/imports/README.md +45 -0
- package/package.json +12 -5
- package/scripts/generate-knowledge-graph.ts +162 -0
- package/scripts/import-book.ts +176 -0
- package/scripts/new-flow-from-chat.ts +238 -0
- package/scripts/new-flow.ts +0 -5
- package/scripts/new-note.ts +53 -0
- package/scripts/sync-book-chapters.ts +210 -0
- package/site.config.ts +30 -7
- package/src/app/authors/[author]/page.tsx +3 -1
- package/src/app/books/[slug]/[chapter]/page.tsx +2 -1
- package/src/app/books/[slug]/page.tsx +6 -5
- package/src/app/flows/[year]/[month]/[day]/page.tsx +35 -29
- package/src/app/flows/[year]/[month]/page.tsx +18 -13
- package/src/app/flows/[year]/page.tsx +25 -15
- package/src/app/flows/page/[page]/page.tsx +5 -9
- package/src/app/flows/page.tsx +5 -8
- package/src/app/globals.css +41 -0
- package/src/app/graph/page.tsx +21 -0
- package/src/app/layout.tsx +4 -2
- package/src/app/notes/[slug]/page.tsx +129 -0
- package/src/app/notes/page/[page]/page.tsx +60 -0
- package/src/app/notes/page.tsx +33 -0
- package/src/app/page/[page]/page.tsx +1 -0
- package/src/app/page.tsx +4 -5
- package/src/app/posts/[slug]/page.tsx +5 -2
- package/src/app/posts/page/[page]/page.tsx +4 -1
- package/src/app/search.json/route.ts +17 -3
- package/src/app/series/[slug]/page/[page]/page.tsx +1 -0
- package/src/app/series/[slug]/page.tsx +3 -3
- package/src/app/sitemap.ts +1 -1
- package/src/app/tags/[tag]/page.tsx +3 -3
- package/src/components/Backlinks.tsx +39 -0
- package/src/components/BookMobileNav.tsx +11 -11
- package/src/components/BookSidebar.tsx +17 -25
- package/src/components/BrowserDetectionBanner.tsx +96 -0
- package/src/components/FeaturedStoriesSection.tsx +1 -1
- package/src/components/FlowCalendarSidebar.tsx +4 -2
- package/src/components/FlowContent.tsx +4 -3
- package/src/components/FlowHubTabs.tsx +50 -0
- package/src/components/FlowTimelineEntry.tsx +7 -9
- package/src/components/KnowledgeGraph.tsx +324 -0
- package/src/components/LanguageProvider.tsx +14 -5
- package/src/components/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +237 -10
- package/src/components/NoteContent.tsx +123 -0
- package/src/components/NoteSidebar.tsx +132 -0
- package/src/components/RecentNotesSection.tsx +6 -11
- package/src/components/Search.tsx +7 -3
- package/src/components/TagContentTabs.tsx +0 -1
- package/src/i18n/translations.ts +43 -17
- package/src/layouts/BookLayout.tsx +3 -3
- package/src/layouts/PostLayout.tsx +8 -3
- package/src/lib/i18n.ts +83 -6
- package/src/lib/markdown.ts +306 -19
- package/src/lib/remark-wikilinks.ts +59 -0
- package/src/lib/search-utils.ts +2 -1
- package/tests/unit/static-params.test.ts +238 -0
- package/content/series/digital-garden/01-philosophy/index.mdx +0 -23
package/src/i18n/translations.ts
CHANGED
|
@@ -18,13 +18,13 @@ export const translations = {
|
|
|
18
18
|
prev_page: "Prev",
|
|
19
19
|
back_to_home: "Back to Home",
|
|
20
20
|
series: "Series",
|
|
21
|
-
related_posts: "Related
|
|
22
|
-
|
|
21
|
+
related_posts: "Related Articles",
|
|
22
|
+
featured_articles: "Featured Articles",
|
|
23
23
|
view_all: "View All",
|
|
24
24
|
parts: "Parts",
|
|
25
25
|
categories: "Categories",
|
|
26
26
|
articles: "Articles",
|
|
27
|
-
posts: "
|
|
27
|
+
posts: "Articles",
|
|
28
28
|
links: "Links",
|
|
29
29
|
explore: "Explore",
|
|
30
30
|
connect: "Connect",
|
|
@@ -33,24 +33,24 @@ export const translations = {
|
|
|
33
33
|
built_with: "Built with Amytis",
|
|
34
34
|
on_this_page: "On this page",
|
|
35
35
|
back_to_top: "Back to top",
|
|
36
|
-
archive_subtitle: "{count}
|
|
37
|
-
archive_subtitle_one: "{count}
|
|
38
|
-
posts_subtitle: "{count}
|
|
36
|
+
archive_subtitle: "{count} articles across {years} years.",
|
|
37
|
+
archive_subtitle_one: "{count} articles across 1 year.",
|
|
38
|
+
posts_subtitle: "{count} articles in total.",
|
|
39
39
|
page_of_total: "Page {page} of {total}",
|
|
40
40
|
series_subtitle: "{count} collections of curated knowledge.",
|
|
41
41
|
series_subtitle_one: "1 collection of curated knowledge.",
|
|
42
42
|
tags_subtitle: "{count} topics cultivated in this garden.",
|
|
43
43
|
tags_subtitle_one: "1 topic cultivated in this garden.",
|
|
44
|
-
tag_posts_found: "{count}
|
|
45
|
-
tag_posts_found_one: "1
|
|
44
|
+
tag_posts_found: "{count} articles found.",
|
|
45
|
+
tag_posts_found_one: "1 article found.",
|
|
46
46
|
about_title: "About Amytis",
|
|
47
47
|
about_subtitle: "Learn more about the philosophy and technology behind this digital garden.",
|
|
48
|
-
view_all_posts: "View All {count}
|
|
48
|
+
view_all_posts: "View All {count} Articles",
|
|
49
49
|
view_full_series: "View full series",
|
|
50
50
|
prev: "Prev",
|
|
51
51
|
next: "Next",
|
|
52
52
|
hide: "Hide",
|
|
53
|
-
all_posts: "All
|
|
53
|
+
all_posts: "All Articles",
|
|
54
54
|
books: "Books",
|
|
55
55
|
book: "Book",
|
|
56
56
|
chapter: "Chapter",
|
|
@@ -60,7 +60,7 @@ export const translations = {
|
|
|
60
60
|
selected_books: "Selected Books",
|
|
61
61
|
flow: "Flow",
|
|
62
62
|
recent_notes: "Recent Notes",
|
|
63
|
-
all_flows: "All
|
|
63
|
+
all_flows: "All Flows",
|
|
64
64
|
no_flows: "No notes yet.",
|
|
65
65
|
flow_subtitle: "{count} daily notes.",
|
|
66
66
|
flows_in_year: "Notes in {year}",
|
|
@@ -72,7 +72,7 @@ export const translations = {
|
|
|
72
72
|
search_showing: "Showing {shown} of {total} results",
|
|
73
73
|
search_results_found: "{total} results for \"{query}\"",
|
|
74
74
|
search_no_results_for: "No results for \"{query}\"",
|
|
75
|
-
search_type_post: "
|
|
75
|
+
search_type_post: "Article",
|
|
76
76
|
search_type_flow: "Flow",
|
|
77
77
|
search_type_book: "Book",
|
|
78
78
|
search_tips: "Tips",
|
|
@@ -84,16 +84,16 @@ export const translations = {
|
|
|
84
84
|
copy_link: "Copy link",
|
|
85
85
|
link_copied: "Copied!",
|
|
86
86
|
flow_notes: "Flow Notes",
|
|
87
|
-
tag_post_count: "{count}
|
|
88
|
-
tag_post_count_one: "1
|
|
87
|
+
tag_post_count: "{count} articles",
|
|
88
|
+
tag_post_count_one: "1 article",
|
|
89
89
|
tag_flow_count: "{count} flow notes",
|
|
90
90
|
tag_flow_count_one: "1 flow note",
|
|
91
91
|
subscribe: "Subscribe",
|
|
92
|
-
subscribe_subtitle: "Stay updated with new
|
|
92
|
+
subscribe_subtitle: "Stay updated with new articles and notes via your preferred channel.",
|
|
93
93
|
rss_readers: "RSS Readers",
|
|
94
94
|
rss_description: "Subscribe with any RSS reader for automatic updates when new content is published.",
|
|
95
95
|
email_newsletter: "Email Newsletter",
|
|
96
|
-
email_newsletter_description: "Get new
|
|
96
|
+
email_newsletter_description: "Get new articles delivered directly to your inbox.",
|
|
97
97
|
telegram_channel: "Telegram",
|
|
98
98
|
telegram_channel_description: "Instant updates via Telegram channel.",
|
|
99
99
|
wechat_official: "WeChat Official Account",
|
|
@@ -117,6 +117,19 @@ export const translations = {
|
|
|
117
117
|
sort_az: "A–Z",
|
|
118
118
|
tags_count: "{shown} / {total} tags",
|
|
119
119
|
tags_no_match: "No tags match \"{filter}\"",
|
|
120
|
+
notes: "Notes",
|
|
121
|
+
notes_subtitle: "{count} knowledge base notes.",
|
|
122
|
+
tab_daily_flow: "Daily",
|
|
123
|
+
tab_graph: "Graph",
|
|
124
|
+
backlinks: "Backlinks",
|
|
125
|
+
graph_subtitle: "A visual map of connected knowledge.",
|
|
126
|
+
search_type_note: "Note",
|
|
127
|
+
all_notes: "All Notes",
|
|
128
|
+
no_notes: "No notes yet.",
|
|
129
|
+
more: "More",
|
|
130
|
+
browser_outdated: "Your browser is outdated and may not display this site correctly.",
|
|
131
|
+
browser_update: "Update your browser",
|
|
132
|
+
browser_dismiss: "Dismiss",
|
|
120
133
|
},
|
|
121
134
|
zh: {
|
|
122
135
|
home: "首页",
|
|
@@ -138,7 +151,7 @@ export const translations = {
|
|
|
138
151
|
back_to_home: "返回首页",
|
|
139
152
|
series: "系列",
|
|
140
153
|
related_posts: "相关文章",
|
|
141
|
-
|
|
154
|
+
featured_articles: "精选文章",
|
|
142
155
|
view_all: "查看全部",
|
|
143
156
|
parts: "篇",
|
|
144
157
|
categories: "分类",
|
|
@@ -236,6 +249,19 @@ export const translations = {
|
|
|
236
249
|
sort_az: "A–Z",
|
|
237
250
|
tags_count: "{shown} / {total} 个标签",
|
|
238
251
|
tags_no_match: "未找到匹配\"{filter}\"的标签",
|
|
252
|
+
notes: "笔记",
|
|
253
|
+
notes_subtitle: "共 {count} 条知识库笔记。",
|
|
254
|
+
tab_daily_flow: "随笔",
|
|
255
|
+
tab_graph: "图谱",
|
|
256
|
+
backlinks: "反向链接",
|
|
257
|
+
graph_subtitle: "知识连接的可视化地图。",
|
|
258
|
+
search_type_note: "笔记",
|
|
259
|
+
all_notes: "全部笔记",
|
|
260
|
+
no_notes: "暂无笔记。",
|
|
261
|
+
more: "更多",
|
|
262
|
+
browser_outdated: "您的浏览器版本过旧,可能无法正常显示本站内容。",
|
|
263
|
+
browser_update: "更新浏览器",
|
|
264
|
+
browser_dismiss: "关闭",
|
|
239
265
|
},
|
|
240
266
|
};
|
|
241
267
|
|
|
@@ -61,13 +61,13 @@ export default function BookLayout({ book, chapter }: BookLayoutProps) {
|
|
|
61
61
|
</header>
|
|
62
62
|
|
|
63
63
|
{/* Content */}
|
|
64
|
-
<MarkdownRenderer content={chapter.content} latex={chapter.latex} slug={`books/${book.slug}`} />
|
|
64
|
+
<MarkdownRenderer content={chapter.content} latex={chapter.latex} slug={chapter.isFolder ? `books/${book.slug}/${chapter.slug}` : `books/${book.slug}`} />
|
|
65
65
|
|
|
66
66
|
{/* Prev/Next navigation */}
|
|
67
67
|
<nav className="mt-16 pt-8 border-t border-muted/10 flex gap-4">
|
|
68
68
|
{chapter.prevChapter ? (
|
|
69
69
|
<Link
|
|
70
|
-
href={`/books/${book.slug}/${chapter.prevChapter.
|
|
70
|
+
href={`/books/${book.slug}/${chapter.prevChapter.id}`}
|
|
71
71
|
className="flex-1 group flex items-center gap-3 py-4 px-5 rounded-xl bg-muted/5 hover:bg-muted/10 no-underline transition-colors"
|
|
72
72
|
>
|
|
73
73
|
<svg className="w-5 h-5 flex-shrink-0 text-muted group-hover:text-accent transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -85,7 +85,7 @@ export default function BookLayout({ book, chapter }: BookLayoutProps) {
|
|
|
85
85
|
)}
|
|
86
86
|
{chapter.nextChapter ? (
|
|
87
87
|
<Link
|
|
88
|
-
href={`/books/${book.slug}/${chapter.nextChapter.
|
|
88
|
+
href={`/books/${book.slug}/${chapter.nextChapter.id}`}
|
|
89
89
|
className="flex-1 group flex items-center justify-end gap-3 py-4 px-5 rounded-xl bg-muted/5 hover:bg-muted/10 no-underline transition-colors text-right"
|
|
90
90
|
>
|
|
91
91
|
<div className="min-w-0">
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import Link from 'next/link';
|
|
2
|
-
import { getAuthorSlug, PostData } from '@/lib/markdown';
|
|
2
|
+
import { getAuthorSlug, PostData, BacklinkSource, SlugRegistryEntry } from '@/lib/markdown';
|
|
3
3
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
4
4
|
import RelatedPosts from '@/components/RelatedPosts';
|
|
5
5
|
import SeriesList from '@/components/SeriesList';
|
|
6
6
|
import PostSidebar from '@/components/PostSidebar';
|
|
7
7
|
import Comments from '@/components/Comments';
|
|
8
8
|
import ExternalLinks from '@/components/ExternalLinks';
|
|
9
|
+
import Backlinks from '@/components/Backlinks';
|
|
9
10
|
import Tag from '@/components/Tag';
|
|
10
11
|
import ReadingProgressBar from '@/components/ReadingProgressBar';
|
|
11
12
|
import PostNavigation from '@/components/PostNavigation';
|
|
@@ -21,9 +22,11 @@ interface PostLayoutProps {
|
|
|
21
22
|
seriesTitle?: string;
|
|
22
23
|
prevPost?: PostData | null;
|
|
23
24
|
nextPost?: PostData | null;
|
|
25
|
+
backlinks?: BacklinkSource[];
|
|
26
|
+
slugRegistry?: Map<string, SlugRegistryEntry>;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitle, prevPost, nextPost }: PostLayoutProps) {
|
|
29
|
+
export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitle, prevPost, nextPost, backlinks, slugRegistry }: PostLayoutProps) {
|
|
27
30
|
const showToc = siteConfig.posts?.toc !== false && post.toc !== false && post.headings && post.headings.length > 0;
|
|
28
31
|
const hasSeries = !!(post.series && seriesPosts && seriesPosts.length > 0);
|
|
29
32
|
const showSidebar = showToc || hasSeries;
|
|
@@ -110,7 +113,7 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
|
|
|
110
113
|
</div>
|
|
111
114
|
)}
|
|
112
115
|
|
|
113
|
-
<MarkdownRenderer content={post.content} latex={post.latex} slug={post.slug} />
|
|
116
|
+
<MarkdownRenderer content={post.content} latex={post.latex} slug={post.slug} slugRegistry={slugRegistry} />
|
|
114
117
|
|
|
115
118
|
{post.tags && post.tags.length > 0 && (
|
|
116
119
|
<div className="mt-12 pt-12 border-t border-muted/20 flex flex-wrap items-center gap-2">
|
|
@@ -125,6 +128,8 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
|
|
|
125
128
|
<ExternalLinks links={post.externalLinks} />
|
|
126
129
|
)}
|
|
127
130
|
|
|
131
|
+
<Backlinks backlinks={backlinks ?? []} />
|
|
132
|
+
|
|
128
133
|
<ShareBar
|
|
129
134
|
url={postUrl}
|
|
130
135
|
title={post.title}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -1,18 +1,95 @@
|
|
|
1
|
-
import { translations, Language } from '@/i18n/translations';
|
|
1
|
+
import { translations, Language, TranslationKey } from '@/i18n/translations';
|
|
2
2
|
import { siteConfig } from '../../site.config';
|
|
3
3
|
|
|
4
|
+
// ── Feature name overrides ────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// When a user configures e.g. features.series.name.zh = "专栏", these maps
|
|
7
|
+
// tell us which translation keys should reflect that name.
|
|
8
|
+
|
|
9
|
+
/** Translation keys whose value IS the feature name (simple substitution). */
|
|
10
|
+
const FEATURE_SIMPLE_KEYS: Record<string, TranslationKey[]> = {
|
|
11
|
+
series: ['series'],
|
|
12
|
+
books: ['books', 'book'],
|
|
13
|
+
flow: ['flow'],
|
|
14
|
+
posts: ['posts'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Translation keys that CONTAIN the feature name as a substring (compound substitution). */
|
|
18
|
+
const FEATURE_COMPOUND_KEYS: Record<string, TranslationKey[]> = {
|
|
19
|
+
series: ['curated_series', 'all_series', 'view_full_series'],
|
|
20
|
+
books: ['all_books', 'selected_books'],
|
|
21
|
+
flow: ['all_flows', 'recent_notes'],
|
|
22
|
+
posts: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function substituteInTranslation(original: string, from: string, to: string): string | null {
|
|
26
|
+
if (original.includes(from)) return original.replaceAll(from, to);
|
|
27
|
+
// Case-insensitive fallback for languages like English
|
|
28
|
+
if (original.toLowerCase().includes(from.toLowerCase())) {
|
|
29
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
return original.replace(new RegExp(escaped, 'gi'), to);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a map of translation overrides derived from siteConfig.features.*.name.
|
|
37
|
+
* Called once per language; results are cached below.
|
|
38
|
+
*/
|
|
39
|
+
export function buildFeatureOverrides(lang: string): Partial<Record<TranslationKey, string>> {
|
|
40
|
+
const overrides: Partial<Record<TranslationKey, string>> = {};
|
|
41
|
+
const features = siteConfig.features as Record<string, { name?: Record<string, string> } | undefined>;
|
|
42
|
+
const langT = translations[lang as Language] ?? translations.en;
|
|
43
|
+
|
|
44
|
+
for (const [featureKey, featureConfig] of Object.entries(features)) {
|
|
45
|
+
if (!featureConfig?.name) continue;
|
|
46
|
+
const configuredName = featureConfig.name[lang] ?? featureConfig.name['en'];
|
|
47
|
+
if (!configuredName) continue;
|
|
48
|
+
|
|
49
|
+
const simpleKeys = FEATURE_SIMPLE_KEYS[featureKey] ?? [];
|
|
50
|
+
const defaultName = langT[simpleKeys[0]];
|
|
51
|
+
// Skip if no default mapping or name hasn't changed
|
|
52
|
+
if (!defaultName || configuredName === defaultName) continue;
|
|
53
|
+
|
|
54
|
+
for (const key of simpleKeys) {
|
|
55
|
+
overrides[key] = configuredName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const key of (FEATURE_COMPOUND_KEYS[featureKey] ?? [])) {
|
|
59
|
+
const original = langT[key];
|
|
60
|
+
if (!original) continue;
|
|
61
|
+
const substituted = substituteInTranslation(original, defaultName, configuredName);
|
|
62
|
+
if (substituted) overrides[key] = substituted;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return overrides;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Module-level cache — siteConfig is static so overrides never change
|
|
70
|
+
const _overridesCache: Record<string, Partial<Record<TranslationKey, string>>> = {};
|
|
71
|
+
|
|
72
|
+
function getOverrides(lang: string): Partial<Record<TranslationKey, string>> {
|
|
73
|
+
if (!_overridesCache[lang]) _overridesCache[lang] = buildFeatureOverrides(lang);
|
|
74
|
+
return _overridesCache[lang];
|
|
75
|
+
}
|
|
76
|
+
|
|
4
77
|
/**
|
|
5
78
|
* Server-side translation helper.
|
|
6
79
|
* For client components, use the `useLanguage()` hook instead.
|
|
7
80
|
*/
|
|
8
|
-
export const t = (key:
|
|
9
|
-
|
|
81
|
+
export const t = (key: TranslationKey): string => {
|
|
82
|
+
const lang = siteConfig.i18n.defaultLocale;
|
|
83
|
+
const overrides = getOverrides(lang);
|
|
84
|
+
if (key in overrides) return overrides[key]!;
|
|
85
|
+
return translations[lang as Language]?.[key] || translations.en[key];
|
|
86
|
+
};
|
|
10
87
|
|
|
11
|
-
export const tWith = (key:
|
|
88
|
+
export const tWith = (key: TranslationKey, params: Record<string, string | number>): string => {
|
|
12
89
|
let result = t(key);
|
|
13
|
-
|
|
90
|
+
for (const [k, v] of Object.entries(params)) {
|
|
14
91
|
result = result.split(`{${k}}`).join(String(v));
|
|
15
|
-
}
|
|
92
|
+
}
|
|
16
93
|
return result;
|
|
17
94
|
};
|
|
18
95
|
|