@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.
- package/.github/workflows/ci.yml +1 -1
- package/CHANGELOG.md +63 -0
- package/CLAUDE.md +9 -18
- package/GEMINI.md +6 -0
- package/README.md +44 -0
- package/TODO.md +15 -3
- package/bun.lock +5 -3
- package/content/about.mdx +64 -10
- package/content/about.zh.mdx +66 -9
- package/content/books/sample-book/index.mdx +3 -3
- 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 +0 -1
- 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/series/digital-garden/01-philosophy.mdx +25 -12
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONTRIBUTING.md +26 -0
- package/docs/DIGITAL_GARDEN.md +72 -0
- package/imports/README.md +45 -0
- package/package.json +12 -5
- package/scripts/generate-knowledge-graph.ts +162 -0
- package/scripts/import-book.ts +176 -0
- package/scripts/new-flow-from-chat.ts +238 -0
- package/scripts/new-flow.ts +0 -5
- package/scripts/new-note.ts +53 -0
- package/scripts/sync-book-chapters.ts +210 -0
- package/site.config.ts +30 -7
- package/src/app/authors/[author]/page.tsx +3 -1
- package/src/app/books/[slug]/[chapter]/page.tsx +2 -1
- package/src/app/books/[slug]/page.tsx +6 -5
- package/src/app/flows/[year]/[month]/[day]/page.tsx +35 -29
- package/src/app/flows/[year]/[month]/page.tsx +18 -13
- package/src/app/flows/[year]/page.tsx +25 -15
- package/src/app/flows/page/[page]/page.tsx +5 -9
- package/src/app/flows/page.tsx +5 -8
- package/src/app/globals.css +41 -0
- package/src/app/graph/page.tsx +21 -0
- package/src/app/layout.tsx +4 -2
- package/src/app/notes/[slug]/page.tsx +129 -0
- package/src/app/notes/page/[page]/page.tsx +60 -0
- package/src/app/notes/page.tsx +33 -0
- package/src/app/page/[page]/page.tsx +1 -0
- package/src/app/page.tsx +4 -5
- package/src/app/posts/[slug]/page.tsx +5 -2
- package/src/app/posts/page/[page]/page.tsx +4 -1
- package/src/app/search.json/route.ts +17 -3
- package/src/app/series/[slug]/page/[page]/page.tsx +1 -0
- package/src/app/series/[slug]/page.tsx +3 -3
- package/src/app/sitemap.ts +1 -1
- package/src/app/tags/[tag]/page.tsx +3 -3
- package/src/components/Backlinks.tsx +39 -0
- package/src/components/BookMobileNav.tsx +11 -11
- package/src/components/BookSidebar.tsx +17 -25
- package/src/components/BrowserDetectionBanner.tsx +96 -0
- package/src/components/FeaturedStoriesSection.tsx +1 -1
- 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/KnowledgeGraph.tsx +324 -0
- package/src/components/LanguageProvider.tsx +14 -5
- package/src/components/MarkdownRenderer.tsx +13 -2
- package/src/components/Navbar.tsx +237 -10
- package/src/components/NoteContent.tsx +123 -0
- package/src/components/NoteSidebar.tsx +132 -0
- package/src/components/RecentNotesSection.tsx +6 -11
- package/src/components/Search.tsx +7 -3
- package/src/components/TagContentTabs.tsx +0 -1
- package/src/i18n/translations.ts +43 -17
- package/src/layouts/BookLayout.tsx +3 -3
- package/src/layouts/PostLayout.tsx +8 -3
- package/src/lib/i18n.ts +83 -6
- package/src/lib/markdown.ts +306 -19
- package/src/lib/remark-wikilinks.ts +59 -0
- package/src/lib/search-utils.ts +2 -1
- package/tests/unit/static-params.test.ts +238 -0
- 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
|
-
<
|
|
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}
|
package/src/app/flows/page.tsx
CHANGED
|
@@ -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 {
|
|
5
|
-
import
|
|
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
|
-
<
|
|
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}
|
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,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
|
+
}
|
package/src/app/layout.tsx
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
:
|
|
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
|
+
}
|
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?.
|
|
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]?.
|
|
90
|
+
firstChapter: b.chapters[0]?.id,
|
|
91
91
|
}))
|
|
92
92
|
: [];
|
|
93
93
|
|
|
94
|
-
const recentNoteItems: RecentNoteItem[] = has('recent-flows') && features?.
|
|
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?.
|
|
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.
|
|
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.
|
|
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
|
}
|
|
@@ -12,9 +12,9 @@ const PAGE_SIZE = siteConfig.pagination.series;
|
|
|
12
12
|
|
|
13
13
|
export async function generateStaticParams() {
|
|
14
14
|
const allSeries = getAllSeries();
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
package/src/app/sitemap.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
13
|
-
|
|
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">“{bl.context}”</p>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|