@hutusi/amytis 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/GEMINI.md +6 -0
  3. package/README.md +14 -0
  4. package/TODO.md +15 -3
  5. package/bun.lock +5 -3
  6. package/content/flows/2026/02/05.md +0 -1
  7. package/content/flows/2026/02/10.mdx +2 -1
  8. package/content/flows/2026/02/15.md +2 -1
  9. package/content/flows/2026/02/18.mdx +2 -1
  10. package/content/flows/2026/02/20.md +0 -1
  11. package/content/notes/algorithms-and-data-structures.mdx +51 -0
  12. package/content/notes/digital-garden-philosophy.mdx +36 -0
  13. package/content/notes/react-server-components.mdx +49 -0
  14. package/content/notes/tailwind-v4.mdx +45 -0
  15. package/content/notes/zettelkasten-method.mdx +33 -0
  16. package/docs/ARCHITECTURE.md +8 -0
  17. package/docs/CONTRIBUTING.md +11 -0
  18. package/docs/DIGITAL_GARDEN.md +64 -0
  19. package/package.json +7 -3
  20. package/scripts/generate-knowledge-graph.ts +162 -0
  21. package/scripts/new-flow.ts +0 -5
  22. package/scripts/new-note.ts +53 -0
  23. package/site.config.ts +21 -1
  24. package/src/app/flows/[year]/[month]/[day]/page.tsx +32 -29
  25. package/src/app/flows/[year]/[month]/page.tsx +15 -13
  26. package/src/app/flows/[year]/page.tsx +22 -15
  27. package/src/app/flows/page/[page]/page.tsx +3 -9
  28. package/src/app/flows/page.tsx +3 -8
  29. package/src/app/globals.css +41 -0
  30. package/src/app/graph/page.tsx +19 -0
  31. package/src/app/notes/[slug]/page.tsx +128 -0
  32. package/src/app/notes/page/[page]/page.tsx +58 -0
  33. package/src/app/notes/page.tsx +31 -0
  34. package/src/app/page.tsx +0 -1
  35. package/src/app/posts/[slug]/page.tsx +4 -2
  36. package/src/app/search.json/route.ts +15 -1
  37. package/src/components/Backlinks.tsx +39 -0
  38. package/src/components/FlowCalendarSidebar.tsx +4 -2
  39. package/src/components/FlowContent.tsx +4 -3
  40. package/src/components/FlowHubTabs.tsx +50 -0
  41. package/src/components/FlowTimelineEntry.tsx +7 -9
  42. package/src/components/KnowledgeGraph.tsx +324 -0
  43. package/src/components/MarkdownRenderer.tsx +13 -2
  44. package/src/components/Navbar.tsx +235 -9
  45. package/src/components/NoteContent.tsx +123 -0
  46. package/src/components/NoteSidebar.tsx +132 -0
  47. package/src/components/RecentNotesSection.tsx +6 -11
  48. package/src/components/Search.tsx +5 -1
  49. package/src/components/TagContentTabs.tsx +0 -1
  50. package/src/i18n/translations.ts +21 -1
  51. package/src/layouts/PostLayout.tsx +8 -3
  52. package/src/lib/markdown.ts +276 -3
  53. package/src/lib/remark-wikilinks.ts +59 -0
  54. package/src/lib/search-utils.ts +2 -1
@@ -60,7 +60,7 @@ export const translations = {
60
60
  selected_books: "Selected Books",
61
61
  flow: "Flow",
62
62
  recent_notes: "Recent Notes",
63
- all_flows: "All Notes",
63
+ all_flows: "All Flows",
64
64
  no_flows: "No notes yet.",
65
65
  flow_subtitle: "{count} daily notes.",
66
66
  flows_in_year: "Notes in {year}",
@@ -117,6 +117,16 @@ export const translations = {
117
117
  sort_az: "A–Z",
118
118
  tags_count: "{shown} / {total} tags",
119
119
  tags_no_match: "No tags match \"{filter}\"",
120
+ notes: "Notes",
121
+ notes_subtitle: "{count} knowledge base notes.",
122
+ tab_daily_flow: "Daily",
123
+ tab_graph: "Graph",
124
+ backlinks: "Backlinks",
125
+ graph_subtitle: "A visual map of connected knowledge.",
126
+ search_type_note: "Note",
127
+ all_notes: "All Notes",
128
+ no_notes: "No notes yet.",
129
+ more: "More",
120
130
  },
121
131
  zh: {
122
132
  home: "首页",
@@ -236,6 +246,16 @@ export const translations = {
236
246
  sort_az: "A–Z",
237
247
  tags_count: "{shown} / {total} 个标签",
238
248
  tags_no_match: "未找到匹配\"{filter}\"的标签",
249
+ notes: "笔记",
250
+ notes_subtitle: "共 {count} 条知识库笔记。",
251
+ tab_daily_flow: "随笔",
252
+ tab_graph: "图谱",
253
+ backlinks: "反向链接",
254
+ graph_subtitle: "知识连接的可视化地图。",
255
+ search_type_note: "笔记",
256
+ all_notes: "全部笔记",
257
+ no_notes: "暂无笔记。",
258
+ more: "更多",
239
259
  },
240
260
  };
241
261
 
@@ -1,11 +1,12 @@
1
1
  import Link from 'next/link';
2
- import { getAuthorSlug, PostData } from '@/lib/markdown';
2
+ import { getAuthorSlug, PostData, BacklinkSource, SlugRegistryEntry } from '@/lib/markdown';
3
3
  import MarkdownRenderer from '@/components/MarkdownRenderer';
4
4
  import RelatedPosts from '@/components/RelatedPosts';
5
5
  import SeriesList from '@/components/SeriesList';
6
6
  import PostSidebar from '@/components/PostSidebar';
7
7
  import Comments from '@/components/Comments';
8
8
  import ExternalLinks from '@/components/ExternalLinks';
9
+ import Backlinks from '@/components/Backlinks';
9
10
  import Tag from '@/components/Tag';
10
11
  import ReadingProgressBar from '@/components/ReadingProgressBar';
11
12
  import PostNavigation from '@/components/PostNavigation';
@@ -21,9 +22,11 @@ interface PostLayoutProps {
21
22
  seriesTitle?: string;
22
23
  prevPost?: PostData | null;
23
24
  nextPost?: PostData | null;
25
+ backlinks?: BacklinkSource[];
26
+ slugRegistry?: Map<string, SlugRegistryEntry>;
24
27
  }
25
28
 
26
- export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitle, prevPost, nextPost }: PostLayoutProps) {
29
+ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitle, prevPost, nextPost, backlinks, slugRegistry }: PostLayoutProps) {
27
30
  const showToc = siteConfig.posts?.toc !== false && post.toc !== false && post.headings && post.headings.length > 0;
28
31
  const hasSeries = !!(post.series && seriesPosts && seriesPosts.length > 0);
29
32
  const showSidebar = showToc || hasSeries;
@@ -110,7 +113,7 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
110
113
  </div>
111
114
  )}
112
115
 
113
- <MarkdownRenderer content={post.content} latex={post.latex} slug={post.slug} />
116
+ <MarkdownRenderer content={post.content} latex={post.latex} slug={post.slug} slugRegistry={slugRegistry} />
114
117
 
115
118
  {post.tags && post.tags.length > 0 && (
116
119
  <div className="mt-12 pt-12 border-t border-muted/20 flex flex-wrap items-center gap-2">
@@ -125,6 +128,8 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
125
128
  <ExternalLinks links={post.externalLinks} />
126
129
  )}
127
130
 
131
+ <Backlinks backlinks={backlinks ?? []} />
132
+
128
133
  <ShareBar
129
134
  url={postUrl}
130
135
  title={post.title}
@@ -10,6 +10,7 @@ const pagesDirectory = path.join(process.cwd(), 'content');
10
10
  const seriesDirectory = path.join(process.cwd(), 'content', 'series');
11
11
  const booksDirectory = path.join(process.cwd(), 'content', 'books');
12
12
  const flowsDirectory = path.join(process.cwd(), 'content', 'flows');
13
+ const notesDirectory = path.join(process.cwd(), 'content', 'notes');
13
14
 
14
15
  const ExternalLinkSchema = z.object({
15
16
  name: z.string(),
@@ -98,7 +99,7 @@ export function generateExcerpt(content: string): string {
98
99
  plain = plain.replace(/!\[[^\]]*\]\([^)]+\)/g, '');
99
100
  plain = plain.replace(/\*\[([^\]]+)\*\]\([^)]+\)/g, '$1');
100
101
  plain = plain.replace(/(\$\*\*|__|\*|_)/g, '');
101
- plain = plain.replace(/`[^`]*`/g, '');
102
+ plain = plain.replace(/`([^`]+)`/g, '$1');
102
103
  plain = plain.replace(/^>\s+/gm, '');
103
104
  plain = plain.replace(/\s+/g, ' ').trim();
104
105
 
@@ -549,6 +550,7 @@ export function getFlowTags(): Record<string, number> {
549
550
  export function getAllTags(): Record<string, number> {
550
551
  const allPosts = getAllPosts();
551
552
  const allFlows = getAllFlows();
553
+ const allNotes = getAllNotes();
552
554
  const tags: Record<string, number> = {};
553
555
 
554
556
  allPosts.forEach((post) => {
@@ -565,6 +567,13 @@ export function getAllTags(): Record<string, number> {
565
567
  });
566
568
  });
567
569
 
570
+ allNotes.forEach((note) => {
571
+ note.tags.forEach((tag) => {
572
+ const normalizedTag = tag.toLowerCase();
573
+ tags[normalizedTag] = (tags[normalizedTag] || 0) + 1;
574
+ });
575
+ });
576
+
568
577
  return tags;
569
578
  }
570
579
 
@@ -965,7 +974,7 @@ export function getBooksByAuthor(author: string): BookData[] {
965
974
  // ─── Flows (Daily Notes) ────────────────────────────────────────────────────
966
975
 
967
976
  const FlowSchema = z.object({
968
- title: z.string(),
977
+ title: z.string().optional(),
969
978
  date: z.union([z.string(), z.date()]).transform(val => new Date(val).toISOString().split('T')[0]).optional(),
970
979
  tags: z.array(z.string()).optional().default([]),
971
980
  draft: z.boolean().optional().default(false),
@@ -1001,7 +1010,7 @@ function parseFlowFile(fullPath: string, slug: string): FlowData {
1001
1010
  return {
1002
1011
  slug,
1003
1012
  date,
1004
- title: data.title,
1013
+ title: data.title ?? date, // fall back to date string if no title in frontmatter
1005
1014
  tags: data.tags,
1006
1015
  draft: data.draft,
1007
1016
  content: contentWithoutH1,
@@ -1121,3 +1130,267 @@ export function getAdjacentFlows(slug: string): { prev: FlowData | null; next: F
1121
1130
  export function getRecentFlows(limit: number = 5): FlowData[] {
1122
1131
  return getAllFlows().slice(0, limit);
1123
1132
  }
1133
+
1134
+ // ─── Notes (Knowledge Base) ──────────────────────────────────────────────────
1135
+
1136
+ const NoteSchema = z.object({
1137
+ title: z.string(),
1138
+ date: z.union([z.string(), z.date()]).transform(val => new Date(val).toISOString().split('T')[0]).optional(),
1139
+ tags: z.array(z.string()).optional().default([]),
1140
+ draft: z.boolean().optional().default(false),
1141
+ aliases: z.array(z.string()).optional().default([]),
1142
+ toc: z.boolean().optional().default(true),
1143
+ backlinks: z.boolean().optional().default(true),
1144
+ });
1145
+
1146
+ export interface NoteData {
1147
+ slug: string;
1148
+ title: string;
1149
+ date: string;
1150
+ tags: string[];
1151
+ draft: boolean;
1152
+ aliases: string[];
1153
+ toc: boolean;
1154
+ backlinks: boolean;
1155
+ content: string;
1156
+ excerpt: string;
1157
+ headings: Heading[];
1158
+ readingTime: string;
1159
+ }
1160
+
1161
+ function parseNoteFile(fullPath: string, slug: string): NoteData {
1162
+ const fileContents = fs.readFileSync(fullPath, 'utf8');
1163
+ const { data: rawData, content } = matter(fileContents);
1164
+
1165
+ const parsed = NoteSchema.safeParse(rawData);
1166
+ if (!parsed.success) {
1167
+ console.error(`Invalid note frontmatter in ${fullPath}:`, parsed.error.format());
1168
+ throw new Error(`Invalid note frontmatter in ${fullPath}`);
1169
+ }
1170
+ const data = parsed.data;
1171
+
1172
+ const contentWithoutH1 = content.replace(/^\s*#\s+[^\n]+/, '').trim();
1173
+ const date = data.date || fs.statSync(fullPath).mtime.toISOString().split('T')[0];
1174
+ const excerpt = generateExcerpt(contentWithoutH1);
1175
+ const headings = getHeadings(content);
1176
+ const readingTime = calculateReadingTime(contentWithoutH1);
1177
+
1178
+ return {
1179
+ slug,
1180
+ title: data.title,
1181
+ date,
1182
+ tags: data.tags,
1183
+ draft: data.draft,
1184
+ aliases: data.aliases,
1185
+ toc: data.toc,
1186
+ backlinks: data.backlinks,
1187
+ content: contentWithoutH1,
1188
+ excerpt,
1189
+ headings,
1190
+ readingTime,
1191
+ };
1192
+ }
1193
+
1194
+ let _allNotes: NoteData[] | null = null;
1195
+
1196
+ export function getAllNotes(): NoteData[] {
1197
+ if (_allNotes && process.env.NODE_ENV === 'production') return _allNotes;
1198
+
1199
+ if (!fs.existsSync(notesDirectory)) {
1200
+ _allNotes = [];
1201
+ return _allNotes;
1202
+ }
1203
+
1204
+ const notes: NoteData[] = [];
1205
+ const items = fs.readdirSync(notesDirectory, { withFileTypes: true });
1206
+
1207
+ for (const item of items) {
1208
+ if (!item.isFile()) continue;
1209
+ if (!item.name.endsWith('.md') && !item.name.endsWith('.mdx')) continue;
1210
+ const slug = item.name.replace(/\.mdx?$/, '');
1211
+ const fullPath = path.join(notesDirectory, item.name);
1212
+ try {
1213
+ notes.push(parseNoteFile(fullPath, slug));
1214
+ } catch (e) {
1215
+ console.error(`Error parsing note ${fullPath}:`, e);
1216
+ }
1217
+ }
1218
+
1219
+ _allNotes = notes
1220
+ .filter(note => process.env.NODE_ENV !== 'production' || !note.draft)
1221
+ .sort((a, b) => (a.date < b.date ? 1 : -1));
1222
+
1223
+ return _allNotes;
1224
+ }
1225
+
1226
+ export function getNoteBySlug(slug: string): NoteData | null {
1227
+ if (!fs.existsSync(notesDirectory)) return null;
1228
+
1229
+ const mdxPath = path.join(notesDirectory, `${slug}.mdx`);
1230
+ const mdPath = path.join(notesDirectory, `${slug}.md`);
1231
+
1232
+ let fullPath = '';
1233
+ if (fs.existsSync(mdxPath)) fullPath = mdxPath;
1234
+ else if (fs.existsSync(mdPath)) fullPath = mdPath;
1235
+ else return null;
1236
+
1237
+ try {
1238
+ const note = parseNoteFile(fullPath, slug);
1239
+ if (process.env.NODE_ENV === 'production' && note.draft) return null;
1240
+ return note;
1241
+ } catch {
1242
+ return null;
1243
+ }
1244
+ }
1245
+
1246
+ export function getAdjacentNotes(slug: string): { prev: NoteData | null; next: NoteData | null } {
1247
+ const allNotes = getAllNotes(); // sorted newest-first
1248
+ const index = allNotes.findIndex(n => n.slug === slug);
1249
+ if (index === -1) return { prev: null, next: null };
1250
+ return {
1251
+ prev: index < allNotes.length - 1 ? allNotes[index + 1] : null, // older
1252
+ next: index > 0 ? allNotes[index - 1] : null, // newer
1253
+ };
1254
+ }
1255
+
1256
+ export function getRecentNotes(limit: number = 5): NoteData[] {
1257
+ return getAllNotes().slice(0, limit);
1258
+ }
1259
+
1260
+ export function getNoteTags(): Record<string, number> {
1261
+ const tags: Record<string, number> = {};
1262
+ getAllNotes().forEach(note => {
1263
+ note.tags.forEach(tag => {
1264
+ const normalized = tag.toLowerCase();
1265
+ tags[normalized] = (tags[normalized] || 0) + 1;
1266
+ });
1267
+ });
1268
+ return tags;
1269
+ }
1270
+
1271
+ export function getNotesByTag(tag: string): NoteData[] {
1272
+ return getAllNotes().filter(n =>
1273
+ n.tags.map(t => t.toLowerCase()).includes(tag.toLowerCase())
1274
+ );
1275
+ }
1276
+
1277
+ // ─── Slug Registry ───────────────────────────────────────────────────────────
1278
+
1279
+ export interface SlugRegistryEntry {
1280
+ url: string;
1281
+ type: 'post' | 'note' | 'flow' | 'series';
1282
+ title: string;
1283
+ }
1284
+
1285
+ let _slugRegistry: Map<string, SlugRegistryEntry> | null = null;
1286
+
1287
+ export function buildSlugRegistry(): Map<string, SlugRegistryEntry> {
1288
+ if (_slugRegistry && process.env.NODE_ENV === 'production') return _slugRegistry;
1289
+
1290
+ const map = new Map<string, SlugRegistryEntry>();
1291
+
1292
+ getAllPosts().forEach(p =>
1293
+ map.set(p.slug, { url: `/posts/${p.slug}`, type: 'post', title: p.title })
1294
+ );
1295
+
1296
+ getAllFlows().forEach(f =>
1297
+ map.set(f.slug, { url: `/flows/${f.slug}`, type: 'flow', title: f.title })
1298
+ );
1299
+
1300
+ getAllNotes().forEach(n => {
1301
+ if (map.has(n.slug)) {
1302
+ console.warn(`[slugRegistry] Note slug "${n.slug}" conflicts with an existing entry.`);
1303
+ }
1304
+ map.set(n.slug, { url: `/notes/${n.slug}`, type: 'note', title: n.title });
1305
+ n.aliases.forEach(a => {
1306
+ if (map.has(a)) {
1307
+ console.warn(`[slugRegistry] Note alias "${a}" (→ ${n.slug}) conflicts with existing slug; skipping.`);
1308
+ } else {
1309
+ map.set(a, { url: `/notes/${n.slug}`, type: 'note', title: n.title });
1310
+ }
1311
+ });
1312
+ });
1313
+
1314
+ if (fs.existsSync(seriesDirectory)) {
1315
+ fs.readdirSync(seriesDirectory, { withFileTypes: true }).forEach(entry => {
1316
+ if (!entry.isDirectory()) return;
1317
+ const slug = entry.name;
1318
+ const seriesData = getSeriesData(slug);
1319
+ map.set(slug, {
1320
+ url: `/series/${slug}`,
1321
+ type: 'series',
1322
+ title: seriesData?.title || slug,
1323
+ });
1324
+ });
1325
+ }
1326
+
1327
+ _slugRegistry = map;
1328
+ return map;
1329
+ }
1330
+
1331
+ // ─── Backlink Index ──────────────────────────────────────────────────────────
1332
+
1333
+ export interface BacklinkSource {
1334
+ slug: string;
1335
+ title: string;
1336
+ type: 'post' | 'note' | 'flow' | 'series';
1337
+ url: string;
1338
+ context: string;
1339
+ }
1340
+
1341
+ function extractWikilinkContext(text: string, matchStart: number, matchEnd: number): string {
1342
+ const RADIUS = 120;
1343
+ const start = Math.max(0, matchStart - RADIUS);
1344
+ const end = Math.min(text.length, matchEnd + RADIUS);
1345
+ let ctx = text.slice(start, end);
1346
+
1347
+ // Replace wikilinks in context with just display text for readability
1348
+ ctx = ctx.replace(/\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g, (_, slug, display) => display || slug);
1349
+
1350
+ if (start > 0) ctx = ctx.replace(/^[^\s.!?]{1,30}/, '').trimStart();
1351
+ if (end < text.length) ctx = ctx.replace(/[^\s.!?]{1,30}$/, '').trimEnd();
1352
+
1353
+ return ctx.trim().slice(0, 200);
1354
+ }
1355
+
1356
+ function buildBacklinkIndex(): Map<string, BacklinkSource[]> {
1357
+ const index = new Map<string, BacklinkSource[]>();
1358
+
1359
+ const addBacklinks = (
1360
+ content: string,
1361
+ sourceSlug: string,
1362
+ sourceTitle: string,
1363
+ sourceType: BacklinkSource['type'],
1364
+ sourceUrl: string
1365
+ ) => {
1366
+ // Create a fresh RegExp per call to avoid lastIndex issues with 'g' flag
1367
+ const WIKILINK = /\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g;
1368
+ let match;
1369
+ while ((match = WIKILINK.exec(content)) !== null) {
1370
+ const targetSlug = match[1].trim();
1371
+ if (targetSlug === sourceSlug) continue; // skip self-references
1372
+ const context = extractWikilinkContext(content, match.index, match.index + match[0].length);
1373
+ let sources = index.get(targetSlug);
1374
+ if (!sources) {
1375
+ sources = [];
1376
+ index.set(targetSlug, sources);
1377
+ }
1378
+ if (!sources.some(b => b.slug === sourceSlug && b.type === sourceType)) {
1379
+ sources.push({ slug: sourceSlug, title: sourceTitle, type: sourceType, url: sourceUrl, context });
1380
+ }
1381
+ }
1382
+ };
1383
+
1384
+ getAllPosts().forEach(p => addBacklinks(p.content, p.slug, p.title, 'post', `/posts/${p.slug}`));
1385
+ getAllNotes().forEach(n => addBacklinks(n.content, n.slug, n.title, 'note', `/notes/${n.slug}`));
1386
+ getAllFlows().forEach(f => addBacklinks(f.content, f.slug, f.title, 'flow', `/flows/${f.slug}`));
1387
+
1388
+ return index;
1389
+ }
1390
+
1391
+ let _backlinkIndex: Map<string, BacklinkSource[]> | null = null;
1392
+
1393
+ export function getBacklinks(slug: string): BacklinkSource[] {
1394
+ if (!_backlinkIndex || process.env.NODE_ENV !== 'production') _backlinkIndex = buildBacklinkIndex();
1395
+ return _backlinkIndex.get(slug) ?? [];
1396
+ }
@@ -0,0 +1,59 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import type { Root, Text, Parent } from 'mdast';
3
+ import type { SlugRegistryEntry } from './markdown';
4
+
5
+ interface WikilinksOptions {
6
+ slugRegistry: Map<string, SlugRegistryEntry>;
7
+ }
8
+
9
+ export default function remarkWikilinks({ slugRegistry }: WikilinksOptions) {
10
+ return (tree: Root) => {
11
+ visit(tree, 'text', (node: Text, index: number | undefined, parent: Parent | undefined) => {
12
+ if (!parent || index === undefined) return;
13
+ if (!node.value.includes('[[')) return;
14
+
15
+ // Create fresh regex each time to avoid lastIndex issue with 'g' flag
16
+ const WIKILINK = /\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g;
17
+ if (!WIKILINK.test(node.value)) return;
18
+ WIKILINK.lastIndex = 0;
19
+
20
+ const newNodes: (Text | { type: 'html'; value: string })[] = [];
21
+ let last = 0;
22
+ let match: RegExpExecArray | null;
23
+
24
+ while ((match = WIKILINK.exec(node.value)) !== null) {
25
+ if (match.index > last) {
26
+ newNodes.push({ type: 'text', value: node.value.slice(last, match.index) });
27
+ }
28
+
29
+ const slug = match[1].trim();
30
+ const display = match[2]?.trim() || slug;
31
+ const entry = slugRegistry.get(slug);
32
+
33
+ if (entry) {
34
+ newNodes.push({
35
+ type: 'html',
36
+ value: `<a href="${entry.url}" class="wikilink wikilink--resolved wikilink--${entry.type}">${display}</a>`,
37
+ });
38
+ } else {
39
+ newNodes.push({
40
+ type: 'html',
41
+ value: `<span class="wikilink wikilink--broken" title="[[${slug}]] not found">${display}</span>`,
42
+ });
43
+ }
44
+
45
+ last = match.index + match[0].length;
46
+ }
47
+
48
+ if (last < node.value.length) {
49
+ newNodes.push({ type: 'text', value: node.value.slice(last) });
50
+ }
51
+
52
+ if (newNodes.length > 1 || (newNodes.length === 1 && newNodes[0].type !== 'text')) {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ (parent.children as any[]).splice(index, 1, ...newNodes);
55
+ return index + newNodes.length; // skip inserted nodes
56
+ }
57
+ });
58
+ };
59
+ }
@@ -1,9 +1,10 @@
1
- export type ContentType = 'All' | 'Post' | 'Flow' | 'Book';
1
+ export type ContentType = 'All' | 'Post' | 'Flow' | 'Book' | 'Note';
2
2
 
3
3
  /** Derive content type from a Pagefind result URL. */
4
4
  export function getResultType(url: string): Exclude<ContentType, 'All'> {
5
5
  if (url.includes('/flows/')) return 'Flow';
6
6
  if (url.includes('/books/')) return 'Book';
7
+ if (url.includes('/notes/')) return 'Note';
7
8
  return 'Post';
8
9
  }
9
10