@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "React Server Components"
|
|
3
|
+
date: "2026-02-10"
|
|
4
|
+
tags: ["react", "nextjs", "architecture"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
React Server Components (RSC) represent the most significant architectural shift in React since hooks. The mental model: components that run only on the server, produce static output, and ship *zero* JavaScript to the client.
|
|
8
|
+
|
|
9
|
+
## The Key Insight
|
|
10
|
+
|
|
11
|
+
Before RSC, React components were always client-side primitives — even when rendered on the server via SSR, their JavaScript was re-hydrated in the browser. RSC breaks this assumption. A server component:
|
|
12
|
+
|
|
13
|
+
- Can directly `await` database queries, file reads, or API calls
|
|
14
|
+
- Never re-renders on the client (no hydration cost)
|
|
15
|
+
- Can import server-only packages without any client bundle impact
|
|
16
|
+
|
|
17
|
+
## Composition Model
|
|
18
|
+
|
|
19
|
+
The power is in the *composition*: you can freely mix server and client components in the same tree. A server component can render a client component; a client component can receive server-rendered children as props.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Server component — runs only on server
|
|
23
|
+
async function ArticlePage({ slug }: { slug: string }) {
|
|
24
|
+
const post = await getPostBySlug(slug); // direct DB/file access
|
|
25
|
+
return (
|
|
26
|
+
<article>
|
|
27
|
+
<h1>{post.title}</h1>
|
|
28
|
+
<LikeButton postId={post.id} /> {/* client component */}
|
|
29
|
+
<MarkdownContent content={post.body} /> {/* server component */}
|
|
30
|
+
</article>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The "use client" Boundary
|
|
36
|
+
|
|
37
|
+
`"use client"` marks the boundary where server-rendered output hands off to client-side JavaScript. It doesn't mean "client-only" — server-rendered HTML still crosses the boundary; only interactivity (event handlers, state) requires the client bundle.
|
|
38
|
+
|
|
39
|
+
Common pitfall: passing non-serializable values (like a `Map` or class instance) through the boundary. These can't be serialized to JSON and will cause a runtime error.
|
|
40
|
+
|
|
41
|
+
## Relation to Hooks
|
|
42
|
+
|
|
43
|
+
RSC doesn't replace React hooks — hooks remain the right tool for client-side state and effects. The shift is that *data fetching* moves to the server, while *interactivity* stays with hooks on the client.
|
|
44
|
+
|
|
45
|
+
The practical result: far less `useEffect` for data loading, simpler components, better performance.
|
|
46
|
+
|
|
47
|
+
## In Next.js App Router
|
|
48
|
+
|
|
49
|
+
Next.js App Router makes RSC the default. Every component in `app/` is a server component unless it declares `"use client"`. This site's data layer (`src/lib/markdown.ts`) runs exclusively on the server — no API routes needed.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Tailwind CSS v4"
|
|
3
|
+
date: "2026-02-12"
|
|
4
|
+
tags: ["css", "tailwind", "frontend"]
|
|
5
|
+
aliases: ["tailwind-css-v4"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Tailwind CSS v4 is a complete rewrite of the framework, shifting from a JavaScript-based configuration model to a CSS-first approach. The migration story is smoother than expected, and the performance improvements are significant.
|
|
9
|
+
|
|
10
|
+
## CSS-First Configuration
|
|
11
|
+
|
|
12
|
+
The biggest change: configuration moves from `tailwind.config.js` into your CSS file using the `@theme` directive.
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
@import "tailwindcss";
|
|
16
|
+
|
|
17
|
+
@theme {
|
|
18
|
+
--color-accent: oklch(60% 0.2 250);
|
|
19
|
+
--font-serif: "Georgia", serif;
|
|
20
|
+
--spacing-18: 4.5rem;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This means your design tokens live in CSS — where they belong. You can inspect them in DevTools, use them in arbitrary CSS, and they compose naturally with CSS custom properties.
|
|
25
|
+
|
|
26
|
+
## The New Engine
|
|
27
|
+
|
|
28
|
+
v4's engine is built in Rust (via Lightning CSS) and is dramatically faster. Cold starts drop from seconds to milliseconds. The dev server no longer needs to scan your content for class names — it intercepts class usage at the PostCSS level.
|
|
29
|
+
|
|
30
|
+
## Breaking Changes
|
|
31
|
+
|
|
32
|
+
- No more `tailwind.config.js` for most use cases (it still exists for plugins)
|
|
33
|
+
- Default ring width changed from 3px to 1px
|
|
34
|
+
- Standalone opacity utilities removed — use slash syntax instead (e.g., `bg-black/50`)
|
|
35
|
+
- The `!important` modifier moves to the end of the class name (e.g., `shadow-md!` not `!shadow-md`)
|
|
36
|
+
- `@apply` works differently for custom utilities
|
|
37
|
+
- Dark mode config moves to CSS: `@custom-variant dark (&:where(.dark, .dark *))`
|
|
38
|
+
|
|
39
|
+
## Integration with React Server Components
|
|
40
|
+
|
|
41
|
+
Tailwind v4 pairs well with [[react-server-components|React Server Components]]. Static CSS generation means zero runtime overhead — the right tool for a server-rendered architecture. The class scanning approach would have conflicted with server components anyway (no runtime class generation).
|
|
42
|
+
|
|
43
|
+
## This Site
|
|
44
|
+
|
|
45
|
+
This garden uses Tailwind v4 with a custom theme defined entirely in `src/app/globals.css`. Theme colors use CSS custom properties so the light/dark toggle works without JavaScript class manipulation.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Zettelkasten Method"
|
|
3
|
+
date: "2026-02-01"
|
|
4
|
+
tags: ["zettelkasten", "note-taking", "knowledge-management"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The Zettelkasten ("slip-box") method is a personal knowledge management system developed by sociologist Niklas Luhmann. He produced over 90 books and hundreds of articles, crediting much of his productivity to this system.
|
|
8
|
+
|
|
9
|
+
## Core Principles
|
|
10
|
+
|
|
11
|
+
**Atomic notes** — each note captures exactly one idea. If you need to say two things, write two notes. This constraint forces clarity and creates more connection points.
|
|
12
|
+
|
|
13
|
+
**Linking over filing** — instead of organizing notes into folders or categories, you link them to related ideas. The structure that emerges is a network, not a tree.
|
|
14
|
+
|
|
15
|
+
**Your own words** — always paraphrase rather than copy. The act of rewriting forces understanding and makes the note genuinely yours.
|
|
16
|
+
|
|
17
|
+
**Permanent notes** — unlike fleeting notes (quick captures) or literature notes (summaries of sources), permanent notes are written to last. They connect to existing knowledge and stand alone without context.
|
|
18
|
+
|
|
19
|
+
## Why It Works
|
|
20
|
+
|
|
21
|
+
A folder system forces you to predict how you'll want to retrieve something. Linking lets you discover connections you didn't anticipate. Luhmann described his Zettelkasten as a "conversation partner" — it would surprise him with unexpected connections between distant ideas.
|
|
22
|
+
|
|
23
|
+
The compounding effect is real: the more notes you have, the more connections are possible, and the more generative each new note becomes.
|
|
24
|
+
|
|
25
|
+
## Relation to Digital Gardens
|
|
26
|
+
|
|
27
|
+
[[digital-garden-philosophy]] takes the Zettelkasten spirit and applies it to public publishing. Where Luhmann's slip-box was private, a digital garden invites readers into the thinking process — with all its incompleteness and revision.
|
|
28
|
+
|
|
29
|
+
## In This Garden
|
|
30
|
+
|
|
31
|
+
This site implements Zettelkasten-style notes with bi-directional links. Any `[[wikilink]]` you write creates a connection that shows up in the backlinks panel of the target note.
|
|
32
|
+
|
|
33
|
+
See the [[digital-garden-philosophy]] note for how this fits the broader philosophy of this garden.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "The Philosophy of Digital Gardening"
|
|
3
3
|
date: "2025-12-15"
|
|
4
|
-
excerpt: "
|
|
4
|
+
excerpt: "How ideas evolve from raw daily flows into articles, series, and books — the philosophy behind the Amytis knowledge ladder."
|
|
5
5
|
category: "Philosophy"
|
|
6
6
|
tags: ["pkm", "knowledge management", "learning"]
|
|
7
7
|
authors: ["Amytis Team"]
|
|
@@ -9,22 +9,35 @@ authors: ["Amytis Team"]
|
|
|
9
9
|
|
|
10
10
|
# The Philosophy of Digital Gardening
|
|
11
11
|
|
|
12
|
-
Unlike a traditional blog
|
|
12
|
+
Unlike a traditional blog — a reverse-chronological stream of finished "publications" — a **digital garden** is a collection of evolving ideas. Notes are never truly done. They grow, branch, and connect over time.
|
|
13
13
|
|
|
14
14
|
## Gardening vs. Architecting
|
|
15
15
|
|
|
16
|
-
- **Architecting** is about planning everything upfront.
|
|
17
|
-
- **Gardening** is about planting seeds and
|
|
16
|
+
- **Architecting** is about planning everything upfront. You design the structure, then fill it in.
|
|
17
|
+
- **Gardening** is about planting seeds and tending to what grows. The structure emerges from the content.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Most personal publishing starts as architecting: "I'll write a three-part series on X." Digital gardening inverts this — you write when you have something to say, and patterns emerge later.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
## Key Principles
|
|
22
|
+
|
|
23
|
+
**Topography over chronology.** Organize by topic and connection, not by date. The question isn't "when did I write this?" but "how does this connect to that?"
|
|
24
|
+
|
|
25
|
+
**Continuous growth.** A note is never "published and done." It is a living document — refined, expanded, and linked as your thinking evolves.
|
|
26
|
+
|
|
27
|
+
**Imperfection is allowed.** Share seedling thoughts that aren't fully formed. A rough idea made public is more valuable than a perfect idea kept private.
|
|
28
|
+
|
|
29
|
+
## From Seeds to Trees: The Knowledge Ladder
|
|
30
|
+
|
|
31
|
+
In Amytis, the digital garden philosophy maps directly to four content types:
|
|
32
|
+
|
|
33
|
+
**Flow** — The seed. Raw, unfiltered daily notes. Capture thoughts as they arrive, without worrying about form or permanence.
|
|
34
|
+
|
|
35
|
+
**Articles** — The sprout. A single idea, tended to until it's clear and shareable. One thought, fully articulated.
|
|
36
|
+
|
|
37
|
+
**Series** — The plant. Related articles gathered into a curated collection, exploring a broader theme across multiple pieces.
|
|
38
|
+
|
|
39
|
+
**Books** — The tree. Mature knowledge organized into structured volumes — the most permanent and distilled form an idea can take.
|
|
24
40
|
|
|
25
41
|
> "A garden is never finished."
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
- **Seedlings:** Rough notes and initial thoughts.
|
|
29
|
-
- **Saplings:** Growing ideas with some structure.
|
|
30
|
-
- **Evergreens:** Well-developed concepts.
|
|
43
|
+
Each stage is valid on its own. Not every flow becomes an article; not every article joins a series. The garden grows at its own pace.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -4,7 +4,7 @@ Amytis is a static site generator built with **Next.js 16 (App Router)**, design
|
|
|
4
4
|
|
|
5
5
|
## Core Stack
|
|
6
6
|
|
|
7
|
-
- **Framework:** Next.js 16.1.
|
|
7
|
+
- **Framework:** Next.js 16.1.6 (App Router) with React 19
|
|
8
8
|
- **Runtime:** Bun
|
|
9
9
|
- **Styling:** Tailwind CSS v4 with CSS variables for theming
|
|
10
10
|
- **Content:** Local `.md`/`.mdx` files parsed at build time
|
|
@@ -40,6 +40,11 @@ src/app/
|
|
|
40
40
|
page.tsx # All books overview
|
|
41
41
|
[slug]/page.tsx # Book landing page
|
|
42
42
|
[slug]/[chapter]/page.tsx # Individual book chapter
|
|
43
|
+
notes/
|
|
44
|
+
page.tsx # All notes list
|
|
45
|
+
[slug]/page.tsx # Individual note
|
|
46
|
+
graph/
|
|
47
|
+
page.tsx # Knowledge graph visualization
|
|
43
48
|
flows/
|
|
44
49
|
page.tsx # Flows stream/listing
|
|
45
50
|
[year]/[month]/[day]/page.tsx # Individual flow entry (optional if modal/stream used)
|
|
@@ -96,6 +101,9 @@ src/app/
|
|
|
96
101
|
| `getAllBooks()` | `BookData[]` | All books metadata |
|
|
97
102
|
| `getBookData(slug)` | `BookData \| null` | Single book metadata & TOC |
|
|
98
103
|
| `getBookChapter(...)` | `BookChapterData` | Single chapter content |
|
|
104
|
+
| `getAllNotes()` | `NoteData[]` | All notes, sorted by date |
|
|
105
|
+
| `getNoteBySlug(slug)` | `NoteData \| null` | Single note content |
|
|
106
|
+
| `getBacklinks(slug)` | `BacklinkSource[]` | Inbound links for a page |
|
|
99
107
|
| `getFlowBySlug(slug)` | `FlowData \| null` | Single flow entry |
|
|
100
108
|
| `getSeriesPosts(slug)` | `PostData[]` | Posts in a series |
|
|
101
109
|
| `calculateReadingTime(content)` | `string` | Estimated reading time (multilingual) |
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -55,6 +55,18 @@ Books are manually structured in `content/books/`.
|
|
|
55
55
|
2. Add `index.mdx` with metadata and chapters configuration.
|
|
56
56
|
3. Create the chapter files (`welcome.mdx`, `conclusion.mdx`) in the same folder.
|
|
57
57
|
|
|
58
|
+
### Creating Notes (Digital Garden)
|
|
59
|
+
|
|
60
|
+
Notes live in `content/notes/`.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create a new note using the dedicated script
|
|
64
|
+
bun run new-note "Zettelkasten Method"
|
|
65
|
+
|
|
66
|
+
# Or using the general new script
|
|
67
|
+
bun run new "Zettelkasten Method" --note
|
|
68
|
+
```
|
|
69
|
+
|
|
58
70
|
### Importing Content
|
|
59
71
|
|
|
60
72
|
```bash
|
|
@@ -63,6 +75,20 @@ bun run new-from-pdf doc.pdf --title "My Doc"
|
|
|
63
75
|
|
|
64
76
|
# Image folder to post
|
|
65
77
|
bun run new-from-images ./photos --title "Gallery"
|
|
78
|
+
|
|
79
|
+
# Chat logs to flows
|
|
80
|
+
bun run new-flow-from-chat
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Maintenance Tools
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Sync book chapters with files in the folder
|
|
87
|
+
bun run sync-book
|
|
88
|
+
|
|
89
|
+
# Bulk draft/publish a series
|
|
90
|
+
bun run series-draft "my-series"
|
|
91
|
+
bun run series-draft "my-series" --undraft
|
|
66
92
|
```
|
|
67
93
|
|
|
68
94
|
## Running Tests
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Digital Garden Features
|
|
2
|
+
|
|
3
|
+
Amytis includes a suite of features designed for non-linear knowledge management, inspired by the "Digital Garden" philosophy. These features allow you to create an interconnected web of thoughts rather than just a linear stream of posts.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
### 1. Notes (`/notes`)
|
|
8
|
+
Notes are atomic units of knowledge. Unlike posts, which are often time-bound articles, notes are evergreen and concept-oriented.
|
|
9
|
+
|
|
10
|
+
- **Location:** `content/notes/*.mdx`
|
|
11
|
+
- **Frontmatter:**
|
|
12
|
+
```yaml
|
|
13
|
+
---
|
|
14
|
+
title: "Zettelkasten Method"
|
|
15
|
+
tags: ["pkm", "productivity"]
|
|
16
|
+
aliases: ["zettelkasten", "slip-box"] # Alternative names for wiki-linking
|
|
17
|
+
backlinks: true # Show backlinks at the bottom (default: true)
|
|
18
|
+
---
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Wiki-links
|
|
22
|
+
You can link between any content (Posts, Notes, Flows) using double-bracket syntax: `[[Slug]]` or `[[Slug|Display Text]]`.
|
|
23
|
+
|
|
24
|
+
- **Standard Link:** `[[zettelkasten-method]]` → links to `/notes/zettelkasten-method`
|
|
25
|
+
- **Aliased Link:** `[[zettelkasten-method|The Slip Box]]` → links to same note but displays "The Slip Box"
|
|
26
|
+
- **Cross-Type Linking:**
|
|
27
|
+
- If a slug matches a Note, it links to `/notes/[slug]`
|
|
28
|
+
- If it matches a Post, it links to `/posts/[slug]`
|
|
29
|
+
- If it matches a Flow, it links to `/flows/[slug]`
|
|
30
|
+
|
|
31
|
+
### 3. Backlinks
|
|
32
|
+
At the bottom of every Note, Amytis automatically generates a "Linked References" section. This lists every other page that links *to* the current note, along with a context snippet showing how it was referenced.
|
|
33
|
+
|
|
34
|
+
### 4. Knowledge Graph
|
|
35
|
+
The `/graph` route visualizes your entire digital garden as an interactive network graph.
|
|
36
|
+
- **Nodes**: Represent Notes, Posts, and Flows.
|
|
37
|
+
- **Edges**: Represent wiki-links connecting them.
|
|
38
|
+
- **Interaction**: Click a node to navigate to that page.
|
|
39
|
+
|
|
40
|
+
### 5. Flows (`/flows`)
|
|
41
|
+
Flows are a stream-style collection of daily notes, micro-blogging, or imported chat logs. They are ideal for quick thoughts that don't necessarily warrant a full blog post.
|
|
42
|
+
|
|
43
|
+
- **Location:** `content/flows/YYYY/MM/DD.mdx`
|
|
44
|
+
- **Navigation:** Grouped by date in a timeline view.
|
|
45
|
+
- **Importing:** Use `bun run new-flow-from-chat` to bring in external conversations.
|
|
46
|
+
|
|
47
|
+
## How to Use
|
|
48
|
+
|
|
49
|
+
1. **Create a Note**: Run `bun run new-note "My Concept"` (or `bun run new "My Concept" --note`).
|
|
50
|
+
2. **Create a Flow**: Run `bun run new-flow`.
|
|
51
|
+
3. **Link to it**: In a blog post, note, or flow, type `[[my-concept]]` or `[[2026-02-27]]`.
|
|
52
|
+
4. **Explore**: Visit `/notes` or `/flows` to see your collection, or `/graph` to see the connections.
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
In `site.config.ts`, you can configure the graph visualization:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export const siteConfig = {
|
|
60
|
+
// ...
|
|
61
|
+
features: {
|
|
62
|
+
graph: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
name: { en: "Graph", zh: "知识图谱" },
|
|
65
|
+
},
|
|
66
|
+
notes: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
name: { en: "Notes", zh: "笔记" },
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# imports/
|
|
2
|
+
|
|
3
|
+
Drop source files here before running import scripts.
|
|
4
|
+
**All contents are gitignored** — files stay local and are never committed.
|
|
5
|
+
|
|
6
|
+
## Subdirectories
|
|
7
|
+
|
|
8
|
+
| Directory | Command | What goes here |
|
|
9
|
+
|------------|--------------------------------|-----------------------------------------|
|
|
10
|
+
| `chats/` | `bun run new-flow-from-chat` | Group chat export files (`.txt`) |
|
|
11
|
+
| `pdfs/` | `bun run new-from-pdf` | PDF documents → posts with page images |
|
|
12
|
+
| `images/` | `bun run new-from-images` | Image folders → gallery posts |
|
|
13
|
+
|
|
14
|
+
## chats/ — auto-import workflow
|
|
15
|
+
|
|
16
|
+
Drop `.txt` chat export files into `imports/chats/` and run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun run new-flow-from-chat # import all new files
|
|
20
|
+
bun run new-flow-from-chat --dry-run # preview first
|
|
21
|
+
bun run new-flow-from-chat --all # re-import everything
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Already-processed filenames are tracked in `imports/chats/.imported`.
|
|
25
|
+
Running the command again will only pick up files added since the last run.
|
|
26
|
+
|
|
27
|
+
### Expected file format
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
username YYYY-MM-DD HH:mm:ss
|
|
31
|
+
message line 1
|
|
32
|
+
message line 2
|
|
33
|
+
|
|
34
|
+
username YYYY-MM-DD HH:mm:ss
|
|
35
|
+
message line 1
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
| Flag | Effect |
|
|
41
|
+
|-------------------|-----------------------------------------------------|
|
|
42
|
+
| `--author "Name"` | Only include messages from one participant |
|
|
43
|
+
| `--append` | Append to an existing flow instead of skipping |
|
|
44
|
+
| `--dry-run` | Preview without writing any files |
|
|
45
|
+
| `--all` | Re-import all files, ignoring previous import history |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hutusi/amytis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"packageManager": "bun@1.3.4",
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "next dev",
|
|
17
|
-
"build": "bun scripts/copy-assets.ts && next build && next-image-export-optimizer && pagefind --site out",
|
|
18
|
-
"build:dev": "bun scripts/copy-assets.ts && next build && pagefind --site out --output-path public/pagefind",
|
|
17
|
+
"build": "bun scripts/copy-assets.ts && bun run build:graph && next build && next-image-export-optimizer && pagefind --site out",
|
|
18
|
+
"build:dev": "bun scripts/copy-assets.ts && bun run build:graph && next build && pagefind --site out --output-path public/pagefind",
|
|
19
|
+
"build:graph": "NODE_ENV=production bun scripts/generate-knowledge-graph.ts",
|
|
19
20
|
"validate": "bun run lint && bun run test && bun run build:dev",
|
|
20
21
|
"clean": "rm -rf .next out public/posts public/books public/flows",
|
|
21
22
|
"new": "bun scripts/new-post.ts",
|
|
@@ -24,17 +25,22 @@
|
|
|
24
25
|
"new-from-pdf": "bun scripts/new-from-pdf.ts",
|
|
25
26
|
"new-from-images": "bun scripts/new-from-images.ts",
|
|
26
27
|
"new-flow": "bun scripts/new-flow.ts",
|
|
28
|
+
"new-flow-from-chat": "bun scripts/new-flow-from-chat.ts",
|
|
29
|
+
"new-note": "bun scripts/new-note.ts",
|
|
30
|
+
"import-book": "bun scripts/import-book.ts",
|
|
31
|
+
"sync-book": "bun scripts/sync-book-chapters.ts",
|
|
27
32
|
"series-draft": "bun scripts/series-draft.ts",
|
|
28
33
|
"start": "next start",
|
|
29
34
|
"lint": "eslint",
|
|
30
|
-
"test": "bun test",
|
|
31
|
-
"test:unit": "bun test src",
|
|
35
|
+
"test": "bun test src tests/unit tests/tooling && bun run test:int",
|
|
36
|
+
"test:unit": "bun test src tests/unit",
|
|
32
37
|
"test:int": "bun test tests/integration",
|
|
33
38
|
"test:e2e": "bun test tests/e2e"
|
|
34
39
|
},
|
|
35
40
|
"dependencies": {
|
|
36
41
|
"@giscus/react": "^3.1.0",
|
|
37
42
|
"@tailwindcss/typography": "^0.5.19",
|
|
43
|
+
"d3": "^7.9.0",
|
|
38
44
|
"github-slugger": "^2.0.0",
|
|
39
45
|
"gray-matter": "^4.0.3",
|
|
40
46
|
"image-size": "^2.0.2",
|
|
@@ -58,6 +64,7 @@
|
|
|
58
64
|
"devDependencies": {
|
|
59
65
|
"@tailwindcss/postcss": "^4.1.18",
|
|
60
66
|
"@types/bun": "^1.3.9",
|
|
67
|
+
"@types/d3": "^7.4.3",
|
|
61
68
|
"@types/image-size": "^0.8.0",
|
|
62
69
|
"@types/node": "^24.10.13",
|
|
63
70
|
"@types/react": "^19.2.14",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate public/knowledge-graph.json for the Knowledge Graph visualization.
|
|
3
|
+
*
|
|
4
|
+
* Nodes: all posts, all notes, and flows that appear as wikilink source/target.
|
|
5
|
+
* Edges: wikilink edges (from backlink index) + series membership edges.
|
|
6
|
+
*
|
|
7
|
+
* Run: NODE_ENV=production bun scripts/generate-knowledge-graph.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { getAllPosts, getAllNotes, getAllFlows, getSeriesData } from '../src/lib/markdown';
|
|
13
|
+
|
|
14
|
+
interface GraphNode {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
type: 'post' | 'note' | 'flow' | 'series';
|
|
18
|
+
url: string;
|
|
19
|
+
connections: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface GraphEdge {
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
type: 'wikilink' | 'series';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractWikilinks(content: string): string[] {
|
|
29
|
+
const slugs: string[] = [];
|
|
30
|
+
const re = /\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]/g;
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = re.exec(content)) !== null) {
|
|
33
|
+
slugs.push(match[1].trim());
|
|
34
|
+
}
|
|
35
|
+
return slugs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
console.log('Generating knowledge graph…');
|
|
40
|
+
|
|
41
|
+
const posts = getAllPosts();
|
|
42
|
+
const notes = getAllNotes();
|
|
43
|
+
const flows = getAllFlows();
|
|
44
|
+
|
|
45
|
+
// Build id→node map
|
|
46
|
+
const nodeMap = new Map<string, GraphNode>();
|
|
47
|
+
const edges: GraphEdge[] = [];
|
|
48
|
+
|
|
49
|
+
// Add all posts and notes
|
|
50
|
+
for (const post of posts) {
|
|
51
|
+
nodeMap.set(post.slug, { id: post.slug, title: post.title, type: 'post', url: `/posts/${post.slug}`, connections: 0 });
|
|
52
|
+
}
|
|
53
|
+
for (const note of notes) {
|
|
54
|
+
nodeMap.set(note.slug, { id: note.slug, title: note.title, type: 'note', url: `/notes/${note.slug}`, connections: 0 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Track which flows appear in wikilinks (to avoid including ALL flows)
|
|
58
|
+
const linkedFlowSlugs = new Set<string>();
|
|
59
|
+
|
|
60
|
+
// Scan all content for wikilinks
|
|
61
|
+
const allContent: Array<{ slug: string; title: string; type: 'post' | 'note' | 'flow'; content: string; url: string }> = [
|
|
62
|
+
...posts.map(p => ({ slug: p.slug, title: p.title, type: 'post' as const, content: p.content, url: `/posts/${p.slug}` })),
|
|
63
|
+
...notes.map(n => ({ slug: n.slug, title: n.title, type: 'note' as const, content: n.content, url: `/notes/${n.slug}` })),
|
|
64
|
+
...flows.map(f => ({ slug: f.slug, title: f.title, type: 'flow' as const, content: f.content, url: `/flows/${f.slug}` })),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Build wikilink edges (deduplicate per source document)
|
|
68
|
+
for (const item of allContent) {
|
|
69
|
+
const targets = extractWikilinks(item.content);
|
|
70
|
+
const seenTargets = new Set<string>();
|
|
71
|
+
for (const target of targets) {
|
|
72
|
+
if (target === item.slug) continue; // skip self
|
|
73
|
+
if (seenTargets.has(target)) continue; // skip duplicate within this doc
|
|
74
|
+
seenTargets.add(target);
|
|
75
|
+
edges.push({ source: item.slug, target, type: 'wikilink' });
|
|
76
|
+
|
|
77
|
+
// Ensure source exists in nodeMap
|
|
78
|
+
if (!nodeMap.has(item.slug)) {
|
|
79
|
+
nodeMap.set(item.slug, { id: item.slug, title: item.title, type: item.type, url: item.url, connections: 0 });
|
|
80
|
+
if (item.type === 'flow') linkedFlowSlugs.add(item.slug);
|
|
81
|
+
}
|
|
82
|
+
// Track referenced flows
|
|
83
|
+
const targetFlow = flows.find(f => f.slug === target);
|
|
84
|
+
if (targetFlow) {
|
|
85
|
+
linkedFlowSlugs.add(target);
|
|
86
|
+
if (!nodeMap.has(target)) {
|
|
87
|
+
nodeMap.set(target, { id: target, title: targetFlow.title, type: 'flow', url: `/flows/${target}`, connections: 0 });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add series nodes + series membership edges
|
|
94
|
+
const seriesSlugsSet = new Set<string>();
|
|
95
|
+
for (const post of posts) {
|
|
96
|
+
if (post.series) seriesSlugsSet.add(post.series);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const seriesSlug of seriesSlugsSet) {
|
|
100
|
+
const seriesData = getSeriesData(seriesSlug);
|
|
101
|
+
const seriesId = `series:${seriesSlug}`;
|
|
102
|
+
nodeMap.set(seriesId, {
|
|
103
|
+
id: seriesId,
|
|
104
|
+
title: seriesData?.title || seriesSlug,
|
|
105
|
+
type: 'series',
|
|
106
|
+
url: `/series/${seriesSlug}`,
|
|
107
|
+
connections: 0,
|
|
108
|
+
});
|
|
109
|
+
// Add edges from series to each post
|
|
110
|
+
for (const post of posts) {
|
|
111
|
+
if (post.series === seriesSlug) {
|
|
112
|
+
edges.push({ source: seriesId, target: post.slug, type: 'series' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Compute connection counts
|
|
118
|
+
for (const edge of edges) {
|
|
119
|
+
const src = nodeMap.get(edge.source);
|
|
120
|
+
if (src) src.connections++;
|
|
121
|
+
const tgt = nodeMap.get(edge.target);
|
|
122
|
+
if (tgt) tgt.connections++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Filter out nodes with no edges (isolated) to keep graph clean
|
|
126
|
+
const connectedIds = new Set<string>();
|
|
127
|
+
for (const edge of edges) {
|
|
128
|
+
connectedIds.add(edge.source);
|
|
129
|
+
connectedIds.add(edge.target);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Always include all notes and posts (they are the knowledge base)
|
|
133
|
+
const nodes = Array.from(nodeMap.values()).filter(n =>
|
|
134
|
+
n.type === 'note' || n.type === 'post' || n.type === 'series' || connectedIds.has(n.id)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Filter edges to only include those with both source and target in nodes
|
|
138
|
+
const validIds = new Set(nodes.map(n => n.id));
|
|
139
|
+
const validEdges = edges.filter(e => validIds.has(e.source) && validIds.has(e.target));
|
|
140
|
+
|
|
141
|
+
// Recompute connection counts from validEdges only (pre-filter counts were inflated)
|
|
142
|
+
const connectionCounts = new Map<string, number>();
|
|
143
|
+
for (const edge of validEdges) {
|
|
144
|
+
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
|
|
145
|
+
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
|
|
146
|
+
}
|
|
147
|
+
for (const node of nodes) {
|
|
148
|
+
node.connections = connectionCounts.get(node.id) ?? 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const graphData = { nodes, edges: validEdges };
|
|
152
|
+
|
|
153
|
+
const outputPath = path.join(process.cwd(), 'public', 'knowledge-graph.json');
|
|
154
|
+
fs.writeFileSync(outputPath, JSON.stringify(graphData, null, 2));
|
|
155
|
+
|
|
156
|
+
console.log(`✓ Written ${nodes.length} nodes, ${validEdges.length} edges → ${outputPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch(err => {
|
|
160
|
+
console.error('Error generating knowledge graph:', err);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|