@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.
- package/CHANGELOG.md +49 -0
- package/GEMINI.md +12 -2
- package/README.md +14 -0
- package/TODO.md +24 -16
- package/bun.lock +8 -3
- package/content/about.mdx +1 -0
- package/content/about.zh.mdx +21 -0
- package/content/flows/2026/02/05.md +0 -1
- package/content/flows/2026/02/10.mdx +2 -1
- package/content/flows/2026/02/15.md +2 -1
- package/content/flows/2026/02/18.mdx +2 -1
- package/content/flows/2026/02/20.md +15 -0
- package/content/links.mdx +42 -0
- package/content/links.zh.mdx +41 -0
- package/content/notes/algorithms-and-data-structures.mdx +51 -0
- package/content/notes/digital-garden-philosophy.mdx +36 -0
- package/content/notes/react-server-components.mdx +49 -0
- package/content/notes/tailwind-v4.mdx +45 -0
- package/content/notes/zettelkasten-method.mdx +33 -0
- package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
- package/content/posts/multimedia-showcase/index.mdx +261 -0
- package/content/privacy.mdx +32 -0
- package/content/privacy.zh.mdx +32 -0
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/DIGITAL_GARDEN.md +64 -0
- package/package.json +8 -3
- package/scripts/copy-assets.ts +1 -1
- package/scripts/generate-knowledge-graph.ts +162 -0
- package/scripts/new-flow.ts +0 -5
- package/scripts/new-note.ts +53 -0
- package/site.config.ts +146 -44
- package/src/app/[slug]/page.tsx +0 -10
- package/src/app/archive/page.tsx +38 -10
- package/src/app/books/[slug]/page.tsx +18 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +51 -31
- package/src/app/flows/[year]/[month]/page.tsx +15 -13
- package/src/app/flows/[year]/page.tsx +22 -15
- package/src/app/flows/page/[page]/page.tsx +3 -9
- package/src/app/flows/page.tsx +3 -8
- package/src/app/globals.css +41 -0
- package/src/app/graph/page.tsx +19 -0
- package/src/app/layout.tsx +47 -21
- package/src/app/notes/[slug]/page.tsx +128 -0
- package/src/app/notes/page/[page]/page.tsx +58 -0
- package/src/app/notes/page.tsx +31 -0
- package/src/app/page.tsx +134 -72
- package/src/app/posts/[slug]/page.tsx +8 -12
- package/src/app/search.json/route.ts +15 -1
- package/src/app/series/[slug]/page.tsx +18 -0
- package/src/app/subscribe/page.tsx +17 -0
- package/src/app/tags/[tag]/page.tsx +9 -26
- package/src/app/tags/page.tsx +3 -8
- package/src/components/AuthorCard.tsx +43 -0
- package/src/components/Backlinks.tsx +39 -0
- package/src/components/Comments.tsx +20 -4
- package/src/components/ExternalLinks.tsx +6 -2
- package/src/components/FlowCalendarSidebar.tsx +4 -2
- package/src/components/FlowContent.tsx +4 -3
- package/src/components/FlowHubTabs.tsx +50 -0
- package/src/components/FlowTimelineEntry.tsx +7 -9
- package/src/components/Footer.tsx +35 -26
- package/src/components/KnowledgeGraph.tsx +324 -0
- package/src/components/LanguageProvider.tsx +0 -5
- package/src/components/LanguageSwitch.tsx +117 -6
- package/src/components/LocaleSwitch.tsx +33 -0
- package/src/components/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +266 -17
- package/src/components/NoteContent.tsx +123 -0
- package/src/components/NoteSidebar.tsx +132 -0
- package/src/components/PostNavigation.tsx +55 -0
- package/src/components/PostSidebar.tsx +172 -126
- package/src/components/ReadingProgressBar.tsx +6 -21
- package/src/components/RecentNotesSection.tsx +6 -11
- package/src/components/RelatedPosts.tsx +1 -1
- package/src/components/Search.tsx +29 -5
- package/src/components/SelectedBooksSection.tsx +12 -6
- package/src/components/ShareBar.tsx +115 -0
- package/src/components/SimpleLayoutHeader.tsx +5 -14
- package/src/components/SubscribePage.tsx +298 -0
- package/src/components/TagContentTabs.tsx +102 -0
- package/src/components/TagPageHeader.tsx +7 -13
- package/src/components/TagSidebar.tsx +142 -0
- package/src/components/TagsIndexClient.tsx +156 -0
- package/src/hooks/useScrollY.ts +41 -0
- package/src/i18n/translations.ts +105 -1
- package/src/layouts/PostLayout.tsx +40 -8
- package/src/layouts/SimpleLayout.tsx +53 -15
- package/src/lib/markdown.ts +347 -18
- package/src/lib/remark-wikilinks.ts +59 -0
- package/src/lib/search-utils.ts +2 -1
- 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-
|
|
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-
|
|
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">
|
|
82
|
-
<div className="text-sm font-
|
|
83
|
-
{prev.
|
|
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">
|
|
94
|
-
<div className="text-sm font-
|
|
95
|
-
{next.
|
|
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
|
-
|
|
48
|
-
<div className="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
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/
|
|
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-
|
|
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
|
-
<
|
|
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}
|
package/src/app/flows/page.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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}
|
package/src/app/globals.css
CHANGED
|
@@ -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
|
+
}
|
package/src/app/layout.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
79
|
-
const featuredBookSlugs =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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=
|
|
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
|
+
}
|