@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
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Scope: `utils/date.ts` — the UTC pinning that makes post ordering and
3
+ * scheduled go-live instants independent of the server's timezone, plus
4
+ * `scheduledTime`'s strict parse.
5
+ *
6
+ * The timezone cases below are the point of the module. They are written by
7
+ * comparing timestamps rather than by mutating `process.env.TZ` mid-run, which
8
+ * V8 caches per-isolate and would not reliably take effect; the repo-level
9
+ * check is `TZ=... bun run test` producing identical output, and the assertions
10
+ * here encode the invariant that makes that true.
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+ import { sortPosts } from "../core/handlePosts";
15
+ import type { BlogPost } from "../types";
16
+ import { dateToTime, scheduledTime } from "../utils/date";
17
+
18
+ const post = (slug: string, date: string): BlogPost => ({
19
+ title: slug,
20
+ slug,
21
+ date,
22
+ excerpt: "",
23
+ });
24
+
25
+ describe("dateToTime", () => {
26
+ it("reads a bare date as midnight UTC, not midnight local", () => {
27
+ expect(dateToTime("2024-06-01")).toBe(Date.UTC(2024, 5, 1));
28
+ });
29
+
30
+ it("pins an offset-less datetime to UTC", () => {
31
+ // Plain `new Date("2024-06-01T00:00:00")` is *local* time — in any zone
32
+ // west of UTC that lands on May 31st, reordering posts across a day
33
+ // boundary depending on which server answered the request.
34
+ expect(dateToTime("2024-06-01T00:00:00")).toBe(Date.UTC(2024, 5, 1));
35
+ });
36
+
37
+ it("honours an explicit offset instead of overriding it", () => {
38
+ expect(dateToTime("2024-06-01T00:00:00-03:00")).toBe(Date.UTC(2024, 5, 1, 3));
39
+ });
40
+
41
+ it("returns 0 rather than NaN for an unparseable value", () => {
42
+ expect(dateToTime("not a date")).toBe(0);
43
+ });
44
+
45
+ it("orders two dates one day apart identically regardless of host offset", () => {
46
+ // Both values are absolute instants, so the comparison cannot shift with
47
+ // the host timezone — this is the property the sort comparator relies on.
48
+ expect(dateToTime("2024-06-02") - dateToTime("2024-06-01")).toBe(24 * 60 * 60 * 1000);
49
+ });
50
+ });
51
+
52
+ describe("sortPosts date ordering is timezone-independent", () => {
53
+ it("keeps day-boundary dates in order", async () => {
54
+ const posts = [
55
+ post("may-31", "2024-05-31"),
56
+ post("jun-02", "2024-06-02"),
57
+ post("jun-01", "2024-06-01"),
58
+ ];
59
+ const sorted = await sortPosts(posts, "date_desc");
60
+ expect(sorted.map((p) => p.slug)).toEqual(["jun-02", "jun-01", "may-31"]);
61
+ });
62
+
63
+ it("does not reorder datetimes that differ only by their offset", async () => {
64
+ // Same instant expressed two ways; neither may sort ahead of the other by
65
+ // accident of the host zone.
66
+ const posts = [
67
+ post("utc", "2024-06-01T03:00:00Z"),
68
+ post("offset", "2024-06-01T00:00:00-03:00"),
69
+ ];
70
+ const sorted = await sortPosts(posts, "date_desc");
71
+ expect(dateToTime(sorted[0].date)).toBe(dateToTime(sorted[1].date));
72
+ });
73
+
74
+ it("orders a mix of bare dates and full ISO timestamps correctly", async () => {
75
+ // The concrete regression this module fixes: the previous implementation
76
+ // built its timestamp as `new Date(\`${date}T00:00:00\`)`, which yields
77
+ // `Invalid Date` → NaN for any value that already carries a time. A NaN
78
+ // comparison is treated as 0, so every full-ISO post compared "equal" to
79
+ // everything and the listing order became arbitrary.
80
+ const posts = [
81
+ post("bare-old", "2024-01-01"),
82
+ post("iso-newest", "2024-12-01T09:30:00Z"),
83
+ post("bare-mid", "2024-06-01"),
84
+ post("iso-offset", "2024-03-01T00:00:00-03:00"),
85
+ ];
86
+ const sorted = await sortPosts(posts, "date_desc");
87
+ expect(sorted.map((p) => p.slug)).toEqual(["iso-newest", "bare-mid", "iso-offset", "bare-old"]);
88
+ });
89
+ });
90
+
91
+ describe("scheduledTime", () => {
92
+ it("accepts a bare date as midnight UTC", () => {
93
+ expect(scheduledTime("2026-09-01")).toBe(Date.UTC(2026, 8, 1));
94
+ });
95
+
96
+ it("accepts a full ISO instant, with or without an offset", () => {
97
+ expect(scheduledTime("2026-09-01T12:30:00Z")).toBe(Date.UTC(2026, 8, 1, 12, 30));
98
+ expect(scheduledTime("2026-09-01T12:30:00-03:00")).toBe(Date.UTC(2026, 8, 1, 15, 30));
99
+ });
100
+
101
+ it("rejects a non-ISO string that `Date` would happily parse in local time", () => {
102
+ expect(scheduledTime("Sep 1 2026")).toBeNull();
103
+ // "0" is the year 2000 to `Date` — i.e. already live.
104
+ expect(scheduledTime("0")).toBeNull();
105
+ expect(scheduledTime("")).toBeNull();
106
+ });
107
+
108
+ it("rejects calendar overflow instead of rolling it forward", () => {
109
+ expect(scheduledTime("2026-02-31")).toBeNull();
110
+ expect(scheduledTime("2026-13-01")).toBeNull();
111
+ expect(scheduledTime("2026-00-10")).toBeNull();
112
+ expect(scheduledTime("2026-01-00")).toBeNull();
113
+ });
114
+
115
+ it("accepts Feb 29 in a leap year and rejects it otherwise", () => {
116
+ expect(scheduledTime("2024-02-29")).toBe(Date.UTC(2024, 1, 29));
117
+ expect(scheduledTime("2026-02-29")).toBeNull();
118
+ });
119
+
120
+ it("rejects out-of-range times", () => {
121
+ expect(scheduledTime("2026-09-01T24:00:00")).toBeNull();
122
+ expect(scheduledTime("2026-09-01T12:60:00")).toBeNull();
123
+ expect(scheduledTime("2026-09-01T12:00:61")).toBeNull();
124
+ });
125
+
126
+ it("rejects an offset that matches the pattern but is not a real instant", () => {
127
+ expect(scheduledTime("2026-09-01T12:00:00+99:00")).toBeNull();
128
+ });
129
+
130
+ it("returns null, not 0, on failure — so the epoch stays representable", () => {
131
+ expect(scheduledTime("garbage")).toBeNull();
132
+ expect(scheduledTime("1970-01-01T00:00:00Z")).toBe(0);
133
+ });
134
+ });