@hutusi/amytis 1.6.0 → 1.8.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 (92) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/GEMINI.md +12 -2
  3. package/README.md +14 -0
  4. package/TODO.md +24 -16
  5. package/bun.lock +8 -3
  6. package/content/about.mdx +1 -0
  7. package/content/about.zh.mdx +21 -0
  8. package/content/flows/2026/02/05.md +0 -1
  9. package/content/flows/2026/02/10.mdx +2 -1
  10. package/content/flows/2026/02/15.md +2 -1
  11. package/content/flows/2026/02/18.mdx +2 -1
  12. package/content/flows/2026/02/20.md +15 -0
  13. package/content/links.mdx +42 -0
  14. package/content/links.zh.mdx +41 -0
  15. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  16. package/content/notes/digital-garden-philosophy.mdx +36 -0
  17. package/content/notes/react-server-components.mdx +49 -0
  18. package/content/notes/tailwind-v4.mdx +45 -0
  19. package/content/notes/zettelkasten-method.mdx +33 -0
  20. package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
  21. package/content/posts/multimedia-showcase/index.mdx +261 -0
  22. package/content/privacy.mdx +32 -0
  23. package/content/privacy.zh.mdx +32 -0
  24. package/docs/ARCHITECTURE.md +16 -0
  25. package/docs/CONTRIBUTING.md +11 -0
  26. package/docs/DIGITAL_GARDEN.md +64 -0
  27. package/package.json +8 -3
  28. package/scripts/copy-assets.ts +1 -1
  29. package/scripts/generate-knowledge-graph.ts +162 -0
  30. package/scripts/new-flow.ts +0 -5
  31. package/scripts/new-note.ts +53 -0
  32. package/site.config.ts +146 -44
  33. package/src/app/[slug]/page.tsx +0 -10
  34. package/src/app/archive/page.tsx +38 -10
  35. package/src/app/books/[slug]/page.tsx +18 -0
  36. package/src/app/flows/[year]/[month]/[day]/page.tsx +51 -31
  37. package/src/app/flows/[year]/[month]/page.tsx +15 -13
  38. package/src/app/flows/[year]/page.tsx +22 -15
  39. package/src/app/flows/page/[page]/page.tsx +3 -9
  40. package/src/app/flows/page.tsx +3 -8
  41. package/src/app/globals.css +41 -0
  42. package/src/app/graph/page.tsx +19 -0
  43. package/src/app/layout.tsx +47 -21
  44. package/src/app/notes/[slug]/page.tsx +128 -0
  45. package/src/app/notes/page/[page]/page.tsx +58 -0
  46. package/src/app/notes/page.tsx +31 -0
  47. package/src/app/page.tsx +134 -72
  48. package/src/app/posts/[slug]/page.tsx +8 -12
  49. package/src/app/search.json/route.ts +15 -1
  50. package/src/app/series/[slug]/page.tsx +18 -0
  51. package/src/app/subscribe/page.tsx +17 -0
  52. package/src/app/tags/[tag]/page.tsx +9 -26
  53. package/src/app/tags/page.tsx +3 -8
  54. package/src/components/AuthorCard.tsx +43 -0
  55. package/src/components/Backlinks.tsx +39 -0
  56. package/src/components/Comments.tsx +20 -4
  57. package/src/components/ExternalLinks.tsx +6 -2
  58. package/src/components/FlowCalendarSidebar.tsx +4 -2
  59. package/src/components/FlowContent.tsx +4 -3
  60. package/src/components/FlowHubTabs.tsx +50 -0
  61. package/src/components/FlowTimelineEntry.tsx +7 -9
  62. package/src/components/Footer.tsx +35 -26
  63. package/src/components/KnowledgeGraph.tsx +324 -0
  64. package/src/components/LanguageProvider.tsx +0 -5
  65. package/src/components/LanguageSwitch.tsx +117 -6
  66. package/src/components/LocaleSwitch.tsx +33 -0
  67. package/src/components/MarkdownRenderer.tsx +13 -2
  68. package/src/components/Navbar.tsx +266 -17
  69. package/src/components/NoteContent.tsx +123 -0
  70. package/src/components/NoteSidebar.tsx +132 -0
  71. package/src/components/PostNavigation.tsx +55 -0
  72. package/src/components/PostSidebar.tsx +172 -126
  73. package/src/components/ReadingProgressBar.tsx +6 -21
  74. package/src/components/RecentNotesSection.tsx +6 -11
  75. package/src/components/RelatedPosts.tsx +1 -1
  76. package/src/components/Search.tsx +29 -5
  77. package/src/components/SelectedBooksSection.tsx +12 -6
  78. package/src/components/ShareBar.tsx +115 -0
  79. package/src/components/SimpleLayoutHeader.tsx +5 -14
  80. package/src/components/SubscribePage.tsx +298 -0
  81. package/src/components/TagContentTabs.tsx +102 -0
  82. package/src/components/TagPageHeader.tsx +7 -13
  83. package/src/components/TagSidebar.tsx +142 -0
  84. package/src/components/TagsIndexClient.tsx +156 -0
  85. package/src/hooks/useScrollY.ts +41 -0
  86. package/src/i18n/translations.ts +105 -1
  87. package/src/layouts/PostLayout.tsx +40 -8
  88. package/src/layouts/SimpleLayout.tsx +53 -15
  89. package/src/lib/markdown.ts +347 -18
  90. package/src/lib/remark-wikilinks.ts +59 -0
  91. package/src/lib/search-utils.ts +2 -1
  92. package/src/components/TableOfContents.tsx +0 -158
@@ -1,10 +1,12 @@
1
- import { getAllFlows, getFlowBySlug, getAdjacentFlows } from '@/lib/markdown';
1
+ import { getAllFlows, getFlowBySlug, getAdjacentFlows, buildSlugRegistry, getBacklinks } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../../../../site.config';
3
3
  import { Metadata } from 'next';
4
4
  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 Backlinks from '@/components/Backlinks';
9
+ import ShareBar from '@/components/ShareBar';
8
10
  import Link from 'next/link';
9
11
 
10
12
  export function generateStaticParams() {
@@ -24,6 +26,19 @@ export async function generateMetadata({ params }: { params: Promise<{ year: str
24
26
  return {
25
27
  title: `${flow.title} | ${resolveLocale(siteConfig.title)}`,
26
28
  description: flow.excerpt,
29
+ openGraph: {
30
+ title: flow.title,
31
+ description: flow.excerpt,
32
+ type: 'article',
33
+ publishedTime: flow.date,
34
+ url: `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`,
35
+ siteName: resolveLocale(siteConfig.title),
36
+ },
37
+ twitter: {
38
+ card: 'summary',
39
+ title: flow.title,
40
+ description: flow.excerpt,
41
+ },
27
42
  };
28
43
  }
29
44
 
@@ -36,53 +51,59 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
36
51
  const allFlows = getAllFlows();
37
52
  const entryDates = allFlows.map(f => f.date);
38
53
  const { prev, next } = getAdjacentFlows(flow.slug);
54
+ const slugRegistry = buildSlugRegistry();
55
+ const backlinks = getBacklinks(flow.slug);
56
+ const flowUrl = `${siteConfig.baseUrl}/flows/${year}/${month}/${day}`;
57
+
58
+ const breadcrumb = (
59
+ <nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1.5 text-sm text-muted">
60
+ <Link href="/flows" className="hover:text-accent no-underline shrink-0">
61
+ {t('all_flows')}
62
+ </Link>
63
+ <span className="text-muted/40" aria-hidden="true">›</span>
64
+ <Link href={`/flows/${year}`} className="hover:text-accent no-underline shrink-0">
65
+ {year}
66
+ </Link>
67
+ <span className="text-muted/40" aria-hidden="true">›</span>
68
+ <Link href={`/flows/${year}/${month}`} className="hover:text-accent no-underline shrink-0">
69
+ {month}
70
+ </Link>
71
+ <span className="text-muted/40" aria-hidden="true">›</span>
72
+ <span className="text-foreground shrink-0">{day}</span>
73
+ </nav>
74
+ );
39
75
 
40
76
  return (
41
77
  <div className="layout-main">
42
- {/* Breadcrumb navigation */}
43
- <nav className="flex items-center gap-1.5 text-sm text-muted mb-6">
44
- <Link href="/flows" className="hover:text-accent no-underline">
45
- {t('all_flows')}
46
- </Link>
47
- <span className="text-muted/40">›</span>
48
- <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
49
- {year}
50
- </Link>
51
- <span className="text-muted/40">›</span>
52
- <Link href={`/flows/${year}/${month}`} className="hover:text-accent no-underline">
53
- {month}
54
- </Link>
55
- <span className="text-muted/40">›</span>
56
- <span className="text-foreground">{day}</span>
57
- </nav>
58
-
59
78
  <div className="flex gap-10">
60
- <FlowCalendarSidebar entryDates={entryDates} currentDate={flow.date} />
79
+ <FlowCalendarSidebar entryDates={entryDates} currentDate={flow.date} breadcrumb={breadcrumb} />
61
80
 
62
81
  <article className="flex-1 min-w-0">
63
82
  {/* Header */}
64
83
  <header className="mb-8">
65
- <time className="text-sm font-mono text-accent" data-pagefind-meta="date[content]">{flow.date}</time>
66
- <h1 className="mt-2 text-3xl md:text-4xl font-serif font-bold text-heading">{flow.title}</h1>
84
+ <time className="text-base font-mono text-accent" data-pagefind-meta="date[content]">{flow.date}</time>
67
85
  </header>
68
86
 
69
87
  {/* Content */}
70
88
  <div className="prose prose-lg dark:prose-invert max-w-none">
71
- <MarkdownRenderer content={flow.content} />
89
+ <MarkdownRenderer content={flow.content} slugRegistry={slugRegistry} />
72
90
  </div>
73
91
 
92
+ <Backlinks backlinks={backlinks} />
93
+
94
+ <ShareBar url={flowUrl} title={flow.title} className="mt-8 mb-2" />
95
+
74
96
  {/* Prev/Next navigation */}
75
- <nav className="mt-16 pt-8 border-t border-muted/20 grid grid-cols-2 gap-4">
97
+ <nav aria-label="Post navigation" className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
76
98
  {prev ? (
77
99
  <Link
78
100
  href={`/flows/${prev.slug}`}
79
101
  className="group text-left no-underline"
80
102
  >
81
- <span className="text-xs text-muted">Older</span>
82
- <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
83
- {prev.title}
103
+ <span className="text-xs text-muted">{t('older')}</span>
104
+ <div className="text-sm font-mono text-heading group-hover:text-accent transition-colors">
105
+ {prev.date}
84
106
  </div>
85
- <span className="text-xs font-mono text-muted">{prev.date}</span>
86
107
  </Link>
87
108
  ) : <div />}
88
109
  {next ? (
@@ -90,11 +111,10 @@ export default async function FlowPage({ params }: { params: Promise<{ year: str
90
111
  href={`/flows/${next.slug}`}
91
112
  className="group text-right no-underline"
92
113
  >
93
- <span className="text-xs text-muted">Newer</span>
94
- <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
95
- {next.title}
114
+ <span className="text-xs text-muted">{t('newer')}</span>
115
+ <div className="text-sm font-mono text-heading group-hover:text-accent transition-colors">
116
+ {next.date}
96
117
  </div>
97
- <span className="text-xs font-mono text-muted">{next.date}</span>
98
118
  </Link>
99
119
  ) : <div />}
100
120
  </nav>
@@ -39,6 +39,20 @@ export default async function FlowsMonthPage({ params }: { params: Promise<{ yea
39
39
  const tags = getFlowTags();
40
40
  const monthLabel = new Date(parseInt(year), parseInt(month) - 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
41
41
 
42
+ const breadcrumb = (
43
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
44
+ <Link href="/flows" className="hover:text-accent no-underline">
45
+ {t('all_flows')}
46
+ </Link>
47
+ <span className="text-muted/40" aria-hidden="true">›</span>
48
+ <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
49
+ {year}
50
+ </Link>
51
+ <span className="text-muted/40" aria-hidden="true">›</span>
52
+ <span className="text-foreground">{month}</span>
53
+ </nav>
54
+ );
55
+
42
56
  return (
43
57
  <div className="layout-main">
44
58
  <PageHeader
@@ -48,24 +62,12 @@ export default async function FlowsMonthPage({ params }: { params: Promise<{ yea
48
62
  subtitleParams={{ count: flows.length }}
49
63
  />
50
64
 
51
- {/* Breadcrumb navigation */}
52
- <nav className="flex items-center gap-1.5 text-sm text-muted mb-6">
53
- <Link href="/flows" className="hover:text-accent no-underline">
54
- {t('all_flows')}
55
- </Link>
56
- <span className="text-muted/40">›</span>
57
- <Link href={`/flows/${year}`} className="hover:text-accent no-underline">
58
- {year}
59
- </Link>
60
- <span className="text-muted/40">›</span>
61
- <span className="text-foreground">{month}</span>
62
- </nav>
63
-
64
65
  <FlowContent
65
66
  flows={flows}
66
67
  entryDates={entryDates}
67
68
  tags={tags}
68
69
  currentDate={`${year}-${month}-01`}
70
+ breadcrumb={breadcrumb}
69
71
  />
70
72
  </div>
71
73
  );
@@ -44,38 +44,45 @@ export default async function FlowsYearPage({ params }: { params: Promise<{ year
44
44
  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
45
45
  ];
46
46
 
47
- return (
48
- <div className="layout-main">
49
- <PageHeader
50
- titleKey="flows_in_year"
51
- titleParams={{ year }}
52
- subtitleKey="flow_subtitle"
53
- subtitleParams={{ count: flows.length }}
54
- />
55
-
56
- {/* Month navigation pills */}
57
- <div className="flex flex-wrap items-center gap-2 mb-6">
58
- <Link href="/flows" className="text-sm text-muted hover:text-accent no-underline">
59
- ← {t('all_flows')}
47
+ const breadcrumb = (
48
+ <div className="space-y-2">
49
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
50
+ <Link href="/flows" className="hover:text-accent no-underline">
51
+ {t('all_flows')}
60
52
  </Link>
61
- <span className="text-muted/30">|</span>
53
+ <span className="text-muted/40" aria-hidden="true">›</span>
54
+ <span className="text-foreground">{year}</span>
55
+ </nav>
56
+ <div className="flex flex-wrap gap-1.5">
62
57
  {sortedMonths.map(m => (
63
58
  <Link
64
59
  key={m}
65
60
  href={`/flows/${year}/${m}`}
66
- className="inline-flex items-center gap-1 px-3 py-1 text-xs rounded-full border border-muted/20 text-foreground hover:border-accent hover:text-accent no-underline transition-colors"
61
+ className="inline-flex items-center gap-1 px-2.5 py-0.5 text-xs rounded-full border border-muted/20 text-foreground hover:border-accent hover:text-accent no-underline transition-colors"
67
62
  >
68
63
  {monthNames[parseInt(m, 10) - 1]}
69
64
  <span className="text-muted text-[10px]">({monthCounts[m]})</span>
70
65
  </Link>
71
66
  ))}
72
67
  </div>
68
+ </div>
69
+ );
70
+
71
+ return (
72
+ <div className="layout-main">
73
+ <PageHeader
74
+ titleKey="flows_in_year"
75
+ titleParams={{ year }}
76
+ subtitleKey="flow_subtitle"
77
+ subtitleParams={{ count: flows.length }}
78
+ />
73
79
 
74
80
  <FlowContent
75
81
  flows={flows}
76
82
  entryDates={entryDates}
77
83
  tags={tags}
78
84
  currentDate={`${year}-01-01`}
85
+ breadcrumb={breadcrumb}
79
86
  />
80
87
  </div>
81
88
  );
@@ -2,9 +2,9 @@ import { getAllFlows, getFlowTags } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../../../site.config';
3
3
  import { Metadata } from 'next';
4
4
  import { notFound } from 'next/navigation';
5
- import { t, resolveLocale } from '@/lib/i18n';
6
- import PageHeader from '@/components/PageHeader';
5
+ import { t, tWith, resolveLocale } from '@/lib/i18n';
7
6
  import FlowContent from '@/components/FlowContent';
7
+ import FlowHubTabs from '@/components/FlowHubTabs';
8
8
 
9
9
  const PAGE_SIZE = siteConfig.pagination.flows;
10
10
 
@@ -45,13 +45,7 @@ export default async function FlowsPaginatedPage({ params }: { params: Promise<{
45
45
 
46
46
  return (
47
47
  <div className="layout-main">
48
- <PageHeader
49
- titleKey="flow"
50
- subtitleKey="page_of_total"
51
- subtitleParams={{ page, total: totalPages }}
52
- className="mb-12"
53
- />
54
-
48
+ <FlowHubTabs subtitle={tWith('page_of_total', { page, total: totalPages })} />
55
49
  <FlowContent
56
50
  flows={flows}
57
51
  entryDates={entryDates}
@@ -1,9 +1,9 @@
1
1
  import { getAllFlows, getFlowTags } from '@/lib/markdown';
2
2
  import { siteConfig } from '../../../site.config';
3
3
  import { Metadata } from 'next';
4
- import { t, resolveLocale } from '@/lib/i18n';
5
- import PageHeader from '@/components/PageHeader';
4
+ import { t, tWith, resolveLocale } from '@/lib/i18n';
6
5
  import FlowContent from '@/components/FlowContent';
6
+ import FlowHubTabs from '@/components/FlowHubTabs';
7
7
 
8
8
  const PAGE_SIZE = siteConfig.pagination.flows;
9
9
 
@@ -21,12 +21,7 @@ export default function FlowsPage() {
21
21
 
22
22
  return (
23
23
  <div className="layout-main">
24
- <PageHeader
25
- titleKey="flow"
26
- subtitleKey="flow_subtitle"
27
- subtitleParams={{ count: allFlows.length }}
28
- />
29
-
24
+ <FlowHubTabs subtitle={tWith('flow_subtitle', { count: allFlows.length })} />
30
25
  <FlowContent
31
26
  flows={flows}
32
27
  entryDates={entryDates}
@@ -54,6 +54,47 @@
54
54
  --accent-hover: #f59e0b;
55
55
  }
56
56
 
57
+ /* Wikilink styles */
58
+ .wikilink {
59
+ text-decoration: none;
60
+ transition: color 0.15s;
61
+ }
62
+ .wikilink::before,
63
+ .wikilink::after {
64
+ font-family: var(--font-mono);
65
+ font-size: 0.72em;
66
+ opacity: 0.45;
67
+ vertical-align: 0.08em;
68
+ transition: opacity 0.15s;
69
+ }
70
+ .wikilink::before { content: '[['; }
71
+ .wikilink::after { content: ']]'; }
72
+ .wikilink--resolved {
73
+ color: var(--accent);
74
+ }
75
+ .wikilink--resolved:hover,
76
+ .wikilink--resolved:focus-visible {
77
+ color: var(--accent-hover);
78
+ text-decoration: underline;
79
+ outline: none;
80
+ }
81
+ .wikilink--resolved:focus-visible {
82
+ outline: 2px solid var(--accent);
83
+ outline-offset: 2px;
84
+ border-radius: 2px;
85
+ }
86
+ .wikilink--resolved:hover::before,
87
+ .wikilink--resolved:hover::after,
88
+ .wikilink--resolved:focus-visible::before,
89
+ .wikilink--resolved:focus-visible::after {
90
+ opacity: 0.75;
91
+ }
92
+ .wikilink--broken {
93
+ color: var(--muted);
94
+ text-decoration: underline dashed;
95
+ cursor: default;
96
+ }
97
+
57
98
  /* PrismJS Syntax Highlighting Custom Theme */
58
99
  code[class*="language-"],
59
100
  pre[class*="language-"] {
@@ -0,0 +1,19 @@
1
+ import { Metadata } from 'next';
2
+ import { t, resolveLocale } from '@/lib/i18n';
3
+ import { siteConfig } from '../../../site.config';
4
+ import FlowHubTabs from '@/components/FlowHubTabs';
5
+ import KnowledgeGraph from '@/components/KnowledgeGraph';
6
+
7
+ export const metadata: Metadata = {
8
+ title: `${t('tab_graph')} | ${resolveLocale(siteConfig.title)}`,
9
+ description: t('graph_subtitle'),
10
+ };
11
+
12
+ export default function GraphPage() {
13
+ return (
14
+ <div className="layout-main">
15
+ <FlowHubTabs subtitle={t('graph_subtitle')} />
16
+ <KnowledgeGraph />
17
+ </div>
18
+ );
19
+ }
@@ -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 allSeries = getAllSeries();
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
- const seriesList = filteredKeys.map(slug => ({
73
- name: allSeries[slug][0]?.series || slug,
74
- slug,
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 allBooks = getAllBooks();
79
- const featuredBookSlugs = siteConfig.books?.navbar;
80
- const booksList = featuredBookSlugs && featuredBookSlugs.length > 0
81
- ? allBooks
82
- .filter(book => featuredBookSlugs.includes(book.slug))
83
- .map(book => ({ name: book.title, slug: book.slug }))
84
- : allBooks.map(book => ({ name: book.title, slug: book.slug }));
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="en" suppressHydrationWarning>
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}
@@ -0,0 +1,128 @@
1
+ import { getAllNotes, getNoteBySlug, buildSlugRegistry, getBacklinks, getAdjacentNotes } from '@/lib/markdown';
2
+ import { notFound } from 'next/navigation';
3
+ import { Metadata } from 'next';
4
+ import { siteConfig } from '../../../../site.config';
5
+ import { t, resolveLocale } from '@/lib/i18n';
6
+ import MarkdownRenderer from '@/components/MarkdownRenderer';
7
+ import NoteSidebar from '@/components/NoteSidebar';
8
+ import Tag from '@/components/Tag';
9
+ import Link from 'next/link';
10
+
11
+ export function generateStaticParams() {
12
+ const notes = getAllNotes();
13
+ // Return a placeholder when empty so Next.js static export doesn't error on the dynamic route
14
+ if (notes.length === 0) return [{ slug: '_empty' }];
15
+ return notes.map(note => ({ slug: note.slug }));
16
+ }
17
+
18
+ export const dynamicParams = false;
19
+
20
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
21
+ const { slug } = await params;
22
+ const note = getNoteBySlug(slug);
23
+ if (!note) return { title: 'Not Found' };
24
+ return {
25
+ title: `${note.title} | ${resolveLocale(siteConfig.title)}`,
26
+ description: note.excerpt,
27
+ openGraph: {
28
+ title: note.title,
29
+ description: note.excerpt,
30
+ type: 'article',
31
+ publishedTime: note.date,
32
+ siteName: resolveLocale(siteConfig.title),
33
+ },
34
+ };
35
+ }
36
+
37
+ export default async function NotePage({ params }: { params: Promise<{ slug: string }> }) {
38
+ const { slug } = await params;
39
+ const note = getNoteBySlug(slug);
40
+ if (!note) notFound();
41
+
42
+ const slugRegistry = buildSlugRegistry();
43
+ const backlinks = getBacklinks(slug);
44
+ const { prev, next } = getAdjacentNotes(slug);
45
+
46
+ const showToc = note.toc !== false && note.headings.length > 0;
47
+ const visibleBacklinks = note.backlinks !== false ? backlinks : [];
48
+ const showSidebar = showToc || visibleBacklinks.length > 0;
49
+
50
+ const breadcrumb = (
51
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
52
+ <Link href="/notes" className="hover:text-accent no-underline">
53
+ {t('notes')}
54
+ </Link>
55
+ <span className="text-muted/40" aria-hidden="true">›</span>
56
+ <span className="text-foreground truncate">{note.title}</span>
57
+ </nav>
58
+ );
59
+
60
+ return (
61
+ <div className="layout-main">
62
+ {!showSidebar && <div className="mb-6">{breadcrumb}</div>}
63
+
64
+ <div className={showSidebar
65
+ ? 'grid grid-cols-1 lg:grid-cols-[280px_minmax(0,1fr)] gap-8 items-start'
66
+ : 'max-w-3xl mx-auto'
67
+ }>
68
+ {showSidebar && (
69
+ <NoteSidebar
70
+ headings={note.headings}
71
+ showToc={showToc}
72
+ backlinks={visibleBacklinks}
73
+ breadcrumb={breadcrumb}
74
+ />
75
+ )}
76
+ <article className="min-w-0 max-w-3xl mx-auto">
77
+ <header className="mb-8 border-b border-muted/10 pb-8">
78
+ {note.draft && (
79
+ <div className="mb-4">
80
+ <span className="text-xs font-bold text-red-500 bg-red-100 dark:bg-red-900/30 px-2 py-1 rounded tracking-widest inline-block">
81
+ DRAFT
82
+ </span>
83
+ </div>
84
+ )}
85
+ <time className="text-sm font-mono text-accent" data-pagefind-meta="date[content]">
86
+ {note.date}
87
+ </time>
88
+ <h1 className="mt-2 text-3xl md:text-4xl font-serif font-bold text-heading leading-tight">
89
+ {note.title}
90
+ </h1>
91
+ {note.tags.length > 0 && (
92
+ <div className="flex flex-wrap gap-2 mt-4">
93
+ {note.tags.map(tag => (
94
+ <Tag key={tag} tag={tag} variant="default" />
95
+ ))}
96
+ </div>
97
+ )}
98
+ </header>
99
+
100
+ <MarkdownRenderer content={note.content} slug={note.slug} slugRegistry={slugRegistry} />
101
+
102
+ {/* Prev/Next navigation */}
103
+ <nav aria-label="Note navigation" className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
104
+ {prev ? (
105
+ <Link href={`/notes/${prev.slug}`} className="group text-left no-underline">
106
+ <span className="text-xs text-muted">{t('older')}</span>
107
+ <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
108
+ {prev.title}
109
+ </div>
110
+ <span className="text-xs font-mono text-muted">{prev.date}</span>
111
+ </Link>
112
+ ) : <div />}
113
+ {next ? (
114
+ <Link href={`/notes/${next.slug}`} className="group text-right no-underline">
115
+ <span className="text-xs text-muted">{t('newer')}</span>
116
+ <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
117
+ {next.title}
118
+ </div>
119
+ <span className="text-xs font-mono text-muted">{next.date}</span>
120
+ </Link>
121
+ ) : <div />}
122
+ </nav>
123
+ </article>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
@@ -0,0 +1,58 @@
1
+ import { getAllNotes, getNoteTags } from '@/lib/markdown';
2
+ import { siteConfig } from '../../../../../site.config';
3
+ import { Metadata } from 'next';
4
+ import { notFound } from 'next/navigation';
5
+ import { t, resolveLocale } from '@/lib/i18n';
6
+ import PageHeader from '@/components/PageHeader';
7
+ import NoteContent from '@/components/NoteContent';
8
+ import FlowHubTabs from '@/components/FlowHubTabs';
9
+
10
+ const PAGE_SIZE = siteConfig.pagination.notes ?? 20;
11
+
12
+ export function generateStaticParams() {
13
+ const allNotes = getAllNotes();
14
+ const totalPages = Math.ceil(allNotes.length / PAGE_SIZE);
15
+ const pageCount = Math.max(1, totalPages - 1);
16
+ return Array.from({ length: pageCount }, (_, i) => ({
17
+ page: (i + 2).toString(),
18
+ }));
19
+ }
20
+
21
+ export const dynamicParams = false;
22
+
23
+ export async function generateMetadata({ params }: { params: Promise<{ page: string }> }): Promise<Metadata> {
24
+ const { page } = await params;
25
+ return {
26
+ title: `${t('notes')} - ${page} | ${resolveLocale(siteConfig.title)}`,
27
+ };
28
+ }
29
+
30
+ export default async function NotesPaginatedPage({ params }: { params: Promise<{ page: string }> }) {
31
+ const { page: pageStr } = await params;
32
+ const page = parseInt(pageStr, 10);
33
+ const allNotes = getAllNotes();
34
+ const totalPages = Math.ceil(allNotes.length / PAGE_SIZE);
35
+
36
+ if (page > totalPages) notFound();
37
+
38
+ const tags = getNoteTags();
39
+ const start = (page - 1) * PAGE_SIZE;
40
+ const notes = allNotes.slice(start, start + PAGE_SIZE);
41
+
42
+ return (
43
+ <div className="layout-main">
44
+ <PageHeader
45
+ titleKey="notes"
46
+ subtitleKey="page_of_total"
47
+ subtitleParams={{ page, total: totalPages }}
48
+ className="mb-12"
49
+ />
50
+ <FlowHubTabs />
51
+ <NoteContent
52
+ notes={notes}
53
+ tags={tags}
54
+ pagination={{ currentPage: page, totalPages, basePath: '/notes' }}
55
+ />
56
+ </div>
57
+ );
58
+ }