@decocms/apps-blog 8.0.0 → 8.1.0-next.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 (66) hide show
  1. package/README.md +21 -0
  2. package/package.json +14 -5
  3. package/src/README.md +69 -0
  4. package/src/__tests__/blogRecords.test.ts +270 -0
  5. package/src/__tests__/categoryTree.test.ts +209 -0
  6. package/src/__tests__/date.test.ts +134 -0
  7. package/src/__tests__/handlePosts.test.ts +256 -230
  8. package/src/__tests__/loaders.test.ts +198 -154
  9. package/src/__tests__/mod.test.ts +6 -6
  10. package/src/__tests__/publicationStatus.test.ts +176 -0
  11. package/src/__tests__/records.test.ts +68 -68
  12. package/src/__tests__/seoSections.test.ts +245 -0
  13. package/src/actions/submitRating.ts +36 -0
  14. package/src/actions/submitReview.ts +95 -0
  15. package/src/actions/submitView.ts +28 -0
  16. package/src/client.ts +70 -0
  17. package/src/core/blogRecords.ts +94 -0
  18. package/src/core/categoryTree.ts +210 -0
  19. package/src/core/handlePosts.ts +160 -64
  20. package/src/core/records.ts +147 -12
  21. package/src/index.ts +48 -14
  22. package/src/loaderMap.ts +28 -24
  23. package/src/loaders/BlogPostItem.ts +15 -5
  24. package/src/loaders/BlogPostPage.ts +61 -24
  25. package/src/loaders/BlogRelatedPosts.ts +45 -45
  26. package/src/loaders/BlogpostList.ts +93 -0
  27. package/src/loaders/BlogpostListing.ts +151 -85
  28. package/src/loaders/GetCategories.ts +51 -28
  29. package/src/loaders/extensions/BlogpostList/ratings.ts +36 -0
  30. package/src/loaders/extensions/BlogpostList/reviews.ts +36 -0
  31. package/src/loaders/extensions/BlogpostListing/ratings.ts +38 -0
  32. package/src/loaders/extensions/BlogpostListing/reviews.ts +38 -0
  33. package/src/loaders/extensions/BlogpostPage/ratings.ts +24 -0
  34. package/src/loaders/extensions/BlogpostPage/reviews.ts +31 -0
  35. package/src/loaders/extensions/types.ts +15 -0
  36. package/src/manifest.gen.ts +82 -14
  37. package/src/mod.ts +73 -20
  38. package/src/registry.ts +5 -5
  39. package/src/sections/Seo/SeoBlogPost.tsx +77 -0
  40. package/src/sections/Seo/SeoBlogPostListing.tsx +80 -0
  41. package/src/sections/Template.tsx +106 -0
  42. package/src/sections/blocks/BlockImage.tsx +42 -0
  43. package/src/sections/blocks/Callout.tsx +64 -0
  44. package/src/sections/blocks/CardGroup.tsx +60 -0
  45. package/src/sections/blocks/Checklist.tsx +58 -0
  46. package/src/sections/blocks/Code.tsx +29 -0
  47. package/src/sections/blocks/Comparison.tsx +77 -0
  48. package/src/sections/blocks/Cta.tsx +22 -0
  49. package/src/sections/blocks/Divider.tsx +3 -0
  50. package/src/sections/blocks/Heading.tsx +28 -0
  51. package/src/sections/blocks/List.tsx +39 -0
  52. package/src/sections/blocks/Paragraph.tsx +15 -0
  53. package/src/sections/blocks/Quote.tsx +32 -0
  54. package/src/sections/blocks/Stat.tsx +23 -0
  55. package/src/sections/blocks/StatGroup.tsx +38 -0
  56. package/src/sections/blocks/Steps.tsx +64 -0
  57. package/src/sections/blocks/Table.tsx +80 -0
  58. package/src/sections/blocks/Video.tsx +39 -0
  59. package/src/static/css.ts +178 -0
  60. package/src/types.ts +333 -77
  61. package/src/utils/blocksToSections.ts +175 -0
  62. package/src/utils/constants.ts +1 -0
  63. package/src/utils/date.ts +95 -0
  64. package/src/utils/hardSanitize.ts +106 -0
  65. package/src/utils/jsonLD.ts +118 -0
  66. package/src/utils/sanitizeHtml.ts +37 -0
@@ -1,285 +1,311 @@
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
+
127
+ /**
128
+ * A parent category lists its descendants' posts by expanding its slug into
129
+ * the whole subtree, so the filter has to match on any slug of a list.
130
+ */
131
+ it("matches a post in any slug of a list", () => {
132
+ const inCategory = (slug: string) =>
133
+ makePost({ slug: `post-${slug}`, categories: [{ name: slug, slug }] });
134
+ const tree = [inCategory("root"), inCategory("child"), inCategory("other")];
135
+
136
+ expect(filterPostsByCategory(tree, ["root", "child"]).map((p) => p.slug)).toEqual([
137
+ "post-root",
138
+ "post-child",
139
+ ]);
140
+ expect(filterPostsByCategory(tree, "child").map((p) => p.slug)).toEqual(["post-child"]);
141
+ // No slug at all means no category filter, same as before.
142
+ expect(filterPostsByCategory(tree)).toHaveLength(3);
143
+ // An empty list is an explicit empty set of categories, which nothing
144
+ // belongs to — diverges from upstream, which answers every post here.
145
+ expect(filterPostsByCategory(tree, [])).toEqual([]);
146
+ });
126
147
  });
127
148
 
128
149
  // ---------------------------------------------------------------------------
129
150
  // filterPostsBySlugs
130
151
  // ---------------------------------------------------------------------------
131
152
  describe("filterPostsBySlugs", () => {
132
- const posts = [makePost({ slug: "a" }), makePost({ slug: "b" }), makePost({ slug: "c" })];
153
+ const posts = [makePost({ slug: "a" }), makePost({ slug: "b" }), makePost({ slug: "c" })];
133
154
 
134
- it("filters to only matching slugs", () => {
135
- expect(filterPostsBySlugs(posts, ["a", "c"]).map((p) => p.slug)).toEqual(["a", "c"]);
136
- });
155
+ it("filters to only matching slugs", () => {
156
+ expect(filterPostsBySlugs(posts, ["a", "c"]).map((p) => p.slug)).toEqual(["a", "c"]);
157
+ });
137
158
 
138
- it("returns empty for empty slugs array", () => {
139
- expect(filterPostsBySlugs(posts, [])).toEqual([]);
140
- });
159
+ it("returns empty for empty slugs array", () => {
160
+ expect(filterPostsBySlugs(posts, [])).toEqual([]);
161
+ });
141
162
  });
142
163
 
143
164
  // ---------------------------------------------------------------------------
144
165
  // filterPostsByTerm
145
166
  // ---------------------------------------------------------------------------
146
167
  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
- });
168
+ const posts = [
169
+ makePost({ slug: "a", title: "Hello World", excerpt: "intro", content: "body" }),
170
+ makePost({ slug: "b", title: "Goodbye", excerpt: "farewell world", content: "end" }),
171
+ makePost({ slug: "c", title: "Nothing", excerpt: "nope", content: "empty" }),
172
+ ];
173
+
174
+ it("matches in title", () => {
175
+ expect(filterPostsByTerm(posts, "Hello").map((p) => p.slug)).toEqual(["a"]);
176
+ });
177
+
178
+ it("matches in excerpt", () => {
179
+ expect(filterPostsByTerm(posts, "farewell").map((p) => p.slug)).toEqual(["b"]);
180
+ });
181
+
182
+ it("matches in content", () => {
183
+ expect(filterPostsByTerm(posts, "empty").map((p) => p.slug)).toEqual(["c"]);
184
+ });
185
+
186
+ it("is case-insensitive", () => {
187
+ expect(filterPostsByTerm(posts, "hELLo")).toHaveLength(1);
188
+ });
189
+
190
+ it("returns empty when no match", () => {
191
+ expect(filterPostsByTerm(posts, "zzz")).toEqual([]);
192
+ });
172
193
  });
173
194
 
174
195
  // ---------------------------------------------------------------------------
175
196
  // filterRelatedPosts
176
197
  // ---------------------------------------------------------------------------
177
198
  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
- });
199
+ const posts = [
200
+ makePost({ slug: "a", categories: [{ name: "News", slug: "news" }] }),
201
+ makePost({ slug: "b", categories: [{ name: "Tech", slug: "tech" }] }),
202
+ makePost({ slug: "c", categories: undefined }),
203
+ ];
204
+
205
+ it("includes post with category overlap", () => {
206
+ expect(filterRelatedPosts(posts, ["news"]).map((p) => p.slug)).toEqual(["a"]);
207
+ });
208
+
209
+ it("excludes post with no overlap", () => {
210
+ expect(filterRelatedPosts(posts, ["sports"])).toEqual([]);
211
+ });
212
+
213
+ it("excludes post with no categories", () => {
214
+ const result = filterRelatedPosts(posts, ["news"]);
215
+ expect(result.find((p) => p.slug === "c")).toBeUndefined();
216
+ });
217
+
218
+ // Opposite of filterPostsByCategory, which reads an empty list as "no filter".
219
+ it("relates nothing on an empty slug list", () => {
220
+ expect(filterRelatedPosts(posts, [])).toEqual([]);
221
+ });
196
222
  });
197
223
 
198
224
  // ---------------------------------------------------------------------------
199
225
  // slicePosts
200
226
  // ---------------------------------------------------------------------------
201
227
  describe("slicePosts", () => {
202
- const posts = Array.from({ length: 5 }, (_, i) => makePost({ slug: `p${i + 1}` }));
228
+ const posts = Array.from({ length: 5 }, (_, i) => makePost({ slug: `p${i + 1}` }));
203
229
 
204
- it("page 1 returns first N", () => {
205
- expect(slicePosts(posts, 1, 2).map((p) => p.slug)).toEqual(["p1", "p2"]);
206
- });
230
+ it("page 1 returns first N", () => {
231
+ expect(slicePosts(posts, 1, 2).map((p) => p.slug)).toEqual(["p1", "p2"]);
232
+ });
207
233
 
208
- it("page 2 returns next N", () => {
209
- expect(slicePosts(posts, 2, 2).map((p) => p.slug)).toEqual(["p3", "p4"]);
210
- });
234
+ it("page 2 returns next N", () => {
235
+ expect(slicePosts(posts, 2, 2).map((p) => p.slug)).toEqual(["p3", "p4"]);
236
+ });
211
237
 
212
- it("last page with fewer items", () => {
213
- expect(slicePosts(posts, 3, 2).map((p) => p.slug)).toEqual(["p5"]);
214
- });
238
+ it("last page with fewer items", () => {
239
+ expect(slicePosts(posts, 3, 2).map((p) => p.slug)).toEqual(["p5"]);
240
+ });
215
241
 
216
- it("page beyond total returns empty", () => {
217
- expect(slicePosts(posts, 10, 2)).toEqual([]);
218
- });
242
+ it("page beyond total returns empty", () => {
243
+ expect(slicePosts(posts, 10, 2)).toEqual([]);
244
+ });
219
245
  });
220
246
 
221
247
  // ---------------------------------------------------------------------------
222
248
  // handlePosts (composition pipeline)
223
249
  // ---------------------------------------------------------------------------
224
250
  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
- });
251
+ const posts = [
252
+ makePost({
253
+ slug: "a",
254
+ title: "Alpha",
255
+ date: "2024-01-01",
256
+ categories: [{ name: "News", slug: "news" }],
257
+ }),
258
+ makePost({
259
+ slug: "b",
260
+ title: "Bravo",
261
+ date: "2024-02-01",
262
+ categories: [{ name: "Tech", slug: "tech" }],
263
+ }),
264
+ makePost({
265
+ slug: "c",
266
+ title: "Charlie",
267
+ date: "2024-03-01",
268
+ categories: [
269
+ { name: "News", slug: "news" },
270
+ { name: "Tech", slug: "tech" },
271
+ ],
272
+ }),
273
+ ];
274
+
275
+ it("slug string + postSlugs — uses slug filtering", async () => {
276
+ const result = await handlePosts(posts, "date_desc", "news", ["a", "c"]);
277
+ // sorted by date_desc: c (March) before a (Jan)
278
+ expect(result?.map((p) => p.slug)).toEqual(["c", "a"]);
279
+ });
280
+
281
+ it("slug string + term — chains category + term filters", async () => {
282
+ const result = await handlePosts(posts, "date_desc", "news", undefined, "Alpha");
283
+ expect(result?.map((p) => p.slug)).toEqual(["a"]);
284
+ });
285
+
286
+ it("slug as string[] — triggers related posts path", async () => {
287
+ const result = await handlePosts(posts, "date_desc", ["tech"]);
288
+ expect(result?.map((p) => p.slug)).toEqual(["c", "b"]);
289
+ });
290
+
291
+ it("no slug + term — term-only filtering", async () => {
292
+ const result = await handlePosts(posts, "date_asc", undefined, undefined, "Bravo");
293
+ expect(result).toHaveLength(1);
294
+ expect(result![0].slug).toBe("b");
295
+ });
296
+
297
+ it("excludePostSlug removes matching post", async () => {
298
+ const result = await handlePosts(posts, "date_desc", undefined, undefined, undefined, "b");
299
+ expect(result?.find((p) => p.slug === "b")).toBeUndefined();
300
+ });
301
+
302
+ it("returns null when result is empty", async () => {
303
+ expect(await handlePosts(posts, "date_desc", "nonexistent")).toBeNull();
304
+ });
305
+
306
+ it("returns all posts sorted when no filters given", async () => {
307
+ const result = await handlePosts(posts, "date_desc");
308
+ expect(result).toHaveLength(3);
309
+ expect(result![0].slug).toBe("c");
310
+ });
285
311
  });