@hutusi/amytis 1.5.5

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 (179) hide show
  1. package/.github/workflows/ci.yml +33 -0
  2. package/.github/workflows/publish.yml +53 -0
  3. package/AGENTS.md +41 -0
  4. package/CLAUDE.md +200 -0
  5. package/GEMINI.md +84 -0
  6. package/README.md +172 -0
  7. package/TODO.md +76 -0
  8. package/bun.lock +1530 -0
  9. package/content/about.mdx +23 -0
  10. package/content/books/sample-book/index.mdx +24 -0
  11. package/content/books/sample-book/introduction.mdx +34 -0
  12. package/content/books/sample-book/setup.mdx +48 -0
  13. package/content/books/sample-book/writing-content.mdx +49 -0
  14. package/content/flows/2026/02/05.md +8 -0
  15. package/content/flows/2026/02/10.mdx +8 -0
  16. package/content/flows/2026/02/15.md +8 -0
  17. package/content/flows/2026/02/18.mdx +14 -0
  18. package/content/posts/2026-01-12-the-art-of-algorithms.mdx +49 -0
  19. package/content/posts/2026-01-15-nested-image-test/images/test.svg +5 -0
  20. package/content/posts/2026-01-15-nested-image-test/index.mdx +27 -0
  21. package/content/posts/2026-01-21-kitchen-sink/assets/test.svg +5 -0
  22. package/content/posts/2026-01-21-kitchen-sink/index.mdx +169 -0
  23. package/content/posts/asynchronous-javascript.mdx +49 -0
  24. package/content/posts/draft-post.mdx +13 -0
  25. package/content/posts/future-post.mdx +12 -0
  26. package/content/posts/legacy-markdown.md +60 -0
  27. package/content/posts/markdown-features.mdx +78 -0
  28. package/content/posts/modern-css-layouts.mdx +45 -0
  29. package/content/posts/multilingual-test.mdx +124 -0
  30. package/content/posts/syntax-highlighting-showcase.mdx +528 -0
  31. package/content/posts/understanding-react-hooks.mdx +48 -0
  32. package/content/posts/welcome-to-amytis.mdx +21 -0
  33. package/content/posts//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +54 -0
  34. package/content/series/ai-nexus-weekly/index.mdx +10 -0
  35. package/content/series/ai-nexus-weekly/week-1.mdx +20 -0
  36. package/content/series/ai-nexus-weekly/week-10.mdx +20 -0
  37. package/content/series/ai-nexus-weekly/week-11.mdx +20 -0
  38. package/content/series/ai-nexus-weekly/week-12.mdx +20 -0
  39. package/content/series/ai-nexus-weekly/week-2.mdx +20 -0
  40. package/content/series/ai-nexus-weekly/week-3.mdx +20 -0
  41. package/content/series/ai-nexus-weekly/week-4.mdx +20 -0
  42. package/content/series/ai-nexus-weekly/week-5.mdx +20 -0
  43. package/content/series/ai-nexus-weekly/week-6.mdx +20 -0
  44. package/content/series/ai-nexus-weekly/week-7.mdx +20 -0
  45. package/content/series/ai-nexus-weekly/week-8.mdx +20 -0
  46. package/content/series/ai-nexus-weekly/week-9.mdx +20 -0
  47. package/content/series/digital-garden/01-philosophy/index.mdx +23 -0
  48. package/content/series/digital-garden/01-philosophy.mdx +30 -0
  49. package/content/series/digital-garden/02-architecture.mdx +19 -0
  50. package/content/series/digital-garden/index.mdx +11 -0
  51. package/content/series/markdown-showcase/index.mdx +11 -0
  52. package/content/series/markdown-showcase/mathematical-notation.mdx +32 -0
  53. package/content/series/markdown-showcase/syntax-highlighting.mdx +119 -0
  54. package/content/series/markdown-showcase/visuals-and-diagrams.mdx +27 -0
  55. package/content/series/nextjs-deep-dive/01-getting-started.mdx +66 -0
  56. package/content/series/nextjs-deep-dive/02-routing-mastery/assets/diagram.svg +8 -0
  57. package/content/series/nextjs-deep-dive/02-routing-mastery/assets/m-p-model.png +0 -0
  58. package/content/series/nextjs-deep-dive/02-routing-mastery/index.mdx +138 -0
  59. package/content/series/nextjs-deep-dive/index.mdx +12 -0
  60. package/docs/ARCHITECTURE.md +103 -0
  61. package/docs/CONTRIBUTING.md +86 -0
  62. package/docs/deployment.md +319 -0
  63. package/eslint.config.mjs +18 -0
  64. package/next.config.ts +25 -0
  65. package/package.json +81 -0
  66. package/postcss.config.mjs +7 -0
  67. package/public/file.svg +1 -0
  68. package/public/globe.svg +1 -0
  69. package/public/icon.svg +9 -0
  70. package/public/logo.svg +11 -0
  71. package/public/next-image-export-optimizer-hashes.json +7 -0
  72. package/public/next.svg +1 -0
  73. package/public/screenshot.png +0 -0
  74. package/public/vercel.svg +1 -0
  75. package/public/window.svg +1 -0
  76. package/scripts/copy-assets.ts +211 -0
  77. package/scripts/new-flow.ts +47 -0
  78. package/scripts/new-from-images.ts +141 -0
  79. package/scripts/new-from-pdf.ts +105 -0
  80. package/scripts/new-post.ts +98 -0
  81. package/scripts/new-series.ts +40 -0
  82. package/scripts/series-draft.ts +136 -0
  83. package/site.config.ts +91 -0
  84. package/src/app/[slug]/page.tsx +67 -0
  85. package/src/app/archive/page.tsx +147 -0
  86. package/src/app/authors/[author]/page.tsx +210 -0
  87. package/src/app/books/[slug]/[chapter]/page.tsx +54 -0
  88. package/src/app/books/[slug]/page.tsx +156 -0
  89. package/src/app/books/page.tsx +63 -0
  90. package/src/app/favicon.ico +0 -0
  91. package/src/app/feed.xml/route.ts +44 -0
  92. package/src/app/flows/[year]/[month]/[day]/page.tsx +105 -0
  93. package/src/app/flows/[year]/[month]/page.tsx +72 -0
  94. package/src/app/flows/[year]/page.tsx +82 -0
  95. package/src/app/flows/page/[page]/page.tsx +63 -0
  96. package/src/app/flows/page.tsx +38 -0
  97. package/src/app/globals.css +406 -0
  98. package/src/app/layout.tsx +114 -0
  99. package/src/app/page/[page]/page.tsx +60 -0
  100. package/src/app/page.tsx +110 -0
  101. package/src/app/posts/[slug]/page.tsx +119 -0
  102. package/src/app/posts/page/[page]/page.tsx +58 -0
  103. package/src/app/posts/page.tsx +40 -0
  104. package/src/app/search.json/route.ts +49 -0
  105. package/src/app/series/[slug]/page/[page]/page.tsx +141 -0
  106. package/src/app/series/[slug]/page.tsx +139 -0
  107. package/src/app/series/page.tsx +96 -0
  108. package/src/app/sitemap.ts +112 -0
  109. package/src/app/tags/[tag]/page.tsx +76 -0
  110. package/src/app/tags/page.tsx +37 -0
  111. package/src/components/Analytics.tsx +49 -0
  112. package/src/components/AuthorStats.tsx +34 -0
  113. package/src/components/BookMobileNav.tsx +171 -0
  114. package/src/components/BookSidebar.tsx +275 -0
  115. package/src/components/CodeBlock.tsx +110 -0
  116. package/src/components/Comments.tsx +63 -0
  117. package/src/components/CoverImage.tsx +93 -0
  118. package/src/components/CuratedSeriesSection.tsx +124 -0
  119. package/src/components/ExternalLinks.tsx +45 -0
  120. package/src/components/FeaturedStoriesSection.tsx +106 -0
  121. package/src/components/FlowCalendarSidebar.tsx +249 -0
  122. package/src/components/FlowContent.tsx +96 -0
  123. package/src/components/FlowTimelineEntry.tsx +34 -0
  124. package/src/components/Footer.tsx +104 -0
  125. package/src/components/Hero.tsx +126 -0
  126. package/src/components/HorizontalScroll.tsx +128 -0
  127. package/src/components/LanguageProvider.tsx +80 -0
  128. package/src/components/LanguageSwitch.tsx +17 -0
  129. package/src/components/LatestWritingSection.tsx +45 -0
  130. package/src/components/MarkdownRenderer.tsx +135 -0
  131. package/src/components/Mermaid.tsx +89 -0
  132. package/src/components/Navbar.tsx +243 -0
  133. package/src/components/PageHeader.tsx +39 -0
  134. package/src/components/Pagination.tsx +120 -0
  135. package/src/components/PostCard.tsx +30 -0
  136. package/src/components/PostList.tsx +104 -0
  137. package/src/components/PostSidebar.tsx +225 -0
  138. package/src/components/ReadingProgressBar.tsx +37 -0
  139. package/src/components/RecentNotesSection.tsx +56 -0
  140. package/src/components/RelatedPosts.tsx +34 -0
  141. package/src/components/Search.tsx +151 -0
  142. package/src/components/SelectedBooksSection.tsx +80 -0
  143. package/src/components/SeriesCatalog.tsx +112 -0
  144. package/src/components/SeriesList.tsx +167 -0
  145. package/src/components/SeriesSidebar.tsx +132 -0
  146. package/src/components/SimpleLayoutHeader.tsx +38 -0
  147. package/src/components/Skeleton.tsx +131 -0
  148. package/src/components/TableOfContents.tsx +158 -0
  149. package/src/components/Tag.tsx +47 -0
  150. package/src/components/TagPageHeader.tsx +38 -0
  151. package/src/components/ThemeProvider.tsx +12 -0
  152. package/src/components/ThemeToggle.tsx +68 -0
  153. package/src/components/TranslatedText.tsx +13 -0
  154. package/src/fonts/Inter-Bold.woff2 +0 -0
  155. package/src/fonts/Inter-Regular.woff2 +0 -0
  156. package/src/fonts/LibreBaskerville-Bold.ttf +0 -0
  157. package/src/fonts/LibreBaskerville-Italic.ttf +0 -0
  158. package/src/fonts/LibreBaskerville-Regular.ttf +0 -0
  159. package/src/i18n/translations.ts +135 -0
  160. package/src/layouts/BookLayout.tsx +109 -0
  161. package/src/layouts/PostLayout.tsx +118 -0
  162. package/src/layouts/SimpleLayout.tsx +31 -0
  163. package/src/lib/i18n.ts +35 -0
  164. package/src/lib/markdown.test.ts +127 -0
  165. package/src/lib/markdown.ts +1067 -0
  166. package/src/lib/rehype-image-metadata.ts +54 -0
  167. package/src/lib/shuffle.ts +11 -0
  168. package/templates/default.mdx +13 -0
  169. package/tests/e2e/navigation.test.ts +51 -0
  170. package/tests/e2e/series-routes.test.ts +63 -0
  171. package/tests/e2e/smoke.test.ts +19 -0
  172. package/tests/integration/markdown-features.test.ts +54 -0
  173. package/tests/integration/posts.test.ts +57 -0
  174. package/tests/integration/reading-time-headings.test.ts +79 -0
  175. package/tests/integration/series-draft.test.ts +46 -0
  176. package/tests/integration/series.test.ts +79 -0
  177. package/tests/tooling/new-from-images.test.ts +173 -0
  178. package/tests/tooling/new-post.test.ts +72 -0
  179. package/tsconfig.json +34 -0
@@ -0,0 +1,98 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+ const valuedFlags = ['--template', '--prefix', '--series'];
6
+ const title = args.filter(arg => !arg.startsWith('--') && !valuedFlags.includes(args[args.indexOf(arg) - 1]))[0];
7
+ const templateArgIndex = args.indexOf('--template');
8
+ const templateName = templateArgIndex > -1 ? args[templateArgIndex + 1] : 'default';
9
+ const prefixArgIndex = args.indexOf('--prefix');
10
+ const prefix = prefixArgIndex > -1 ? args[prefixArgIndex + 1] : '';
11
+ const seriesArgIndex = args.indexOf('--series');
12
+ const series = seriesArgIndex > -1 ? args[seriesArgIndex + 1] : '';
13
+ const useFolder = args.includes('--folder');
14
+ const useMd = args.includes('--md');
15
+
16
+ if (!title) {
17
+ console.error('Please provide a post title.');
18
+ console.error('Usage: bun new <title> [--template <name>] [--prefix <name>] [--series <slug>] [--folder] [--md]');
19
+ process.exit(1);
20
+ }
21
+
22
+ const slug = title
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/(^-|-$)+/g, '');
26
+
27
+ const date = new Date().toISOString().split('T')[0];
28
+ const ext = useMd ? '.md' : '.mdx';
29
+ const prefixedSlug = prefix ? `${prefix}-${slug}` : slug;
30
+ let targetPath = '';
31
+
32
+ if (series) {
33
+ // Series posts go into content/series/<slug>/ without date prefix
34
+ const seriesDir = path.join(process.cwd(), 'content', 'series', series);
35
+ if (!fs.existsSync(seriesDir)) {
36
+ console.error(`Error: Series directory "${series}" does not exist at ${seriesDir}`);
37
+ process.exit(1);
38
+ }
39
+ if (useFolder) {
40
+ const dirPath = path.join(seriesDir, prefixedSlug);
41
+ if (!fs.existsSync(dirPath)) {
42
+ fs.mkdirSync(dirPath, { recursive: true });
43
+ }
44
+ fs.mkdirSync(path.join(dirPath, 'images'), { recursive: true });
45
+ targetPath = path.join(dirPath, `index${ext}`);
46
+ } else {
47
+ targetPath = path.join(seriesDir, `${prefixedSlug}${ext}`);
48
+ }
49
+ } else if (useFolder) {
50
+ const dirName = `${date}-${prefixedSlug}`;
51
+ const dirPath = path.join(process.cwd(), 'content', 'posts', dirName);
52
+ if (!fs.existsSync(dirPath)) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ fs.mkdirSync(path.join(dirPath, 'images'), { recursive: true });
56
+ targetPath = path.join(dirPath, `index${ext}`);
57
+ } else {
58
+ const filename = `${date}-${prefixedSlug}${ext}`;
59
+ targetPath = path.join(process.cwd(), 'content', 'posts', filename);
60
+ }
61
+
62
+ const templatePath = path.join(process.cwd(), 'templates', `${templateName}${ext}`);
63
+ // Fallback to .mdx template if .md specific template doesn't exist
64
+ const fallbackTemplatePath = path.join(process.cwd(), 'templates', `${templateName}.mdx`);
65
+
66
+ let content = '';
67
+
68
+ if (fs.existsSync(templatePath)) {
69
+ content = fs.readFileSync(templatePath, 'utf8');
70
+ } else if (fs.existsSync(fallbackTemplatePath)) {
71
+ content = fs.readFileSync(fallbackTemplatePath, 'utf8');
72
+ } else {
73
+ // Fallback default template if file not found
74
+ content = `---
75
+ title: "{{title}}"
76
+ date: "{{date}}"
77
+ excerpt: ""
78
+ category: "Uncategorized"
79
+ tags: []
80
+ authors: ["Amytis"]
81
+ layout: "post"
82
+ draft: false
83
+ latex: false
84
+ ---
85
+
86
+ Write your content here...
87
+ `;
88
+ }
89
+
90
+ content = content.replace(/{{title}}/g, title).replace(/{{date}}/g, date);
91
+
92
+ if (fs.existsSync(targetPath)) {
93
+ console.error(`Error: Post already exists at ${targetPath}`);
94
+ process.exit(1);
95
+ }
96
+
97
+ fs.writeFileSync(targetPath, content);
98
+ console.log(`Created new post: ${targetPath}`);
@@ -0,0 +1,40 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import GithubSlugger from 'github-slugger';
4
+
5
+ const args = process.argv.slice(2);
6
+ const title = args[0];
7
+
8
+ if (!title) {
9
+ console.error('Please provide a series title.');
10
+ console.error('Usage: bun run new-series "My Series Name"');
11
+ process.exit(1);
12
+ }
13
+
14
+ const slugger = new GithubSlugger();
15
+ const slug = slugger.slug(title);
16
+ const seriesDir = path.join(process.cwd(), 'content', 'series', slug);
17
+
18
+ if (fs.existsSync(seriesDir)) {
19
+ console.error(`Series "${slug}" already exists.`);
20
+ process.exit(1);
21
+ }
22
+
23
+ fs.mkdirSync(seriesDir, { recursive: true });
24
+ fs.mkdirSync(path.join(seriesDir, 'images'));
25
+
26
+ const date = new Date().toISOString().split('T')[0];
27
+
28
+ const content = `---
29
+ title: "${title}"
30
+ excerpt: "A description for ${title}."
31
+ date: "${date}"
32
+ coverImage: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=800&q=80"
33
+ ---
34
+
35
+ Welcome to the ${title} series.
36
+ `;
37
+
38
+ fs.writeFileSync(path.join(seriesDir, 'index.mdx'), content);
39
+
40
+ console.log(`Created new series at content/series/${slug}/index.mdx`);
@@ -0,0 +1,136 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+
5
+ const args = process.argv.slice(2);
6
+ const seriesSlug = args.find(arg => !arg.startsWith('--'));
7
+ const undraft = args.includes('--undraft');
8
+
9
+ if (!seriesSlug) {
10
+ console.error('Please provide a series slug.');
11
+ console.error('Usage: bun run series-draft <series-slug> [--undraft]');
12
+ console.error('');
13
+ console.error('Options:');
14
+ console.error(' --undraft Remove draft status instead of setting it');
15
+ process.exit(1);
16
+ }
17
+
18
+ const contentDir = path.join(process.cwd(), 'content', 'posts');
19
+ const seriesDir = path.join(process.cwd(), 'content', 'series', seriesSlug);
20
+
21
+ // Check if series exists
22
+ if (!fs.existsSync(seriesDir)) {
23
+ console.error(`Error: Series "${seriesSlug}" not found at ${seriesDir}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ // Read series metadata to get manual posts list
28
+ let manualPosts: string[] = [];
29
+ const seriesIndexMdx = path.join(seriesDir, 'index.mdx');
30
+ const seriesIndexMd = path.join(seriesDir, 'index.md');
31
+ let seriesIndexPath = '';
32
+
33
+ if (fs.existsSync(seriesIndexMdx)) {
34
+ seriesIndexPath = seriesIndexMdx;
35
+ } else if (fs.existsSync(seriesIndexMd)) {
36
+ seriesIndexPath = seriesIndexMd;
37
+ }
38
+
39
+ if (seriesIndexPath) {
40
+ const seriesContent = fs.readFileSync(seriesIndexPath, 'utf8');
41
+ const { data } = matter(seriesContent);
42
+ if (data.posts && Array.isArray(data.posts)) {
43
+ manualPosts = data.posts;
44
+ }
45
+ }
46
+
47
+ // Find all post files that belong to this series
48
+ const postFiles: { path: string; slug: string }[] = [];
49
+
50
+ // 1. Check posts in series folder
51
+ if (fs.existsSync(seriesDir)) {
52
+ const items = fs.readdirSync(seriesDir, { withFileTypes: true });
53
+ for (const item of items) {
54
+ if (item.name === 'index.mdx' || item.name === 'index.md') continue;
55
+
56
+ if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
57
+ const slug = item.name.replace(/\.mdx?$/, '');
58
+ postFiles.push({ path: path.join(seriesDir, item.name), slug });
59
+ } else if (item.isDirectory()) {
60
+ const indexMdx = path.join(seriesDir, item.name, 'index.mdx');
61
+ const indexMd = path.join(seriesDir, item.name, 'index.md');
62
+ if (fs.existsSync(indexMdx)) {
63
+ postFiles.push({ path: indexMdx, slug: item.name });
64
+ } else if (fs.existsSync(indexMd)) {
65
+ postFiles.push({ path: indexMd, slug: item.name });
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // 2. Check posts in content/posts with series frontmatter or in manual list
72
+ if (fs.existsSync(contentDir)) {
73
+ const items = fs.readdirSync(contentDir, { withFileTypes: true });
74
+ for (const item of items) {
75
+ let filePath = '';
76
+ let slug = '';
77
+
78
+ if (item.isFile() && (item.name.endsWith('.mdx') || item.name.endsWith('.md'))) {
79
+ filePath = path.join(contentDir, item.name);
80
+ slug = item.name.replace(/\.mdx?$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '');
81
+ } else if (item.isDirectory()) {
82
+ const indexMdx = path.join(contentDir, item.name, 'index.mdx');
83
+ const indexMd = path.join(contentDir, item.name, 'index.md');
84
+ if (fs.existsSync(indexMdx)) {
85
+ filePath = indexMdx;
86
+ } else if (fs.existsSync(indexMd)) {
87
+ filePath = indexMd;
88
+ }
89
+ slug = item.name.replace(/^\d{4}-\d{2}-\d{2}-/, '');
90
+ }
91
+
92
+ if (!filePath) continue;
93
+
94
+ const content = fs.readFileSync(filePath, 'utf8');
95
+ const { data } = matter(content);
96
+
97
+ // Include if series matches or slug is in manual posts list
98
+ if (data.series === seriesSlug || manualPosts.includes(slug)) {
99
+ // Avoid duplicates
100
+ if (!postFiles.find(p => p.path === filePath)) {
101
+ postFiles.push({ path: filePath, slug });
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ if (postFiles.length === 0) {
108
+ console.log(`No posts found in series "${seriesSlug}".`);
109
+ process.exit(0);
110
+ }
111
+
112
+ console.log(`Found ${postFiles.length} post(s) in series "${seriesSlug}":`);
113
+
114
+ let updated = 0;
115
+ for (const { path: filePath, slug } of postFiles) {
116
+ const content = fs.readFileSync(filePath, 'utf8');
117
+ const { data, content: body } = matter(content);
118
+
119
+ const currentDraft = data.draft === true;
120
+ const targetDraft = !undraft;
121
+
122
+ if (currentDraft === targetDraft) {
123
+ console.log(` [skip] ${slug} - already ${targetDraft ? 'draft' : 'published'}`);
124
+ continue;
125
+ }
126
+
127
+ data.draft = targetDraft;
128
+
129
+ const newContent = matter.stringify(body, data);
130
+ fs.writeFileSync(filePath, newContent);
131
+
132
+ console.log(` [${targetDraft ? 'draft' : 'publish'}] ${slug}`);
133
+ updated++;
134
+ }
135
+
136
+ console.log(`\nUpdated ${updated} post(s).`);
package/site.config.ts ADDED
@@ -0,0 +1,91 @@
1
+ export const siteConfig = {
2
+ title: { en: "Amytis", zh: "Amytis" },
3
+ description: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
4
+ baseUrl: "https://example.com", // Replace with your actual domain
5
+ footerText: { en: `© ${new Date().getFullYear()} Amytis. All rights reserved.`, zh: `© ${new Date().getFullYear()} Amytis. 保留所有权利。` },
6
+ nav: [
7
+ { name: "Home", url: "/", weight: 1 },
8
+ { name: "Flow", url: "/flows", weight: 1.1 },
9
+ { name: "Books", url: "/books", weight: 1.3 },
10
+ { name: "Series", url: "/series", weight: 1.5 },
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",
19
+ },
20
+ series: {
21
+ navbar: ["digital-garden", "markdown-showcase", "ai-nexus-weekly"], // Slugs of series to show in navbar
22
+ },
23
+ books: {
24
+ navbar: [] as string[], // Slugs of books to show in navbar dropdown
25
+ },
26
+ archive: {
27
+ showAuthors: true,
28
+ },
29
+ pagination: {
30
+ posts: 5,
31
+ series: 1,
32
+ flows: 20,
33
+ },
34
+ includeDateInUrl: false,
35
+ // trailingSlash is configured in next.config.ts (Next.js handles URL normalization)
36
+ showFuturePosts: false,
37
+ toc: true,
38
+ themeColor: 'default', // 'default' | 'blue' | 'rose' | 'amber'
39
+ hero: {
40
+ tagline: { en: "Digital Garden", zh: "数字花园" },
41
+ title: { en: "Cultivating Digital Knowledge", zh: "培育数字知识" },
42
+ subtitle: { en: "A minimalist digital garden for growing thoughts and sharing knowledge.", zh: "一个极简的数字花园,用于培育思想和分享知识。" },
43
+ },
44
+ about: {
45
+ title: { en: "About Amytis", zh: "关于 Amytis" },
46
+ subtitle: { en: "Learn more about the philosophy and technology behind this digital garden.", zh: "了解这座数字花园背后的理念与技术。" },
47
+ },
48
+ flows: {
49
+ recentCount: 5,
50
+ },
51
+ featured: {
52
+ series: {
53
+ scrollThreshold: 2, // Enable scrolling when more than this number
54
+ maxItems: 6,
55
+ },
56
+ stories: {
57
+ scrollThreshold: 1, // Enable scrolling when more than this number
58
+ maxItems: 4,
59
+ },
60
+ },
61
+ i18n: {
62
+ defaultLocale: 'en',
63
+ locales: ['en', 'zh'],
64
+ },
65
+ analytics: {
66
+ provider: 'umami', // 'umami' | 'plausible' | 'google' | null
67
+ umami: {
68
+ websiteId: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || '', // Your Umami Website ID
69
+ src: process.env.NEXT_PUBLIC_UMAMI_URL || 'https://us.umami.is/script.js', // Default or self-hosted URL
70
+ },
71
+ plausible: {
72
+ domain: '', // Your domain
73
+ src: 'https://plausible.io/js/script.js',
74
+ },
75
+ google: {
76
+ measurementId: '', // G-XXXXXXXXXX
77
+ },
78
+ },
79
+ comments: {
80
+ provider: 'giscus', // 'giscus' | 'disqus' | null
81
+ giscus: {
82
+ repo: 'hutusi/amytis', // username/repo
83
+ repoId: 'R_kgDOQ1YSwA',
84
+ category: 'Announcements',
85
+ categoryId: 'DIC_kwDOQ1YSwM4C2NmL',
86
+ },
87
+ disqus: {
88
+ shortname: '',
89
+ },
90
+ },
91
+ };
@@ -0,0 +1,67 @@
1
+ import { getPageBySlug, getAllPages } from '@/lib/markdown';
2
+ import { notFound } from 'next/navigation';
3
+ import PostLayout from '@/layouts/PostLayout';
4
+ import SimpleLayout from '@/layouts/SimpleLayout';
5
+ import { Metadata } from 'next';
6
+ import { siteConfig } from '../../../site.config';
7
+ import { resolveLocale } from '@/lib/i18n';
8
+
9
+ /**
10
+ * Generates the static paths for all top-level pages at build time.
11
+ */
12
+ export async function generateStaticParams() {
13
+ const pages = getAllPages();
14
+ return pages.map((page) => ({
15
+ slug: page.slug,
16
+ }));
17
+ }
18
+
19
+ export const dynamicParams = false;
20
+
21
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
22
+ const { slug: rawSlug } = await params;
23
+ const slug = decodeURIComponent(rawSlug);
24
+ const page = getPageBySlug(slug);
25
+
26
+ if (!page) {
27
+ return { title: 'Page Not Found' };
28
+ }
29
+
30
+ return {
31
+ title: `${page.title} | ${resolveLocale(siteConfig.title)}`,
32
+ description: page.excerpt,
33
+ };
34
+ }
35
+
36
+ export default async function Page({
37
+ params,
38
+ }: {
39
+ params: Promise<{ slug: string }>;
40
+ }) {
41
+ const { slug: rawSlug } = await params;
42
+ const slug = decodeURIComponent(rawSlug);
43
+ const page = getPageBySlug(slug);
44
+
45
+ if (!page) {
46
+ notFound();
47
+ }
48
+
49
+ // Determine layout based on frontmatter, defaulting to 'simple' for pages
50
+ const layout = page.layout || 'simple';
51
+
52
+ if (layout === 'post') {
53
+ return <PostLayout post={page} />;
54
+ }
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
+ return <SimpleLayout post={page} />;
67
+ }
@@ -0,0 +1,147 @@
1
+ import Link from 'next/link';
2
+ import { getAllPosts, PostData } from '@/lib/markdown';
3
+ import { siteConfig } from '../../../site.config';
4
+ import { resolveLocale } from '@/lib/i18n';
5
+ import PageHeader from '@/components/PageHeader';
6
+
7
+ export const metadata = {
8
+ title: `Archive | ${resolveLocale(siteConfig.title)}`,
9
+ description: 'A complete list of all notes and thoughts.',
10
+ };
11
+
12
+ // Use month number as key for reliable sorting across all locales
13
+ type GroupedPosts = Record<string, Record<string, PostData[]>>;
14
+
15
+ const locale = siteConfig.i18n.defaultLocale === 'zh' ? 'zh-CN' : siteConfig.i18n.defaultLocale;
16
+
17
+ function getMonthName(monthNum: number): string {
18
+ return new Date(2000, monthNum - 1).toLocaleString(locale, { month: 'long' });
19
+ }
20
+
21
+ function groupPostsByDate(posts: PostData[]): GroupedPosts {
22
+ const groups: GroupedPosts = {};
23
+
24
+ posts.forEach((post) => {
25
+ const date = new Date(post.date);
26
+ const year = date.getFullYear().toString();
27
+ const month = (date.getMonth() + 1).toString();
28
+
29
+ if (!groups[year]) {
30
+ groups[year] = {};
31
+ }
32
+ if (!groups[year][month]) {
33
+ groups[year][month] = [];
34
+ }
35
+ groups[year][month].push(post);
36
+ });
37
+
38
+ return groups;
39
+ }
40
+
41
+ export default function ArchivePage() {
42
+ const posts = getAllPosts();
43
+ const groupedPosts = groupPostsByDate(posts);
44
+ const showAuthors = siteConfig.archive?.showAuthors;
45
+
46
+ // Sort years descending to show newest content first
47
+ const years = Object.keys(groupedPosts).sort((a, b) => Number(b) - Number(a));
48
+ const totalPosts = posts.length;
49
+
50
+ return (
51
+ <div className="layout-main">
52
+ <PageHeader
53
+ titleKey="archive"
54
+ subtitleKey="archive_subtitle"
55
+ subtitleOneKey="archive_subtitle_one"
56
+ count={years.length}
57
+ subtitleParams={{ count: totalPosts, years: years.length }}
58
+ />
59
+
60
+ <main className="max-w-4xl mx-auto">
61
+ <div className="space-y-24">
62
+ {years.map((year) => {
63
+ // Sort months within the year in descending order (December -> January)
64
+ const months = Object.keys(groupedPosts[year]).sort((a, b) => Number(b) - Number(a));
65
+
66
+ // Calculate total posts for the year
67
+ const yearTotal = months.reduce((total, month) => total + groupedPosts[year][month].length, 0);
68
+
69
+ return (
70
+ <section key={year} className="relative grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-16">
71
+ {/* Year Marker */}
72
+ <div className="relative">
73
+ <div className="sticky top-24 lg:top-32 text-left md:text-right">
74
+ <h2 className="text-4xl md:text-5xl font-serif font-bold text-muted/50">
75
+ {year}
76
+ </h2>
77
+ <span className="block text-xs font-bold uppercase tracking-widest text-muted mt-2">
78
+ {yearTotal} Posts
79
+ </span>
80
+ </div>
81
+ </div>
82
+
83
+ {/* Content Timeline */}
84
+ <div className="relative border-l-2 border-muted/20 pl-8 md:pl-12 space-y-16">
85
+ {months.map((month) => {
86
+ const monthPosts = groupedPosts[year][month];
87
+ return (
88
+ <div key={month} className="relative">
89
+ {/* Month Marker - positioned relative to border */}
90
+ <div className="absolute -left-[calc(2rem+1px)] md:-left-[calc(3rem+1px)] -translate-x-1/2 top-1.5 w-3 h-3 rounded-full bg-background border-2 border-accent/50"></div>
91
+
92
+ <h3 className="text-base font-sans font-bold uppercase tracking-widest text-accent mb-8">
93
+ {getMonthName(Number(month))}
94
+ <span className="ml-2 text-xs font-normal text-muted/60">({monthPosts.length})</span>
95
+ </h3>
96
+
97
+ <ul className="space-y-6">
98
+ {monthPosts.map((post) => {
99
+ const dateObj = new Date(post.date);
100
+ const day = dateObj.getDate().toString().padStart(2, '0');
101
+
102
+ return (
103
+ <li key={post.slug} className="group">
104
+ <Link href={`/posts/${post.slug}`} className="block no-underline">
105
+ <div className="flex flex-col sm:flex-row sm:items-baseline justify-between gap-2 sm:gap-6">
106
+ <div className="flex items-baseline gap-6">
107
+ <span className="font-mono text-base text-muted shrink-0 w-8">
108
+ {day}
109
+ </span>
110
+ <div className="flex items-baseline gap-2">
111
+ <h4 className="text-xl font-serif font-medium text-heading/80 group-hover:text-accent transition-colors duration-200">
112
+ {post.title}
113
+ </h4>
114
+ {post.series && (
115
+ <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-[10ch] truncate inline-block align-baseline"
118
+ >
119
+ {post.series}
120
+ </span>
121
+ )}
122
+ </div>
123
+ </div>
124
+ {showAuthors && post.authors.length > 0 && (
125
+ <span className="text-sm font-sans italic text-muted/60 shrink-0 hidden sm:block">
126
+ {post.authors.join(', ')}
127
+ </span>
128
+ )}
129
+ </div>
130
+ </Link>
131
+ </li>
132
+ );
133
+ })}
134
+ </ul>
135
+ </div>
136
+ );
137
+ })}
138
+ </div>
139
+ </section>
140
+ );
141
+
142
+ })}
143
+ </div>
144
+ </main>
145
+ </div>
146
+ );
147
+ }