@hutusi/amytis 1.12.0 → 1.14.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 (78) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/GEMINI.md +9 -1
  3. package/README.md +26 -17
  4. package/README.zh.md +180 -100
  5. package/bun.lock +78 -74
  6. package/content/books/notes-on-thinking/cost-of-certainty.mdx +9 -0
  7. package/content/books/notes-on-thinking/index.mdx +16 -0
  8. package/content/books/notes-on-thinking/mental-models.mdx +9 -0
  9. package/content/books/the-pragmatic-writer/finding-your-voice.mdx +9 -0
  10. package/content/books/the-pragmatic-writer/index.mdx +18 -0
  11. package/content/books/the-pragmatic-writer/the-editing-loop.mdx +9 -0
  12. package/content/books/the-pragmatic-writer/why-writing-matters.mdx +9 -0
  13. package/content/flows/2026/03/01.md +9 -0
  14. package/content/flows/2026/03/03.md +9 -0
  15. package/content/flows/2026/03/05.md +10 -0
  16. package/content/flows/2026/03/07.md +11 -0
  17. package/content/posts/images/vibrant-waves.jpg +0 -0
  18. package/content/posts/welcome-to-amytis.mdx +3 -0
  19. package/content/series/markdown-showcase/index.mdx +2 -1
  20. package/content/series/markdown-showcase/mathematical-notation.mdx +8 -4
  21. package/content/series/markdown-showcase/syntax-highlighting.mdx +9 -5
  22. package/content/series/markdown-showcase/visuals-and-diagrams.mdx +8 -4
  23. package/content/{posts → series/markdown-showcase}//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +12 -7
  24. package/content/series/modern-web-dev/index.mdx +4 -2
  25. package/docs/ARCHITECTURE.md +8 -1
  26. package/docs/DIGITAL_GARDEN.md +22 -1
  27. package/package.json +12 -12
  28. package/public/next-image-export-optimizer-hashes.json +3 -2
  29. package/scripts/new-flow.ts +1 -0
  30. package/site.config.example.ts +3 -4
  31. package/site.config.ts +6 -7
  32. package/src/app/[slug]/[postSlug]/page.tsx +19 -2
  33. package/src/app/[slug]/page/[page]/page.tsx +26 -5
  34. package/src/app/[slug]/page.tsx +28 -8
  35. package/src/app/all.atom/route.ts +7 -0
  36. package/src/app/all.xml/route.ts +7 -0
  37. package/src/app/archive/page.tsx +7 -4
  38. package/src/app/feed.atom/route.ts +2 -57
  39. package/src/app/feed.xml/route.ts +2 -64
  40. package/src/app/flows/[year]/[month]/[day]/page.tsx +13 -0
  41. package/src/app/flows/feed.atom/route.ts +7 -0
  42. package/src/app/flows/feed.xml/route.ts +7 -0
  43. package/src/app/page.tsx +1 -2
  44. package/src/app/posts/[slug]/page.tsx +28 -9
  45. package/src/app/posts/feed.atom/route.ts +9 -0
  46. package/src/app/posts/feed.xml/route.ts +9 -0
  47. package/src/app/series/[slug]/page.tsx +46 -4
  48. package/src/components/CuratedSeriesSection.tsx +7 -11
  49. package/src/components/FeaturedStoriesSection.tsx +1 -1
  50. package/src/components/FlowCalendarSidebar.tsx +1 -1
  51. package/src/components/FlowContent.tsx +2 -1
  52. package/src/components/FlowTimelineEntry.tsx +7 -1
  53. package/src/components/Footer.tsx +6 -6
  54. package/src/components/HorizontalScroll.tsx +5 -14
  55. package/src/components/MarkdownRenderer.test.tsx +6 -0
  56. package/src/components/MarkdownRenderer.tsx +18 -16
  57. package/src/components/Navbar.tsx +1 -1
  58. package/src/components/PostList.tsx +20 -36
  59. package/src/components/PostSidebar.tsx +1 -1
  60. package/src/components/RecentNotesSection.tsx +4 -0
  61. package/src/components/SelectedBooksSection.tsx +65 -25
  62. package/src/components/SeriesCatalog.tsx +9 -7
  63. package/src/i18n/translations.ts +2 -0
  64. package/src/layouts/PostLayout.tsx +1 -1
  65. package/src/layouts/SimpleLayout.tsx +3 -3
  66. package/src/lib/feed-utils.ts +158 -18
  67. package/src/lib/markdown.ts +26 -5
  68. package/src/lib/urls.ts +9 -4
  69. package/tests/e2e/mobile/mobile-compat.spec.ts +58 -0
  70. package/tests/e2e/navigation.test.ts +26 -0
  71. package/tests/integration/collections.test.ts +17 -2
  72. package/tests/integration/feed-utils.test.ts +52 -0
  73. package/tests/integration/flow-title.test.ts +53 -0
  74. package/tests/integration/markdown-features.test.ts +3 -3
  75. package/tests/integration/reading-time-headings.test.ts +2 -2
  76. package/tests/unit/static-params.test.ts +155 -22
  77. package/tests/unit/urls.test.ts +10 -12
  78. /package/content/posts/{multilingual-test.mdx → multilingual-test-/344/270/255/346/226/207/351/225/277/346/240/207/351/242/230.mdx"} +0 -0
@@ -121,6 +121,8 @@ export interface PostData {
121
121
  content: string;
122
122
  headings: Heading[];
123
123
  contentLocales?: Record<string, { content: string; title?: string; excerpt?: string; headings?: Heading[] }>;
124
+ /** Public-relative base path used for resolving co-located images (e.g. "posts/my-post" or "posts" for root flat files). */
125
+ imageBaseSlug: string;
124
126
  }
125
127
 
126
128
  export function calculateReadingTime(content: string): string {
@@ -235,6 +237,12 @@ function getSeriesTitle(slug: string): string | undefined {
235
237
  function parseMarkdownFile(fullPath: string, slug: string, dateFromFileName?: string, seriesName?: string): PostData {
236
238
  const fileContents = fs.readFileSync(fullPath, 'utf8');
237
239
  const { data: rawData, content } = matter(fileContents);
240
+ // Flat files directly in content/posts/ share the posts root public directory for images.
241
+ // Folder-based posts and series posts each have their own public subdirectory.
242
+ const isRootFlatPost = path.basename(fullPath) !== 'index.mdx' &&
243
+ path.basename(fullPath) !== 'index.md' &&
244
+ path.dirname(fullPath) === contentDirectory;
245
+ const imageBaseSlug = isRootFlatPost ? 'posts' : `posts/${slug}`;
238
246
 
239
247
  const parsed = PostSchema.safeParse(rawData);
240
248
  if (!parsed.success) {
@@ -279,7 +287,7 @@ function parseMarkdownFile(fullPath: string, slug: string, dateFromFileName?: st
279
287
  let coverImage = data.coverImage;
280
288
  if (coverImage && !coverImage.startsWith('http') && !coverImage.startsWith('/') && !coverImage.startsWith('text:')) {
281
289
  const cleanPath = coverImage.replace(/^\.\//, '');
282
- coverImage = `/posts/${slug}/${cleanPath}`;
290
+ coverImage = `/${imageBaseSlug}/${cleanPath}`;
283
291
  }
284
292
 
285
293
  return {
@@ -310,6 +318,7 @@ function parseMarkdownFile(fullPath: string, slug: string, dateFromFileName?: st
310
318
  readingTime,
311
319
  content: contentWithoutH1,
312
320
  headings,
321
+ imageBaseSlug,
313
322
  };
314
323
  }
315
324
 
@@ -874,19 +883,30 @@ export function getCollectionPosts(collectionSlug: string): PostData[] {
874
883
  const data = getSeriesData(collectionSlug);
875
884
  if (data?.type !== 'collection' || !data.items) return [];
876
885
 
886
+ const getCollectionKey = (post: PostData) =>
887
+ post.series ? `${post.series}/${post.slug}` : `posts/${post.slug}`;
888
+
889
+ const allPosts = getAllPosts();
890
+ const postIndex = new Map(allPosts.map((post) => [getCollectionKey(post), post]));
877
891
  const seen = new Set<string>();
892
+
878
893
  return data.items
879
894
  .flatMap(item => {
880
895
  if ('series' in item) {
881
896
  const posts = getSeriesPosts(item.series);
882
897
  return item.exclude ? posts.filter(p => !item.exclude!.includes(p.slug)) : posts;
883
898
  }
884
- const post = getPostBySlug(item.post);
899
+
900
+ const post = item.post.includes('/')
901
+ ? postIndex.get(item.post)
902
+ : getPostBySlug(item.post);
903
+
885
904
  return post ? [post] : [];
886
905
  })
887
906
  .filter(post => {
888
- if (seen.has(post.slug)) return false;
889
- seen.add(post.slug);
907
+ const key = getCollectionKey(post);
908
+ if (seen.has(key)) return false;
909
+ seen.add(key);
890
910
  return true;
891
911
  });
892
912
  }
@@ -1176,6 +1196,7 @@ function parseFlowFile(fullPath: string, slug: string): FlowData {
1176
1196
  }
1177
1197
  const data = parsed.data;
1178
1198
 
1199
+ const h1Match = content.match(/^\s*#\s+(.+)/);
1179
1200
  const contentWithoutH1 = content.replace(/^\s*#\s+[^\n]+/, '').trim();
1180
1201
  const date = data.date || slug.replace(/\//g, '-'); // slug is YYYY/MM/DD, convert to YYYY-MM-DD
1181
1202
  const excerpt = generateExcerpt(contentWithoutH1);
@@ -1184,7 +1205,7 @@ function parseFlowFile(fullPath: string, slug: string): FlowData {
1184
1205
  return {
1185
1206
  slug,
1186
1207
  date,
1187
- title: data.title ?? date, // fall back to date string if no title in frontmatter
1208
+ title: data.title?.trim() || h1Match?.[1]?.trim() || date, // frontmatter(non-empty) H1 date
1188
1209
  tags: data.tags,
1189
1210
  draft: data.draft,
1190
1211
  commentable: data.commentable,
package/src/lib/urls.ts CHANGED
@@ -6,8 +6,8 @@ function normalizeSegment(segment: string): string {
6
6
  }
7
7
 
8
8
  // Top-level route segments reserved by the app — series slugs must not collide with these.
9
- const RESERVED_ROUTE_SEGMENTS = new Set([
10
- 'series', 'books', 'flows', 'tags', 'authors', 'archive', 'notes', 'graph', 'page', 'api',
9
+ export const RESERVED_ROUTE_SEGMENTS = new Set([
10
+ 'posts', 'series', 'books', 'flows', 'tags', 'authors', 'archive', 'notes', 'graph', 'search', 'page', 'api',
11
11
  ]);
12
12
 
13
13
  export function getPostsBasePath(): string {
@@ -22,7 +22,7 @@ export function getSeriesCustomPaths(): Record<string, string> {
22
22
  }
23
23
 
24
24
  export function getSeriesAutoPaths(): boolean {
25
- return siteConfig.series?.autoPaths ?? false;
25
+ return siteConfig.series?.autoPaths ?? true;
26
26
  }
27
27
 
28
28
  /**
@@ -41,7 +41,7 @@ export function validateSeriesAutoPaths(seriesSlugs: string[], extraReserved: st
41
41
  const reserved = new Set([...RESERVED_ROUTE_SEGMENTS, basePath, ...extraReserved]);
42
42
 
43
43
  for (const slug of seriesSlugs) {
44
- if (slug in customPaths) continue; // Has an explicit override — skip
44
+ if (Object.hasOwn(customPaths, slug)) continue; // Has an explicit override — skip
45
45
  if (reserved.has(slug)) {
46
46
  throw new Error(
47
47
  `[amytis] Series slug "${slug}" conflicts with the reserved route "/${slug}". ` +
@@ -71,6 +71,11 @@ export function getPostsPageUrl(page: number): string {
71
71
  return `/${getPostsBasePath()}/page/${page}`;
72
72
  }
73
73
 
74
+ /** Returns the series listing URL. */
75
+ export function getSeriesListUrl(): string {
76
+ return '/series';
77
+ }
78
+
74
79
  /** Returns the books listing URL. */
75
80
  export function getBooksListUrl(): string {
76
81
  return '/books';
@@ -404,4 +404,62 @@ test.describe('Mobile Compatibility', () => {
404
404
  expect(await hasNoHorizontalOverflow(page)).toBe(true);
405
405
  });
406
406
  });
407
+
408
+ // ── Post list cover image links ────────────────────────────────────────────
409
+ test.describe('Post list cover image links', () => {
410
+ test('cover image in post list is wrapped in a link', async ({ page }) => {
411
+ await page.goto('/posts');
412
+ await page.waitForLoadState('load');
413
+
414
+ // Find the first cover image inside the post list and check its parent link
415
+ const imageLink = page.locator('article a:has(img)').first();
416
+ await expect(imageLink).toHaveAttribute('href', /.+/);
417
+ });
418
+
419
+ test('clicking post list cover image navigates to post page', async ({ page }) => {
420
+ await page.goto('/posts');
421
+ await page.waitForLoadState('load');
422
+
423
+ const imageLink = page.locator('article a:has(img)').first();
424
+ const href = await imageLink.getAttribute('href');
425
+ if (!href) { test.skip(); return; }
426
+
427
+ await imageLink.click();
428
+ await page.waitForLoadState('domcontentloaded');
429
+ expect(page.url()).toContain(href);
430
+ });
431
+
432
+ test('cover image in series catalog is wrapped in a link', async ({ page }) => {
433
+ await page.goto('/series');
434
+ await page.waitForLoadState('load');
435
+ const seriesLink = page.locator('a[href^="/series/"]').first();
436
+ const seriesHref = await seriesLink.getAttribute('href');
437
+ if (!seriesHref) { test.skip(); return; }
438
+
439
+ await page.goto(seriesHref);
440
+ await page.waitForLoadState('load');
441
+
442
+ const imageLink = page.locator('article a:has(img)').first();
443
+ await expect(imageLink).toHaveAttribute('href', /.+/);
444
+ });
445
+
446
+ test('clicking series catalog cover image navigates to post page', async ({ page }) => {
447
+ await page.goto('/series');
448
+ await page.waitForLoadState('load');
449
+ const seriesLink = page.locator('a[href^="/series/"]').first();
450
+ const seriesHref = await seriesLink.getAttribute('href');
451
+ if (!seriesHref) { test.skip(); return; }
452
+
453
+ await page.goto(seriesHref);
454
+ await page.waitForLoadState('load');
455
+
456
+ const imageLink = page.locator('article a:has(img)').first();
457
+ const href = await imageLink.getAttribute('href');
458
+ if (!href) { test.skip(); return; }
459
+
460
+ await imageLink.click();
461
+ await page.waitForLoadState('domcontentloaded');
462
+ expect(page.url()).toContain(href);
463
+ });
464
+ });
407
465
  });
@@ -1,4 +1,5 @@
1
1
  import { describe, test, expect } from "bun:test";
2
+ import { siteConfig } from "../../site.config";
2
3
 
3
4
  const BASE_URL = "http://localhost:3000";
4
5
 
@@ -54,6 +55,7 @@ describe("E2E: Navigation & Assets", () => {
54
55
 
55
56
  test("feed.atom should be a valid Atom feed", async () => {
56
57
  if (!(await isServerRunning())) return;
58
+ if (siteConfig.feed.format === "rss") return; // skip if Atom is disabled
57
59
 
58
60
  const response = await fetch(`${BASE_URL}/feed.atom`);
59
61
  expect(response.status).toBe(200);
@@ -62,4 +64,28 @@ describe("E2E: Navigation & Assets", () => {
62
64
  expect(text).toContain('xmlns="http://www.w3.org/2005/Atom"');
63
65
  expect(text).toContain("<entry>");
64
66
  });
67
+
68
+ test("type-specific feeds should be accessible", async () => {
69
+ if (!(await isServerRunning())) return;
70
+
71
+ const feedUrls = [
72
+ "/posts/feed.xml",
73
+ "/flows/feed.xml",
74
+ "/all.xml",
75
+ ];
76
+
77
+ if (siteConfig.feed.format === "atom" || siteConfig.feed.format === "both") {
78
+ feedUrls.push(
79
+ "/posts/feed.atom",
80
+ "/flows/feed.atom",
81
+ "/all.atom"
82
+ );
83
+ }
84
+
85
+ for (const url of feedUrls) {
86
+ const response = await fetch(`${BASE_URL}${url}`);
87
+ expect(response.status).toBe(200);
88
+ expect(response.headers.get("content-type")).toContain("xml");
89
+ }
90
+ });
65
91
  });
@@ -44,6 +44,21 @@ describe("Integration: Collections", () => {
44
44
  expect(slugs).toContain("understanding-react-hooks");
45
45
  });
46
46
 
47
+ test("getCollectionPosts resolves namespaced post entries properly", () => {
48
+ // The modern-web-dev collection uses the "posts/asynchronous-javascript" syntax.
49
+ // This test ensures that the namespaced resolution logic successfully found it.
50
+ const posts = getCollectionPosts("modern-web-dev");
51
+ const foundPost = posts.find(p => p.slug === "asynchronous-javascript");
52
+ expect(foundPost).toBeDefined();
53
+ // Because the namespace was "posts/", its series should be undefined
54
+ expect(foundPost!.series).toBeUndefined();
55
+
56
+ // Also verify series-based namespace
57
+ const seriesPost = posts.find(p => p.slug === "02-architecture");
58
+ expect(seriesPost).toBeDefined();
59
+ expect(seriesPost!.series).toBe("digital-garden");
60
+ });
61
+
47
62
  test("getCollectionPosts includes posts from referenced series", () => {
48
63
  const posts = getCollectionPosts("modern-web-dev");
49
64
  const slugs = posts.map((p) => p.slug);
@@ -108,8 +123,8 @@ describe("Integration: Collections", () => {
108
123
  test("getAllSeries includes collection with correct post count", () => {
109
124
  const all = getAllSeries();
110
125
  expect(all).toHaveProperty("modern-web-dev");
111
- // modern-web-dev has 2 standalone posts + 2 from nextjs-deep-dive
112
- expect(all["modern-web-dev"].length).toBe(4);
126
+ // modern-web-dev has 2 standalone posts + 4 from nextjs-deep-dive
127
+ expect(all["modern-web-dev"].length).toBe(6);
113
128
  });
114
129
 
115
130
  test("getAllSeries collection posts are sorted by date descending", () => {
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { getFeedItems } from "../../src/lib/feed-utils";
3
3
  import { getAllPosts, getAllFlows } from "../../src/lib/markdown";
4
+ import { getPostUrl, getFlowUrl } from "../../src/lib/urls";
4
5
  import { siteConfig } from "../../site.config";
5
6
 
6
7
  describe("Integration: Feed Utils", () => {
@@ -142,4 +143,55 @@ describe("Integration: Feed Utils", () => {
142
143
  }
143
144
  });
144
145
  });
146
+
147
+ test("feedType 'posts' returns only post items", () => {
148
+ const originalMaxItems = siteConfig.feed.maxItems;
149
+ try {
150
+ siteConfig.feed.maxItems = 0;
151
+ const items = getFeedItems('posts');
152
+ const allPosts = getAllPosts();
153
+ const baseUrl = siteConfig.baseUrl.replace(/\/+$/, "");
154
+ expect(items.length).toBe(allPosts.length);
155
+ expect(items.map((item) => item.url).sort()).toEqual(
156
+ allPosts.map((post) => `${baseUrl}${getPostUrl(post)}`).sort()
157
+ );
158
+ } finally {
159
+ siteConfig.feed.maxItems = originalMaxItems;
160
+ }
161
+ });
162
+
163
+ test("feedType 'flows' returns only flow items", () => {
164
+ const originalMaxItems = siteConfig.feed.maxItems;
165
+ try {
166
+ siteConfig.feed.maxItems = 0;
167
+ const items = getFeedItems('flows');
168
+ const allFlows = getAllFlows();
169
+ const baseUrl = siteConfig.baseUrl.replace(/\/+$/, "");
170
+ expect(items.length).toBe(allFlows.length);
171
+ expect(items.map((item) => item.url).sort()).toEqual(
172
+ allFlows.map((flow) => `${baseUrl}${getFlowUrl(flow.slug)}`).sort()
173
+ );
174
+ } finally {
175
+ siteConfig.feed.maxItems = originalMaxItems;
176
+ }
177
+ });
178
+
179
+ test("feedType 'all' returns both post and flow items", () => {
180
+ const originalMaxItems = siteConfig.feed.maxItems;
181
+ try {
182
+ siteConfig.feed.maxItems = 0;
183
+ const items = getFeedItems('all');
184
+ const allPosts = getAllPosts();
185
+ const allFlows = getAllFlows();
186
+ const baseUrl = siteConfig.baseUrl.replace(/\/+$/, "");
187
+ expect(items.map((item) => item.url).sort()).toEqual(
188
+ [
189
+ ...allPosts.map((post) => `${baseUrl}${getPostUrl(post)}`),
190
+ ...allFlows.map((flow) => `${baseUrl}${getFlowUrl(flow.slug)}`),
191
+ ].sort()
192
+ );
193
+ } finally {
194
+ siteConfig.feed.maxItems = originalMaxItems;
195
+ }
196
+ });
145
197
  });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getFlowBySlug, getAllFlows } from "../../src/lib/markdown";
3
+
4
+ describe("Integration: Flow Title Resolution", () => {
5
+ test("frontmatter title takes priority over H1 and date", () => {
6
+ // content/flows/2026/03/05.md has `title: 'JSDoc type comments'` in frontmatter
7
+ const flow = getFlowBySlug("2026/03/05");
8
+ expect(flow).not.toBeNull();
9
+ expect(flow!.title).toBe("JSDoc type comments");
10
+ });
11
+
12
+ test("H1 heading is extracted as title when no frontmatter title", () => {
13
+ // content/flows/2026/03/07.md has no frontmatter title but has `# Using Claude Code`
14
+ const flow = getFlowBySlug("2026/03/07");
15
+ expect(flow).not.toBeNull();
16
+ expect(flow!.title).toBe("Using Claude Code");
17
+ });
18
+
19
+ test("date is used as fallback when no frontmatter title or H1", () => {
20
+ // content/flows/2026/02/05.md has no title and no H1
21
+ const flow = getFlowBySlug("2026/02/05");
22
+ expect(flow).not.toBeNull();
23
+ expect(flow!.title).toBe("2026-02-05");
24
+ });
25
+
26
+ test("H1 heading is stripped from content body", () => {
27
+ const flow = getFlowBySlug("2026/03/07");
28
+ expect(flow).not.toBeNull();
29
+ // The H1 should be extracted as title but removed from content
30
+ expect(flow!.content).not.toMatch(/^#\s+Using Claude Code/m);
31
+ // The body content should still be present
32
+ expect(flow!.content).toContain("Claude Code");
33
+ });
34
+
35
+ test("every flow has a non-empty title", () => {
36
+ const flows = getAllFlows();
37
+ expect(flows.length).toBeGreaterThan(0);
38
+ flows.forEach((flow) => {
39
+ expect(flow.title).toBeTruthy();
40
+ expect(flow.title.trim().length).toBeGreaterThan(0);
41
+ });
42
+ });
43
+
44
+ test("flow with frontmatter title preserves content H1 independently", () => {
45
+ // content/flows/2026/03/05.md has frontmatter title — any H1 in content
46
+ // should still be stripped, but title comes from frontmatter
47
+ const flow = getFlowBySlug("2026/03/05");
48
+ expect(flow).not.toBeNull();
49
+ expect(flow!.title).toBe("JSDoc type comments");
50
+ // Content should not start with an H1
51
+ expect(flow!.content).not.toMatch(/^\s*#\s+/);
52
+ });
53
+ });
@@ -3,14 +3,14 @@ import { getPostBySlug } from "../../src/lib/markdown";
3
3
 
4
4
  describe("Integration: Markdown Features", () => {
5
5
  test("should correctly load multilingual post", () => {
6
- const post = getPostBySlug("multilingual-test");
6
+ const post = getPostBySlug("multilingual-test-中文长标题");
7
7
  expect(post).not.toBeNull();
8
8
  expect(post?.title).toContain("多语言测试");
9
9
  expect(post?.latex).toBe(true);
10
10
  });
11
11
 
12
12
  test("should generate correct Unicode IDs for TOC", () => {
13
- const post = getPostBySlug("multilingual-test");
13
+ const post = getPostBySlug("multilingual-test-中文长标题");
14
14
  expect(post).not.toBeNull();
15
15
 
16
16
  // Check headings
@@ -43,7 +43,7 @@ describe("Integration: Markdown Features", () => {
43
43
  });
44
44
 
45
45
  test("should correctly identify latex enabled posts", () => {
46
- const post = getPostBySlug("multilingual-test");
46
+ const post = getPostBySlug("multilingual-test-中文长标题");
47
47
  expect(post?.latex).toBe(true);
48
48
 
49
49
  const otherPost = getPostBySlug("hello-world"); // Assuming this doesn't exist or doesn't have latex
@@ -63,9 +63,9 @@ describe("Integration: Reading Time & Headings", () => {
63
63
  });
64
64
 
65
65
  test("multilingual post has headings with correct IDs", () => {
66
- const post = getPostBySlug("multilingual-test");
66
+ const post = getPostBySlug("multilingual-test-中文长标题");
67
67
  if (!post) {
68
- console.warn("Skipping: multilingual-test post not found");
68
+ console.warn("Skipping: multilingual-test-中文长标题 post not found");
69
69
  return;
70
70
  }
71
71