@decocms/apps-blog 7.52.1 → 7.53.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 (63) hide show
  1. package/package.json +13 -4
  2. package/src/README.md +69 -0
  3. package/src/__tests__/blogRecords.test.ts +270 -0
  4. package/src/__tests__/date.test.ts +134 -0
  5. package/src/__tests__/handlePosts.test.ts +230 -230
  6. package/src/__tests__/loaders.test.ts +154 -154
  7. package/src/__tests__/mod.test.ts +6 -6
  8. package/src/__tests__/publicationStatus.test.ts +176 -0
  9. package/src/__tests__/records.test.ts +68 -68
  10. package/src/__tests__/seoSections.test.ts +245 -0
  11. package/src/actions/submitRating.ts +36 -0
  12. package/src/actions/submitReview.ts +95 -0
  13. package/src/actions/submitView.ts +28 -0
  14. package/src/client.ts +69 -0
  15. package/src/core/blogRecords.ts +94 -0
  16. package/src/core/handlePosts.ts +154 -63
  17. package/src/core/records.ts +147 -12
  18. package/src/index.ts +48 -14
  19. package/src/loaderMap.ts +28 -24
  20. package/src/loaders/BlogPostItem.ts +15 -5
  21. package/src/loaders/BlogPostPage.ts +25 -20
  22. package/src/loaders/BlogRelatedPosts.ts +45 -45
  23. package/src/loaders/BlogpostList.ts +73 -0
  24. package/src/loaders/BlogpostListing.ts +114 -77
  25. package/src/loaders/GetCategories.ts +42 -28
  26. package/src/loaders/extensions/BlogpostList/ratings.ts +36 -0
  27. package/src/loaders/extensions/BlogpostList/reviews.ts +36 -0
  28. package/src/loaders/extensions/BlogpostListing/ratings.ts +38 -0
  29. package/src/loaders/extensions/BlogpostListing/reviews.ts +38 -0
  30. package/src/loaders/extensions/BlogpostPage/ratings.ts +24 -0
  31. package/src/loaders/extensions/BlogpostPage/reviews.ts +31 -0
  32. package/src/loaders/extensions/types.ts +15 -0
  33. package/src/manifest.gen.ts +82 -14
  34. package/src/mod.ts +71 -20
  35. package/src/registry.ts +5 -5
  36. package/src/sections/Seo/SeoBlogPost.tsx +75 -0
  37. package/src/sections/Seo/SeoBlogPostListing.tsx +80 -0
  38. package/src/sections/Template.tsx +96 -0
  39. package/src/sections/blocks/BlockImage.tsx +21 -0
  40. package/src/sections/blocks/Callout.tsx +64 -0
  41. package/src/sections/blocks/CardGroup.tsx +60 -0
  42. package/src/sections/blocks/Checklist.tsx +58 -0
  43. package/src/sections/blocks/Code.tsx +29 -0
  44. package/src/sections/blocks/Comparison.tsx +77 -0
  45. package/src/sections/blocks/Cta.tsx +22 -0
  46. package/src/sections/blocks/Divider.tsx +3 -0
  47. package/src/sections/blocks/Heading.tsx +28 -0
  48. package/src/sections/blocks/List.tsx +39 -0
  49. package/src/sections/blocks/Paragraph.tsx +15 -0
  50. package/src/sections/blocks/Quote.tsx +32 -0
  51. package/src/sections/blocks/Stat.tsx +23 -0
  52. package/src/sections/blocks/StatGroup.tsx +38 -0
  53. package/src/sections/blocks/Steps.tsx +64 -0
  54. package/src/sections/blocks/Table.tsx +80 -0
  55. package/src/sections/blocks/Video.tsx +39 -0
  56. package/src/static/css.ts +178 -0
  57. package/src/types.ts +312 -77
  58. package/src/utils/blocksToSections.ts +170 -0
  59. package/src/utils/constants.ts +1 -0
  60. package/src/utils/date.ts +95 -0
  61. package/src/utils/hardSanitize.ts +106 -0
  62. package/src/utils/jsonLD.ts +115 -0
  63. package/src/utils/sanitizeHtml.ts +37 -0
@@ -1,285 +1,285 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import handlePosts, {
3
- filterPostsByCategory,
4
- filterPostsBySlugs,
5
- filterPostsByTerm,
6
- filterRelatedPosts,
7
- slicePosts,
8
- sortPosts,
3
+ filterPostsByCategory,
4
+ filterPostsBySlugs,
5
+ filterPostsByTerm,
6
+ filterRelatedPosts,
7
+ slicePosts,
8
+ sortPosts,
9
9
  } from "../core/handlePosts";
10
10
  import type { BlogPost, SortBy } from "../types";
11
11
 
12
12
  function makePost(overrides: Partial<BlogPost> = {}): BlogPost {
13
- return {
14
- title: "Test Post",
15
- slug: "test-post",
16
- date: "2024-06-01",
17
- excerpt: "A test post",
18
- content: "Full content here",
19
- categories: [{ name: "News", slug: "news" }],
20
- authors: [{ name: "Author", email: "a@b.com" }],
21
- ...overrides,
22
- };
13
+ return {
14
+ title: "Test Post",
15
+ slug: "test-post",
16
+ date: "2024-06-01",
17
+ excerpt: "A test post",
18
+ content: "Full content here",
19
+ categories: [{ name: "News", slug: "news" }],
20
+ authors: [{ name: "Author", email: "a@b.com" }],
21
+ ...overrides,
22
+ };
23
23
  }
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // sortPosts
27
27
  // ---------------------------------------------------------------------------
28
28
  describe("sortPosts", () => {
29
- const posts = [
30
- makePost({ title: "Alpha", slug: "a", date: "2024-01-01" }),
31
- makePost({ title: "Charlie", slug: "c", date: "2024-03-01" }),
32
- makePost({ title: "Bravo", slug: "b", date: "2024-02-01" }),
33
- ];
34
-
35
- it("sorts date_desc — most recent first", () => {
36
- const sorted = sortPosts(posts, "date_desc");
37
- expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
38
- });
39
-
40
- it("sorts date_asc — oldest first", () => {
41
- const sorted = sortPosts(posts, "date_asc");
42
- expect(sorted.map((p) => p.slug)).toEqual(["a", "b", "c"]);
43
- });
44
-
45
- it("sorts title_asc", () => {
46
- const sorted = sortPosts(posts, "title_asc");
47
- // localeCompare(a,b) is negated for asc → Z-A
48
- expect(sorted.map((p) => p.title)).toEqual(["Charlie", "Bravo", "Alpha"]);
49
- });
50
-
51
- it("sorts title_desc", () => {
52
- const sorted = sortPosts(posts, "title_desc");
53
- // localeCompare(a,b) kept as-is for desc → A-Z
54
- expect(sorted.map((p) => p.title)).toEqual(["Alpha", "Bravo", "Charlie"]);
55
- });
56
-
57
- it("falls back to date for invalid sort field", () => {
58
- const sorted = sortPosts(posts, "invalid_desc" as SortBy);
59
- expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
60
- });
61
-
62
- it("falls back to desc for invalid sort order", () => {
63
- const sorted = sortPosts(posts, "date_wrong" as SortBy);
64
- expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
65
- });
66
-
67
- it("sorts posts with missing date to end", () => {
68
- const withMissing = [
69
- makePost({ slug: "no-date", date: undefined as unknown as string }),
70
- makePost({ slug: "has-date", date: "2024-06-01" }),
71
- ];
72
- const sorted = sortPosts(withMissing, "date_desc");
73
- expect(sorted[0].slug).toBe("has-date");
74
- expect(sorted[1].slug).toBe("no-date");
75
- });
76
-
77
- it("throws on empty array (accesses blogPosts[0])", () => {
78
- expect(() => sortPosts([], "date_desc")).toThrow();
79
- });
80
-
81
- it("does not mutate original array", () => {
82
- const original = [...posts];
83
- sortPosts(posts, "date_asc");
84
- expect(posts).toEqual(original);
85
- });
29
+ const posts = [
30
+ makePost({ title: "Alpha", slug: "a", date: "2024-01-01" }),
31
+ makePost({ title: "Charlie", slug: "c", date: "2024-03-01" }),
32
+ makePost({ title: "Bravo", slug: "b", date: "2024-02-01" }),
33
+ ];
34
+
35
+ it("sorts date_desc — most recent first", async () => {
36
+ const sorted = await sortPosts(posts, "date_desc");
37
+ expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
38
+ });
39
+
40
+ it("sorts date_asc — oldest first", async () => {
41
+ const sorted = await sortPosts(posts, "date_asc");
42
+ expect(sorted.map((p) => p.slug)).toEqual(["a", "b", "c"]);
43
+ });
44
+
45
+ it("sorts title_asc", async () => {
46
+ const sorted = await sortPosts(posts, "title_asc");
47
+ // localeCompare(a,b) is negated for asc → Z-A
48
+ expect(sorted.map((p) => p.title)).toEqual(["Charlie", "Bravo", "Alpha"]);
49
+ });
50
+
51
+ it("sorts title_desc", async () => {
52
+ const sorted = await sortPosts(posts, "title_desc");
53
+ // localeCompare(a,b) kept as-is for desc → A-Z
54
+ expect(sorted.map((p) => p.title)).toEqual(["Alpha", "Bravo", "Charlie"]);
55
+ });
56
+
57
+ it("falls back to date for invalid sort field", async () => {
58
+ const sorted = await sortPosts(posts, "invalid_desc" as SortBy);
59
+ expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
60
+ });
61
+
62
+ it("falls back to desc for invalid sort order", async () => {
63
+ const sorted = await sortPosts(posts, "date_wrong" as SortBy);
64
+ expect(sorted.map((p) => p.slug)).toEqual(["c", "b", "a"]);
65
+ });
66
+
67
+ it("sorts posts with missing date to end", async () => {
68
+ const withMissing = [
69
+ makePost({ slug: "no-date", date: undefined as unknown as string }),
70
+ makePost({ slug: "has-date", date: "2024-06-01" }),
71
+ ];
72
+ const sorted = await sortPosts(withMissing, "date_desc");
73
+ expect(sorted[0].slug).toBe("has-date");
74
+ expect(sorted[1].slug).toBe("no-date");
75
+ });
76
+
77
+ it("throws on empty array (accesses blogPosts[0])", async () => {
78
+ await expect(sortPosts([], "date_desc")).rejects.toThrow();
79
+ });
80
+
81
+ it("does not mutate original array", async () => {
82
+ const original = [...posts];
83
+ await sortPosts(posts, "date_asc");
84
+ expect(posts).toEqual(original);
85
+ });
86
86
  });
87
87
 
88
88
  // ---------------------------------------------------------------------------
89
89
  // filterPostsByCategory
90
90
  // ---------------------------------------------------------------------------
91
91
  describe("filterPostsByCategory", () => {
92
- const posts = [
93
- makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
94
- makePost({
95
- slug: "b",
96
- categories: [
97
- { name: "News", slug: "news" },
98
- { name: "Tech", slug: "tech" },
99
- ],
100
- }),
101
- makePost({ slug: "c", categories: [{ name: "Tech", slug: "tech" }] }),
102
- ];
103
-
104
- it("returns all posts when no slug provided", () => {
105
- expect(filterPostsByCategory(posts)).toHaveLength(3);
106
- });
107
-
108
- it("filters by matching slug", () => {
109
- const result = filterPostsByCategory(posts, "tech");
110
- expect(result.map((p) => p.slug)).toEqual(["b", "c"]);
111
- });
112
-
113
- it("returns empty when no match", () => {
114
- expect(filterPostsByCategory(posts, "sports")).toEqual([]);
115
- });
116
-
117
- it("excludes posts with no categories", () => {
118
- const withNone = [...posts, makePost({ slug: "d", categories: undefined })];
119
- expect(filterPostsByCategory(withNone, "news").map((p) => p.slug)).toEqual(["a", "b"]);
120
- });
121
-
122
- it("includes post when one of multiple categories matches", () => {
123
- const result = filterPostsByCategory(posts, "tech");
124
- expect(result.find((p) => p.slug === "b")).toBeTruthy();
125
- });
92
+ const posts = [
93
+ makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
94
+ makePost({
95
+ slug: "b",
96
+ categories: [
97
+ { name: "News", slug: "news" },
98
+ { name: "Tech", slug: "tech" },
99
+ ],
100
+ }),
101
+ makePost({ slug: "c", categories: [{ name: "Tech", slug: "tech" }] }),
102
+ ];
103
+
104
+ it("returns all posts when no slug provided", () => {
105
+ expect(filterPostsByCategory(posts)).toHaveLength(3);
106
+ });
107
+
108
+ it("filters by matching slug", () => {
109
+ const result = filterPostsByCategory(posts, "tech");
110
+ expect(result.map((p) => p.slug)).toEqual(["b", "c"]);
111
+ });
112
+
113
+ it("returns empty when no match", () => {
114
+ expect(filterPostsByCategory(posts, "sports")).toEqual([]);
115
+ });
116
+
117
+ it("excludes posts with no categories", () => {
118
+ const withNone = [...posts, makePost({ slug: "d", categories: undefined })];
119
+ expect(filterPostsByCategory(withNone, "news").map((p) => p.slug)).toEqual(["a", "b"]);
120
+ });
121
+
122
+ it("includes post when one of multiple categories matches", () => {
123
+ const result = filterPostsByCategory(posts, "tech");
124
+ expect(result.find((p) => p.slug === "b")).toBeTruthy();
125
+ });
126
126
  });
127
127
 
128
128
  // ---------------------------------------------------------------------------
129
129
  // filterPostsBySlugs
130
130
  // ---------------------------------------------------------------------------
131
131
  describe("filterPostsBySlugs", () => {
132
- const posts = [makePost({ slug: "a" }), makePost({ slug: "b" }), makePost({ slug: "c" })];
132
+ const posts = [makePost({ slug: "a" }), makePost({ slug: "b" }), makePost({ slug: "c" })];
133
133
 
134
- it("filters to only matching slugs", () => {
135
- expect(filterPostsBySlugs(posts, ["a", "c"]).map((p) => p.slug)).toEqual(["a", "c"]);
136
- });
134
+ it("filters to only matching slugs", () => {
135
+ expect(filterPostsBySlugs(posts, ["a", "c"]).map((p) => p.slug)).toEqual(["a", "c"]);
136
+ });
137
137
 
138
- it("returns empty for empty slugs array", () => {
139
- expect(filterPostsBySlugs(posts, [])).toEqual([]);
140
- });
138
+ it("returns empty for empty slugs array", () => {
139
+ expect(filterPostsBySlugs(posts, [])).toEqual([]);
140
+ });
141
141
  });
142
142
 
143
143
  // ---------------------------------------------------------------------------
144
144
  // filterPostsByTerm
145
145
  // ---------------------------------------------------------------------------
146
146
  describe("filterPostsByTerm", () => {
147
- const posts = [
148
- makePost({ slug: "a", title: "Hello World", excerpt: "intro", content: "body" }),
149
- makePost({ slug: "b", title: "Goodbye", excerpt: "farewell world", content: "end" }),
150
- makePost({ slug: "c", title: "Nothing", excerpt: "nope", content: "empty" }),
151
- ];
152
-
153
- it("matches in title", () => {
154
- expect(filterPostsByTerm(posts, "Hello").map((p) => p.slug)).toEqual(["a"]);
155
- });
156
-
157
- it("matches in excerpt", () => {
158
- expect(filterPostsByTerm(posts, "farewell").map((p) => p.slug)).toEqual(["b"]);
159
- });
160
-
161
- it("matches in content", () => {
162
- expect(filterPostsByTerm(posts, "empty").map((p) => p.slug)).toEqual(["c"]);
163
- });
164
-
165
- it("is case-insensitive", () => {
166
- expect(filterPostsByTerm(posts, "hELLo")).toHaveLength(1);
167
- });
168
-
169
- it("returns empty when no match", () => {
170
- expect(filterPostsByTerm(posts, "zzz")).toEqual([]);
171
- });
147
+ const posts = [
148
+ makePost({ slug: "a", title: "Hello World", excerpt: "intro", content: "body" }),
149
+ makePost({ slug: "b", title: "Goodbye", excerpt: "farewell world", content: "end" }),
150
+ makePost({ slug: "c", title: "Nothing", excerpt: "nope", content: "empty" }),
151
+ ];
152
+
153
+ it("matches in title", () => {
154
+ expect(filterPostsByTerm(posts, "Hello").map((p) => p.slug)).toEqual(["a"]);
155
+ });
156
+
157
+ it("matches in excerpt", () => {
158
+ expect(filterPostsByTerm(posts, "farewell").map((p) => p.slug)).toEqual(["b"]);
159
+ });
160
+
161
+ it("matches in content", () => {
162
+ expect(filterPostsByTerm(posts, "empty").map((p) => p.slug)).toEqual(["c"]);
163
+ });
164
+
165
+ it("is case-insensitive", () => {
166
+ expect(filterPostsByTerm(posts, "hELLo")).toHaveLength(1);
167
+ });
168
+
169
+ it("returns empty when no match", () => {
170
+ expect(filterPostsByTerm(posts, "zzz")).toEqual([]);
171
+ });
172
172
  });
173
173
 
174
174
  // ---------------------------------------------------------------------------
175
175
  // filterRelatedPosts
176
176
  // ---------------------------------------------------------------------------
177
177
  describe("filterRelatedPosts", () => {
178
- const posts = [
179
- makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
180
- makePost({ slug: "b", categories: [{ name: "Tech", slug: "tech" }] }),
181
- makePost({ slug: "c", categories: undefined }),
182
- ];
183
-
184
- it("includes post with category overlap", () => {
185
- expect(filterRelatedPosts(posts, ["news"]).map((p) => p.slug)).toEqual(["a"]);
186
- });
187
-
188
- it("excludes post with no overlap", () => {
189
- expect(filterRelatedPosts(posts, ["sports"])).toEqual([]);
190
- });
191
-
192
- it("excludes post with no categories", () => {
193
- const result = filterRelatedPosts(posts, ["news"]);
194
- expect(result.find((p) => p.slug === "c")).toBeUndefined();
195
- });
178
+ const posts = [
179
+ makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
180
+ makePost({ slug: "b", categories: [{ name: "Tech", slug: "tech" }] }),
181
+ makePost({ slug: "c", categories: undefined }),
182
+ ];
183
+
184
+ it("includes post with category overlap", () => {
185
+ expect(filterRelatedPosts(posts, ["news"]).map((p) => p.slug)).toEqual(["a"]);
186
+ });
187
+
188
+ it("excludes post with no overlap", () => {
189
+ expect(filterRelatedPosts(posts, ["sports"])).toEqual([]);
190
+ });
191
+
192
+ it("excludes post with no categories", () => {
193
+ const result = filterRelatedPosts(posts, ["news"]);
194
+ expect(result.find((p) => p.slug === "c")).toBeUndefined();
195
+ });
196
196
  });
197
197
 
198
198
  // ---------------------------------------------------------------------------
199
199
  // slicePosts
200
200
  // ---------------------------------------------------------------------------
201
201
  describe("slicePosts", () => {
202
- const posts = Array.from({ length: 5 }, (_, i) => makePost({ slug: `p${i + 1}` }));
202
+ const posts = Array.from({ length: 5 }, (_, i) => makePost({ slug: `p${i + 1}` }));
203
203
 
204
- it("page 1 returns first N", () => {
205
- expect(slicePosts(posts, 1, 2).map((p) => p.slug)).toEqual(["p1", "p2"]);
206
- });
204
+ it("page 1 returns first N", () => {
205
+ expect(slicePosts(posts, 1, 2).map((p) => p.slug)).toEqual(["p1", "p2"]);
206
+ });
207
207
 
208
- it("page 2 returns next N", () => {
209
- expect(slicePosts(posts, 2, 2).map((p) => p.slug)).toEqual(["p3", "p4"]);
210
- });
208
+ it("page 2 returns next N", () => {
209
+ expect(slicePosts(posts, 2, 2).map((p) => p.slug)).toEqual(["p3", "p4"]);
210
+ });
211
211
 
212
- it("last page with fewer items", () => {
213
- expect(slicePosts(posts, 3, 2).map((p) => p.slug)).toEqual(["p5"]);
214
- });
212
+ it("last page with fewer items", () => {
213
+ expect(slicePosts(posts, 3, 2).map((p) => p.slug)).toEqual(["p5"]);
214
+ });
215
215
 
216
- it("page beyond total returns empty", () => {
217
- expect(slicePosts(posts, 10, 2)).toEqual([]);
218
- });
216
+ it("page beyond total returns empty", () => {
217
+ expect(slicePosts(posts, 10, 2)).toEqual([]);
218
+ });
219
219
  });
220
220
 
221
221
  // ---------------------------------------------------------------------------
222
222
  // handlePosts (composition pipeline)
223
223
  // ---------------------------------------------------------------------------
224
224
  describe("handlePosts", () => {
225
- const posts = [
226
- makePost({
227
- slug: "a",
228
- title: "Alpha",
229
- date: "2024-01-01",
230
- categories: [{ name: "News", slug: "news" }],
231
- }),
232
- makePost({
233
- slug: "b",
234
- title: "Bravo",
235
- date: "2024-02-01",
236
- categories: [{ name: "Tech", slug: "tech" }],
237
- }),
238
- makePost({
239
- slug: "c",
240
- title: "Charlie",
241
- date: "2024-03-01",
242
- categories: [
243
- { name: "News", slug: "news" },
244
- { name: "Tech", slug: "tech" },
245
- ],
246
- }),
247
- ];
248
-
249
- it("slug string + postSlugs — uses slug filtering", () => {
250
- const result = handlePosts(posts, "date_desc", "news", ["a", "c"]);
251
- // sorted by date_desc: c (March) before a (Jan)
252
- expect(result?.map((p) => p.slug)).toEqual(["c", "a"]);
253
- });
254
-
255
- it("slug string + term — chains category + term filters", () => {
256
- const result = handlePosts(posts, "date_desc", "news", undefined, "Alpha");
257
- expect(result?.map((p) => p.slug)).toEqual(["a"]);
258
- });
259
-
260
- it("slug as string[] — triggers related posts path", () => {
261
- const result = handlePosts(posts, "date_desc", ["tech"]);
262
- expect(result?.map((p) => p.slug)).toEqual(["c", "b"]);
263
- });
264
-
265
- it("no slug + term — term-only filtering", () => {
266
- const result = handlePosts(posts, "date_asc", undefined, undefined, "Bravo");
267
- expect(result).toHaveLength(1);
268
- expect(result![0].slug).toBe("b");
269
- });
270
-
271
- it("excludePostSlug removes matching post", () => {
272
- const result = handlePosts(posts, "date_desc", undefined, undefined, undefined, "b");
273
- expect(result?.find((p) => p.slug === "b")).toBeUndefined();
274
- });
275
-
276
- it("returns null when result is empty", () => {
277
- expect(handlePosts(posts, "date_desc", "nonexistent")).toBeNull();
278
- });
279
-
280
- it("returns all posts sorted when no filters given", () => {
281
- const result = handlePosts(posts, "date_desc");
282
- expect(result).toHaveLength(3);
283
- expect(result![0].slug).toBe("c");
284
- });
225
+ const posts = [
226
+ makePost({
227
+ slug: "a",
228
+ title: "Alpha",
229
+ date: "2024-01-01",
230
+ categories: [{ name: "News", slug: "news" }],
231
+ }),
232
+ makePost({
233
+ slug: "b",
234
+ title: "Bravo",
235
+ date: "2024-02-01",
236
+ categories: [{ name: "Tech", slug: "tech" }],
237
+ }),
238
+ makePost({
239
+ slug: "c",
240
+ title: "Charlie",
241
+ date: "2024-03-01",
242
+ categories: [
243
+ { name: "News", slug: "news" },
244
+ { name: "Tech", slug: "tech" },
245
+ ],
246
+ }),
247
+ ];
248
+
249
+ it("slug string + postSlugs — uses slug filtering", async () => {
250
+ const result = await handlePosts(posts, "date_desc", "news", ["a", "c"]);
251
+ // sorted by date_desc: c (March) before a (Jan)
252
+ expect(result?.map((p) => p.slug)).toEqual(["c", "a"]);
253
+ });
254
+
255
+ it("slug string + term — chains category + term filters", async () => {
256
+ const result = await handlePosts(posts, "date_desc", "news", undefined, "Alpha");
257
+ expect(result?.map((p) => p.slug)).toEqual(["a"]);
258
+ });
259
+
260
+ it("slug as string[] — triggers related posts path", async () => {
261
+ const result = await handlePosts(posts, "date_desc", ["tech"]);
262
+ expect(result?.map((p) => p.slug)).toEqual(["c", "b"]);
263
+ });
264
+
265
+ it("no slug + term — term-only filtering", async () => {
266
+ const result = await handlePosts(posts, "date_asc", undefined, undefined, "Bravo");
267
+ expect(result).toHaveLength(1);
268
+ expect(result![0].slug).toBe("b");
269
+ });
270
+
271
+ it("excludePostSlug removes matching post", async () => {
272
+ const result = await handlePosts(posts, "date_desc", undefined, undefined, undefined, "b");
273
+ expect(result?.find((p) => p.slug === "b")).toBeUndefined();
274
+ });
275
+
276
+ it("returns null when result is empty", async () => {
277
+ expect(await handlePosts(posts, "date_desc", "nonexistent")).toBeNull();
278
+ });
279
+
280
+ it("returns all posts sorted when no filters given", async () => {
281
+ const result = await handlePosts(posts, "date_desc");
282
+ expect(result).toHaveLength(3);
283
+ expect(result![0].slug).toBe("c");
284
+ });
285
285
  });