@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
@@ -2,13 +2,14 @@ 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
 
11
11
  export function generateStaticParams() {
12
+ if (siteConfig.features?.flow?.enabled === false) return [{ page: '2' }];
12
13
  const allFlows = getAllFlows();
13
14
  const totalPages = Math.ceil(allFlows.length / PAGE_SIZE);
14
15
 
@@ -29,6 +30,7 @@ export async function generateMetadata({ params }: { params: Promise<{ page: str
29
30
  }
30
31
 
31
32
  export default async function FlowsPaginatedPage({ params }: { params: Promise<{ page: string }> }) {
33
+ if (siteConfig.features?.flow?.enabled === false) notFound();
32
34
  const { page: pageStr } = await params;
33
35
  const page = parseInt(pageStr, 10);
34
36
  const allFlows = getAllFlows();
@@ -45,13 +47,7 @@ export default async function FlowsPaginatedPage({ params }: { params: Promise<{
45
47
 
46
48
  return (
47
49
  <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
-
50
+ <FlowHubTabs subtitle={tWith('page_of_total', { page, total: totalPages })} />
55
51
  <FlowContent
56
52
  flows={flows}
57
53
  entryDates={entryDates}
@@ -1,9 +1,10 @@
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 { notFound } from 'next/navigation';
5
+ import { t, tWith, resolveLocale } from '@/lib/i18n';
6
6
  import FlowContent from '@/components/FlowContent';
7
+ import FlowHubTabs from '@/components/FlowHubTabs';
7
8
 
8
9
  const PAGE_SIZE = siteConfig.pagination.flows;
9
10
 
@@ -13,6 +14,7 @@ export const metadata: Metadata = {
13
14
  };
14
15
 
15
16
  export default function FlowsPage() {
17
+ if (siteConfig.features?.flow?.enabled === false) notFound();
16
18
  const allFlows = getAllFlows();
17
19
  const totalPages = Math.ceil(allFlows.length / PAGE_SIZE);
18
20
  const flows = allFlows.slice(0, PAGE_SIZE);
@@ -21,12 +23,7 @@ export default function FlowsPage() {
21
23
 
22
24
  return (
23
25
  <div className="layout-main">
24
- <PageHeader
25
- titleKey="flow"
26
- subtitleKey="flow_subtitle"
27
- subtitleParams={{ count: allFlows.length }}
28
- />
29
-
26
+ <FlowHubTabs subtitle={tWith('flow_subtitle', { count: allFlows.length })} />
30
27
  <FlowContent
31
28
  flows={flows}
32
29
  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,21 @@
1
+ import { Metadata } from 'next';
2
+ import { notFound } from 'next/navigation';
3
+ import { t, resolveLocale } from '@/lib/i18n';
4
+ import { siteConfig } from '../../../site.config';
5
+ import FlowHubTabs from '@/components/FlowHubTabs';
6
+ import KnowledgeGraph from '@/components/KnowledgeGraph';
7
+
8
+ export const metadata: Metadata = {
9
+ title: `${t('tab_graph')} | ${resolveLocale(siteConfig.title)}`,
10
+ description: t('graph_subtitle'),
11
+ };
12
+
13
+ export default function GraphPage() {
14
+ if (siteConfig.features?.flow?.enabled === false) notFound();
15
+ return (
16
+ <div className="layout-main">
17
+ <FlowHubTabs subtitle={t('graph_subtitle')} />
18
+ <KnowledgeGraph />
19
+ </div>
20
+ );
21
+ }
@@ -3,6 +3,7 @@ import localFont from "next/font/local";
3
3
  import Navbar from "@/components/Navbar";
4
4
  import Footer from "@/components/Footer";
5
5
  import Analytics from "@/components/Analytics";
6
+ import BrowserDetectionBanner from "@/components/BrowserDetectionBanner";
6
7
  import { siteConfig } from "../../site.config";
7
8
  import { ThemeProvider } from "@/components/ThemeProvider";
8
9
  import { LanguageProvider } from "@/components/LanguageProvider";
@@ -89,7 +90,7 @@ export default function RootLayout({
89
90
  const seriesKeys = Object.keys(allSeries).sort();
90
91
  const filteredKeys = featuredSeries && featuredSeries.length > 0
91
92
  ? seriesKeys.filter(slug => featuredSeries.includes(slug))
92
- : seriesKeys.slice(0, 5);
93
+ : [];
93
94
  seriesList = filteredKeys.map(slug => ({
94
95
  name: getSeriesData(slug)?.title || allSeries[slug][0]?.series || slug,
95
96
  slug,
@@ -106,7 +107,7 @@ export default function RootLayout({
106
107
  ? allBooks
107
108
  .filter(book => featuredBookSlugs.includes(book.slug))
108
109
  .map(book => ({ name: book.title, slug: book.slug }))
109
- : allBooks.slice(0, 5).map(book => ({ name: book.title, slug: book.slug }));
110
+ : [];
110
111
  }
111
112
 
112
113
  return (
@@ -128,6 +129,7 @@ export default function RootLayout({
128
129
  <div className="selection:bg-accent/20 selection:text-accent dark:selection:bg-accent/30 dark:selection:text-accent min-h-screen flex flex-col">
129
130
  <Navbar seriesList={seriesList} booksList={booksList} />
130
131
  <main id="main-content" className="pt-16 flex-grow">
132
+ <BrowserDetectionBanner updateUrl={siteConfig.browserCheck?.updateUrl} />
131
133
  {children}
132
134
  </main>
133
135
  <Footer />
@@ -0,0 +1,129 @@
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
+ if (siteConfig.features?.flow?.enabled === false) return [{ slug: '_' }];
13
+ const notes = getAllNotes();
14
+ if (notes.length === 0) return [{ slug: '_' }];
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
+ if (siteConfig.features?.flow?.enabled === false) notFound();
39
+ const { slug } = await params;
40
+ const note = getNoteBySlug(slug);
41
+ if (!note) notFound();
42
+
43
+ const slugRegistry = buildSlugRegistry();
44
+ const backlinks = getBacklinks(slug);
45
+ const { prev, next } = getAdjacentNotes(slug);
46
+
47
+ const showToc = note.toc !== false && note.headings.length > 0;
48
+ const visibleBacklinks = note.backlinks !== false ? backlinks : [];
49
+ const showSidebar = showToc || visibleBacklinks.length > 0;
50
+
51
+ const breadcrumb = (
52
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-sm text-muted">
53
+ <Link href="/notes" className="hover:text-accent no-underline">
54
+ {t('notes')}
55
+ </Link>
56
+ <span className="text-muted/40" aria-hidden="true">›</span>
57
+ <span className="text-foreground truncate">{note.title}</span>
58
+ </nav>
59
+ );
60
+
61
+ return (
62
+ <div className="layout-main">
63
+ {!showSidebar && <div className="mb-6">{breadcrumb}</div>}
64
+
65
+ <div className={showSidebar
66
+ ? 'grid grid-cols-1 lg:grid-cols-[280px_minmax(0,1fr)] gap-8 items-start'
67
+ : 'max-w-3xl mx-auto'
68
+ }>
69
+ {showSidebar && (
70
+ <NoteSidebar
71
+ headings={note.headings}
72
+ showToc={showToc}
73
+ backlinks={visibleBacklinks}
74
+ breadcrumb={breadcrumb}
75
+ />
76
+ )}
77
+ <article className="min-w-0 max-w-3xl mx-auto">
78
+ <header className="mb-8 border-b border-muted/10 pb-8">
79
+ {note.draft && (
80
+ <div className="mb-4">
81
+ <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">
82
+ DRAFT
83
+ </span>
84
+ </div>
85
+ )}
86
+ <time className="text-sm font-mono text-accent" data-pagefind-meta="date[content]">
87
+ {note.date}
88
+ </time>
89
+ <h1 className="mt-2 text-3xl md:text-4xl font-serif font-bold text-heading leading-tight">
90
+ {note.title}
91
+ </h1>
92
+ {note.tags.length > 0 && (
93
+ <div className="flex flex-wrap gap-2 mt-4">
94
+ {note.tags.map(tag => (
95
+ <Tag key={tag} tag={tag} variant="default" />
96
+ ))}
97
+ </div>
98
+ )}
99
+ </header>
100
+
101
+ <MarkdownRenderer content={note.content} slug={note.slug} slugRegistry={slugRegistry} />
102
+
103
+ {/* Prev/Next navigation */}
104
+ <nav aria-label="Note navigation" className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-2 gap-4">
105
+ {prev ? (
106
+ <Link href={`/notes/${prev.slug}`} className="group text-left no-underline">
107
+ <span className="text-xs text-muted">{t('older')}</span>
108
+ <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
109
+ {prev.title}
110
+ </div>
111
+ <span className="text-xs font-mono text-muted">{prev.date}</span>
112
+ </Link>
113
+ ) : <div />}
114
+ {next ? (
115
+ <Link href={`/notes/${next.slug}`} className="group text-right no-underline">
116
+ <span className="text-xs text-muted">{t('newer')}</span>
117
+ <div className="text-sm font-medium text-heading group-hover:text-accent transition-colors truncate">
118
+ {next.title}
119
+ </div>
120
+ <span className="text-xs font-mono text-muted">{next.date}</span>
121
+ </Link>
122
+ ) : <div />}
123
+ </nav>
124
+ </article>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
@@ -0,0 +1,60 @@
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
+ if (siteConfig.features?.flow?.enabled === false) return [{ page: '2' }];
14
+ const allNotes = getAllNotes();
15
+ const totalPages = Math.ceil(allNotes.length / PAGE_SIZE);
16
+ const pageCount = Math.max(1, totalPages - 1);
17
+ return Array.from({ length: pageCount }, (_, i) => ({
18
+ page: (i + 2).toString(),
19
+ }));
20
+ }
21
+
22
+ export const dynamicParams = false;
23
+
24
+ export async function generateMetadata({ params }: { params: Promise<{ page: string }> }): Promise<Metadata> {
25
+ const { page } = await params;
26
+ return {
27
+ title: `${t('notes')} - ${page} | ${resolveLocale(siteConfig.title)}`,
28
+ };
29
+ }
30
+
31
+ export default async function NotesPaginatedPage({ params }: { params: Promise<{ page: string }> }) {
32
+ if (siteConfig.features?.flow?.enabled === false) notFound();
33
+ const { page: pageStr } = await params;
34
+ const page = parseInt(pageStr, 10);
35
+ const allNotes = getAllNotes();
36
+ const totalPages = Math.ceil(allNotes.length / PAGE_SIZE);
37
+
38
+ if (page > totalPages) notFound();
39
+
40
+ const tags = getNoteTags();
41
+ const start = (page - 1) * PAGE_SIZE;
42
+ const notes = allNotes.slice(start, start + PAGE_SIZE);
43
+
44
+ return (
45
+ <div className="layout-main">
46
+ <PageHeader
47
+ titleKey="notes"
48
+ subtitleKey="page_of_total"
49
+ subtitleParams={{ page, total: totalPages }}
50
+ className="mb-12"
51
+ />
52
+ <FlowHubTabs />
53
+ <NoteContent
54
+ notes={notes}
55
+ tags={tags}
56
+ pagination={{ currentPage: page, totalPages, basePath: '/notes' }}
57
+ />
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,33 @@
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, tWith, resolveLocale } from '@/lib/i18n';
6
+ import NoteContent from '@/components/NoteContent';
7
+ import FlowHubTabs from '@/components/FlowHubTabs';
8
+
9
+ const PAGE_SIZE = siteConfig.pagination.notes ?? 20;
10
+
11
+ export const metadata: Metadata = {
12
+ title: `${t('notes')} | ${resolveLocale(siteConfig.title)}`,
13
+ description: 'Knowledge base notes.',
14
+ };
15
+
16
+ export default function NotesPage() {
17
+ if (siteConfig.features?.flow?.enabled === false) notFound();
18
+ const allNotes = getAllNotes();
19
+ const totalPages = Math.ceil(allNotes.length / PAGE_SIZE);
20
+ const notes = allNotes.slice(0, PAGE_SIZE);
21
+ const tags = getNoteTags();
22
+
23
+ return (
24
+ <div className="layout-main">
25
+ <FlowHubTabs subtitle={tWith('notes_subtitle', { count: allNotes.length })} />
26
+ <NoteContent
27
+ notes={notes}
28
+ tags={tags}
29
+ pagination={totalPages > 1 ? { currentPage: 1, totalPages, basePath: '/notes' } : undefined}
30
+ />
31
+ </div>
32
+ );
33
+ }
@@ -15,6 +15,7 @@ export async function generateStaticParams() {
15
15
  for (let i = 2; i <= totalPages; i++) {
16
16
  params.push({ page: i.toString() });
17
17
  }
18
+ if (params.length === 0) return [{ page: '2' }];
18
19
  return params;
19
20
  }
20
21
 
package/src/app/page.tsx CHANGED
@@ -52,7 +52,7 @@ export default function Home() {
52
52
  // Load data only for sections that are both enabled on homepage and globally
53
53
  const allSeries = has('featured-series') && features?.series?.enabled !== false ? getFeaturedSeries() : {};
54
54
  const featuredBooks = has('featured-books') && features?.books?.enabled !== false ? getFeaturedBooks() : [];
55
- const recentFlows = has('recent-flows') && features?.flows?.enabled !== false
55
+ const recentFlows = has('recent-flows') && features?.flow?.enabled !== false
56
56
  ? getRecentFlows(recentFlowsMax)
57
57
  : [];
58
58
  const needsPosts = has('featured-posts') || has('latest-posts');
@@ -87,15 +87,14 @@ export default function Home() {
87
87
  coverImage: b.coverImage,
88
88
  authors: b.authors,
89
89
  chapterCount: b.chapters.length,
90
- firstChapter: b.chapters[0]?.file,
90
+ firstChapter: b.chapters[0]?.id,
91
91
  }))
92
92
  : [];
93
93
 
94
- const recentNoteItems: RecentNoteItem[] = has('recent-flows') && features?.flows?.enabled !== false
94
+ const recentNoteItems: RecentNoteItem[] = has('recent-flows') && features?.flow?.enabled !== false
95
95
  ? recentFlows.map(f => ({
96
96
  slug: f.slug,
97
97
  date: f.date,
98
- title: f.title,
99
98
  excerpt: f.excerpt,
100
99
  }))
101
100
  : [];
@@ -148,7 +147,7 @@ export default function Home() {
148
147
  if (features?.posts?.enabled === false) return null;
149
148
  return <LatestWritingSection key="latest-posts" posts={posts} totalCount={allPosts.length} />;
150
149
  case 'recent-flows':
151
- if (features?.flows?.enabled === false) return null;
150
+ if (features?.flow?.enabled === false) return null;
152
151
  return <RecentNotesSection key="recent-flows" notes={recentNoteItems} />;
153
152
  default:
154
153
  return null;
@@ -1,4 +1,4 @@
1
- import { getPostBySlug, getAllPosts, getRelatedPosts, getSeriesPosts, getSeriesData, getAdjacentPosts, PostData } from '@/lib/markdown';
1
+ import { getPostBySlug, getAllPosts, getRelatedPosts, getSeriesPosts, getSeriesData, getAdjacentPosts, buildSlugRegistry, getBacklinks, PostData } from '@/lib/markdown';
2
2
  import { notFound } from 'next/navigation';
3
3
  import PostLayout from '@/layouts/PostLayout';
4
4
  import SimpleLayout from '@/layouts/SimpleLayout';
@@ -30,6 +30,7 @@ function resolvePostFromParam(rawSlug: string) {
30
30
  */
31
31
  export async function generateStaticParams() {
32
32
  const posts = getAllPosts();
33
+ if (posts.length === 0) return [{ slug: '_' }];
33
34
  return posts.map((post) => ({ slug: post.slug }));
34
35
  }
35
36
 
@@ -99,6 +100,8 @@ export default async function PostPage({
99
100
 
100
101
  const relatedPosts = getRelatedPosts(slug);
101
102
  const { prev, next } = getAdjacentPosts(slug);
103
+ const slugRegistry = buildSlugRegistry();
104
+ const backlinks = getBacklinks(slug);
102
105
  let seriesPosts: PostData[] = [];
103
106
  let seriesTitle: string | undefined;
104
107
 
@@ -109,5 +112,5 @@ export default async function PostPage({
109
112
  }
110
113
 
111
114
  // Default to standard post layout
112
- return <PostLayout post={post} relatedPosts={relatedPosts} seriesPosts={seriesPosts} seriesTitle={seriesTitle} prevPost={prev} nextPost={next} />;
115
+ return <PostLayout post={post} relatedPosts={relatedPosts} seriesPosts={seriesPosts} seriesTitle={seriesTitle} prevPost={prev} nextPost={next} backlinks={backlinks} slugRegistry={slugRegistry} />;
113
116
  }
@@ -3,6 +3,7 @@ import PostList from '@/components/PostList';
3
3
  import Pagination from '@/components/Pagination';
4
4
  import { siteConfig } from '../../../../../site.config';
5
5
  import { Metadata } from 'next';
6
+ import { notFound } from 'next/navigation';
6
7
  import { t, resolveLocale } from '@/lib/i18n';
7
8
  import PageHeader from '@/components/PageHeader';
8
9
 
@@ -13,7 +14,7 @@ export function generateStaticParams() {
13
14
  const totalPages = Math.ceil(allPosts.length / PAGE_SIZE);
14
15
 
15
16
  // Generate params for page 2 to totalPages (page 1 is handled by /posts/page.tsx)
16
- if (totalPages <= 1) return [];
17
+ if (totalPages <= 1) return [{ page: '2' }];
17
18
 
18
19
  return Array.from({ length: totalPages - 1 }, (_, i) => ({
19
20
  page: (i + 2).toString(),
@@ -35,6 +36,8 @@ export default async function PostsPage({ params }: { params: Promise<{ page: st
35
36
  const allPosts = getAllPosts();
36
37
  const totalPages = Math.ceil(allPosts.length / PAGE_SIZE);
37
38
 
39
+ if (isNaN(page) || page < 2 || page > totalPages) notFound();
40
+
38
41
  const start = (page - 1) * PAGE_SIZE;
39
42
  const end = start + PAGE_SIZE;
40
43
  const posts = allPosts.slice(start, end);
@@ -1,4 +1,4 @@
1
- import { getAllPosts, getAllBooks, getBookChapter, getAllFlows } from '@/lib/markdown';
1
+ import { getAllPosts, getAllBooks, getBookChapter, getAllFlows, getAllNotes } from '@/lib/markdown';
2
2
  import { stripMarkdown } from '@/lib/search-utils';
3
3
 
4
4
  export const dynamic = 'force-static';
@@ -20,11 +20,11 @@ export async function GET() {
20
20
  const books = getAllBooks();
21
21
  for (const book of books) {
22
22
  for (const ch of book.chapters) {
23
- const chapter = getBookChapter(book.slug, ch.file);
23
+ const chapter = getBookChapter(book.slug, ch.id);
24
24
  if (chapter) {
25
25
  searchIndex.push({
26
26
  title: `${chapter.title} — ${book.title}`,
27
- slug: `books/${book.slug}/${ch.file}`,
27
+ slug: `books/${book.slug}/${ch.id}`,
28
28
  date: book.date,
29
29
  excerpt: chapter.excerpt || '',
30
30
  category: 'Book',
@@ -49,5 +49,19 @@ export async function GET() {
49
49
  });
50
50
  }
51
51
 
52
+ // Add notes to search index
53
+ const notes = getAllNotes();
54
+ for (const note of notes) {
55
+ searchIndex.push({
56
+ title: note.title,
57
+ slug: `notes/${note.slug}`,
58
+ date: note.date,
59
+ excerpt: note.excerpt,
60
+ category: 'Note',
61
+ tags: note.tags,
62
+ content: stripMarkdown(note.content),
63
+ });
64
+ }
65
+
52
66
  return Response.json(searchIndex);
53
67
  }
@@ -23,6 +23,7 @@ export async function generateStaticParams() {
23
23
  }
24
24
  }
25
25
  });
26
+ if (params.length === 0) return [{ slug: '_', page: '2' }];
26
27
  return params;
27
28
  }
28
29
 
@@ -12,9 +12,9 @@ const PAGE_SIZE = siteConfig.pagination.series;
12
12
 
13
13
  export async function generateStaticParams() {
14
14
  const allSeries = getAllSeries();
15
- return Object.keys(allSeries).map((slug) => ({
16
- slug,
17
- }));
15
+ const slugs = Object.keys(allSeries);
16
+ if (slugs.length === 0) return [{ slug: '_' }];
17
+ return slugs.map((slug) => ({ slug }));
18
18
  }
19
19
 
20
20
  export const dynamicParams = false;
@@ -34,7 +34,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
34
34
  priority: 0.8,
35
35
  },
36
36
  ...book.chapters.map((ch) => ({
37
- url: `${baseUrl}/books/${book.slug}/${ch.file}`,
37
+ url: `${baseUrl}/books/${book.slug}/${ch.id}`,
38
38
  lastModified: book.date,
39
39
  changeFrequency: 'monthly' as const,
40
40
  priority: 0.7,
@@ -9,9 +9,9 @@ import TagContentTabs from '@/components/TagContentTabs';
9
9
 
10
10
  export async function generateStaticParams() {
11
11
  const tags = getAllTags();
12
- return Object.keys(tags).map((tag) => ({
13
- tag,
14
- }));
12
+ const tagKeys = Object.keys(tags);
13
+ if (tagKeys.length === 0) return [{ tag: '_' }];
14
+ return tagKeys.map((tag) => ({ tag }));
15
15
  }
16
16
 
17
17
  export const dynamicParams = false;
@@ -0,0 +1,39 @@
1
+ import Link from 'next/link';
2
+ import { BacklinkSource } from '@/lib/markdown';
3
+ import { t } from '@/lib/i18n';
4
+
5
+ interface BacklinksProps {
6
+ backlinks: BacklinkSource[];
7
+ }
8
+
9
+ export default function Backlinks({ backlinks }: BacklinksProps) {
10
+ if (!backlinks.length) return null;
11
+
12
+ return (
13
+ <div className="mt-12 pt-12 border-t border-muted/20">
14
+ <h3 className="text-sm font-sans font-semibold uppercase tracking-widest text-muted mb-4">
15
+ {t('backlinks')}
16
+ </h3>
17
+ <div className="flex flex-col gap-4">
18
+ {backlinks.map(bl => (
19
+ <div key={`${bl.type}-${bl.slug}`} className="flex flex-col gap-1">
20
+ <div className="flex items-center gap-2">
21
+ <span className="text-[10px] font-sans font-bold uppercase tracking-widest text-muted/60 border border-muted/20 rounded px-1.5 py-0.5">
22
+ {bl.type}
23
+ </span>
24
+ <Link
25
+ href={bl.url}
26
+ className="text-sm font-medium text-heading hover:text-accent no-underline transition-colors"
27
+ >
28
+ {bl.title}
29
+ </Link>
30
+ </div>
31
+ {bl.context && (
32
+ <p className="text-sm text-muted leading-relaxed">&ldquo;{bl.context}&rdquo;</p>
33
+ )}
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ );
39
+ }