@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.
Files changed (83) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/CHANGELOG.md +63 -0
  3. package/CLAUDE.md +9 -18
  4. package/GEMINI.md +6 -0
  5. package/README.md +44 -0
  6. package/TODO.md +15 -3
  7. package/bun.lock +5 -3
  8. package/content/about.mdx +64 -10
  9. package/content/about.zh.mdx +66 -9
  10. package/content/books/sample-book/index.mdx +3 -3
  11. package/content/flows/2026/02/05.md +0 -1
  12. package/content/flows/2026/02/10.mdx +2 -1
  13. package/content/flows/2026/02/15.md +2 -1
  14. package/content/flows/2026/02/18.mdx +2 -1
  15. package/content/flows/2026/02/20.md +0 -1
  16. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  17. package/content/notes/digital-garden-philosophy.mdx +36 -0
  18. package/content/notes/react-server-components.mdx +49 -0
  19. package/content/notes/tailwind-v4.mdx +45 -0
  20. package/content/notes/zettelkasten-method.mdx +33 -0
  21. package/content/series/digital-garden/01-philosophy.mdx +25 -12
  22. package/docs/ARCHITECTURE.md +9 -1
  23. package/docs/CONTRIBUTING.md +26 -0
  24. package/docs/DIGITAL_GARDEN.md +72 -0
  25. package/imports/README.md +45 -0
  26. package/package.json +12 -5
  27. package/scripts/generate-knowledge-graph.ts +162 -0
  28. package/scripts/import-book.ts +176 -0
  29. package/scripts/new-flow-from-chat.ts +238 -0
  30. package/scripts/new-flow.ts +0 -5
  31. package/scripts/new-note.ts +53 -0
  32. package/scripts/sync-book-chapters.ts +210 -0
  33. package/site.config.ts +30 -7
  34. package/src/app/authors/[author]/page.tsx +3 -1
  35. package/src/app/books/[slug]/[chapter]/page.tsx +2 -1
  36. package/src/app/books/[slug]/page.tsx +6 -5
  37. package/src/app/flows/[year]/[month]/[day]/page.tsx +35 -29
  38. package/src/app/flows/[year]/[month]/page.tsx +18 -13
  39. package/src/app/flows/[year]/page.tsx +25 -15
  40. package/src/app/flows/page/[page]/page.tsx +5 -9
  41. package/src/app/flows/page.tsx +5 -8
  42. package/src/app/globals.css +41 -0
  43. package/src/app/graph/page.tsx +21 -0
  44. package/src/app/layout.tsx +4 -2
  45. package/src/app/notes/[slug]/page.tsx +129 -0
  46. package/src/app/notes/page/[page]/page.tsx +60 -0
  47. package/src/app/notes/page.tsx +33 -0
  48. package/src/app/page/[page]/page.tsx +1 -0
  49. package/src/app/page.tsx +4 -5
  50. package/src/app/posts/[slug]/page.tsx +5 -2
  51. package/src/app/posts/page/[page]/page.tsx +4 -1
  52. package/src/app/search.json/route.ts +17 -3
  53. package/src/app/series/[slug]/page/[page]/page.tsx +1 -0
  54. package/src/app/series/[slug]/page.tsx +3 -3
  55. package/src/app/sitemap.ts +1 -1
  56. package/src/app/tags/[tag]/page.tsx +3 -3
  57. package/src/components/Backlinks.tsx +39 -0
  58. package/src/components/BookMobileNav.tsx +11 -11
  59. package/src/components/BookSidebar.tsx +17 -25
  60. package/src/components/BrowserDetectionBanner.tsx +96 -0
  61. package/src/components/FeaturedStoriesSection.tsx +1 -1
  62. package/src/components/FlowCalendarSidebar.tsx +4 -2
  63. package/src/components/FlowContent.tsx +4 -3
  64. package/src/components/FlowHubTabs.tsx +50 -0
  65. package/src/components/FlowTimelineEntry.tsx +7 -9
  66. package/src/components/KnowledgeGraph.tsx +324 -0
  67. package/src/components/LanguageProvider.tsx +14 -5
  68. package/src/components/MarkdownRenderer.tsx +13 -2
  69. package/src/components/Navbar.tsx +237 -10
  70. package/src/components/NoteContent.tsx +123 -0
  71. package/src/components/NoteSidebar.tsx +132 -0
  72. package/src/components/RecentNotesSection.tsx +6 -11
  73. package/src/components/Search.tsx +7 -3
  74. package/src/components/TagContentTabs.tsx +0 -1
  75. package/src/i18n/translations.ts +43 -17
  76. package/src/layouts/BookLayout.tsx +3 -3
  77. package/src/layouts/PostLayout.tsx +8 -3
  78. package/src/lib/i18n.ts +83 -6
  79. package/src/lib/markdown.ts +306 -19
  80. package/src/lib/remark-wikilinks.ts +59 -0
  81. package/src/lib/search-utils.ts +2 -1
  82. package/tests/unit/static-params.test.ts +238 -0
  83. package/content/series/digital-garden/01-philosophy/index.mdx +0 -23
@@ -89,7 +89,6 @@ export default function TagContentTabs({ posts, flows }: TagContentTabsProps) {
89
89
  <FlowTimelineEntry
90
90
  key={flow.slug}
91
91
  date={flow.date}
92
- title={flow.title}
93
92
  excerpt={flow.excerpt}
94
93
  tags={flow.tags}
95
94
  slug={flow.slug}
@@ -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 Posts",
22
- featured_stories: "Featured Stories",
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: "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} posts across {years} years.",
37
- archive_subtitle_one: "{count} posts across 1 year.",
38
- posts_subtitle: "{count} posts in total.",
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} posts found.",
45
- tag_posts_found_one: "1 post found.",
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} Posts",
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 posts",
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 Notes",
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: "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} posts",
88
- tag_post_count_one: "1 post",
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 posts and notes via your preferred channel.",
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 posts delivered directly to your inbox.",
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
- featured_stories: "精选文章",
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.file}`}
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.file}`}
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: keyof typeof translations.en) =>
9
- translations[siteConfig.i18n.defaultLocale as Language]?.[key] || translations.en[key];
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: keyof typeof translations.en, params: Record<string, string | number>) => {
88
+ export const tWith = (key: TranslationKey, params: Record<string, string | number>): string => {
12
89
  let result = t(key);
13
- Object.entries(params).forEach(([k, v]) => {
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