@jant/core 0.3.23 → 0.3.24

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 (169) hide show
  1. package/dist/app.js +4 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +61 -72
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
@@ -3,12 +3,11 @@ import { getSiteName } from "../../lib/config.js";
3
3
  /**
4
4
  * Dashboard Pages Routes
5
5
  *
6
- * Management for custom pages (posts with type="page")
6
+ * Management for standalone pages (about, now, etc.)
7
7
  */ import { Hono } from "hono";
8
8
  import { useLingui as $_useLingui } from "@jant/core/i18n";
9
9
  import { DashLayout } from "../../theme/layouts/index.js";
10
- import { PageForm, VisibilityBadge, EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
11
- import * as sqid from "../../lib/sqid.js";
10
+ import { PageForm, EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
12
11
  import * as time from "../../lib/time.js";
13
12
  import { dsRedirect } from "../../lib/sse.js";
14
13
  export const pagesRoutes = new Hono();
@@ -41,32 +40,27 @@ function PagesListContent({ pages }) {
41
40
  class: "flex flex-col divide-y",
42
41
  children: pages.map((page)=>/*#__PURE__*/ _jsxs(ListItemRow, {
43
42
  actions: /*#__PURE__*/ _jsx(ActionButtons, {
44
- editHref: `/dash/pages/${sqid.encode(page.id)}/edit`,
43
+ editHref: `/dash/pages/${page.id}/edit`,
45
44
  editLabel: $__i18n._({
46
45
  id: "ePK91l",
47
46
  message: "Edit"
48
47
  }),
49
- viewHref: page.visibility !== "draft" && page.path ? `/${page.path}` : undefined,
48
+ viewHref: page.status !== "draft" ? `/${page.slug}` : undefined,
50
49
  viewLabel: $__i18n._({
51
50
  id: "jpctdh",
52
51
  message: "View"
53
52
  })
54
53
  }),
55
54
  children: [
56
- /*#__PURE__*/ _jsxs("div", {
55
+ /*#__PURE__*/ _jsx("div", {
57
56
  class: "flex items-center gap-2 mb-1",
58
- children: [
59
- /*#__PURE__*/ _jsx(VisibilityBadge, {
60
- visibility: page.visibility
61
- }),
62
- /*#__PURE__*/ _jsx("span", {
63
- class: "text-xs text-muted-foreground",
64
- children: time.formatDate(page.updatedAt)
65
- })
66
- ]
57
+ children: /*#__PURE__*/ _jsx("span", {
58
+ class: "text-xs text-muted-foreground",
59
+ children: time.formatDate(page.updatedAt)
60
+ })
67
61
  }),
68
62
  /*#__PURE__*/ _jsx("a", {
69
- href: `/dash/pages/${sqid.encode(page.id)}`,
63
+ href: `/dash/pages/${page.id}`,
70
64
  class: "font-medium hover:underline",
71
65
  children: page.title || $__i18n._({
72
66
  id: "wja8aL",
@@ -77,7 +71,7 @@ function PagesListContent({ pages }) {
77
71
  class: "text-sm text-muted-foreground mt-1",
78
72
  children: [
79
73
  "/",
80
- page.path
74
+ page.slug
81
75
  ]
82
76
  })
83
77
  ]
@@ -119,22 +113,22 @@ function ViewPageContent({ page }) {
119
113
  message: "Page"
120
114
  })
121
115
  }),
122
- page.path && /*#__PURE__*/ _jsxs("p", {
116
+ /*#__PURE__*/ _jsxs("p", {
123
117
  class: "text-muted-foreground mt-1",
124
118
  children: [
125
119
  "/",
126
- page.path
120
+ page.slug
127
121
  ]
128
122
  })
129
123
  ]
130
124
  }),
131
125
  /*#__PURE__*/ _jsx(ActionButtons, {
132
- editHref: `/dash/pages/${sqid.encode(page.id)}/edit`,
126
+ editHref: `/dash/pages/${page.id}/edit`,
133
127
  editLabel: $__i18n._({
134
128
  id: "ePK91l",
135
129
  message: "Edit"
136
130
  }),
137
- viewHref: page.visibility !== "draft" && page.path ? `/${page.path}` : undefined,
131
+ viewHref: page.status !== "draft" ? `/${page.slug}` : undefined,
138
132
  viewLabel: $__i18n._({
139
133
  id: "jpctdh",
140
134
  message: "View"
@@ -148,7 +142,7 @@ function ViewPageContent({ page }) {
148
142
  children: /*#__PURE__*/ _jsx("div", {
149
143
  class: "prose",
150
144
  dangerouslySetInnerHTML: {
151
- __html: page.contentHtml || ""
145
+ __html: page.bodyHtml || ""
152
146
  }
153
147
  })
154
148
  })
@@ -158,7 +152,7 @@ function ViewPageContent({ page }) {
158
152
  id: "4KzVT6",
159
153
  message: "Delete Page"
160
154
  }),
161
- formAction: `/dash/pages/${sqid.encode(page.id)}/delete`,
155
+ formAction: `/dash/pages/${page.id}/delete`,
162
156
  confirmMessage: "Are you sure you want to delete this page?"
163
157
  })
164
158
  ]
@@ -177,21 +171,14 @@ function EditPageContent({ page }) {
177
171
  }),
178
172
  /*#__PURE__*/ _jsx(PageForm, {
179
173
  page: page,
180
- action: `/dash/pages/${sqid.encode(page.id)}`
174
+ action: `/dash/pages/${page.id}`
181
175
  })
182
176
  ]
183
177
  });
184
178
  }
185
179
  // List pages
186
180
  pagesRoutes.get("/", async (c)=>{
187
- const pages = await c.var.services.posts.list({
188
- type: "page",
189
- visibility: [
190
- "unlisted",
191
- "draft"
192
- ],
193
- limit: 100
194
- });
181
+ const pages = await c.var.services.pages.list();
195
182
  const siteName = await getSiteName(c);
196
183
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
197
184
  c: c,
@@ -217,21 +204,20 @@ pagesRoutes.get("/new", async (c)=>{
217
204
  // Create page
218
205
  pagesRoutes.post("/", async (c)=>{
219
206
  const body = await c.req.json();
220
- const page = await c.var.services.posts.create({
221
- type: "page",
207
+ const page = await c.var.services.pages.create({
222
208
  title: body.title,
223
- content: body.content,
224
- visibility: body.visibility,
225
- path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-")
209
+ body: body.body,
210
+ status: body.status,
211
+ slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-")
226
212
  });
227
- return dsRedirect(`/dash/pages/${sqid.encode(page.id)}`);
213
+ return dsRedirect(`/dash/pages/${page.id}`);
228
214
  });
229
215
  // View single page
230
216
  pagesRoutes.get("/:id", async (c)=>{
231
- const id = sqid.decode(c.req.param("id"));
232
- if (!id) return c.notFound();
233
- const page = await c.var.services.posts.getById(id);
234
- if (!page || page.type !== "page") return c.notFound();
217
+ const id = parseInt(c.req.param("id"), 10);
218
+ if (isNaN(id)) return c.notFound();
219
+ const page = await c.var.services.pages.getById(id);
220
+ if (!page) return c.notFound();
235
221
  const siteName = await getSiteName(c);
236
222
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
237
223
  c: c,
@@ -245,10 +231,10 @@ pagesRoutes.get("/:id", async (c)=>{
245
231
  });
246
232
  // Edit page form
247
233
  pagesRoutes.get("/:id/edit", async (c)=>{
248
- const id = sqid.decode(c.req.param("id"));
249
- if (!id) return c.notFound();
250
- const page = await c.var.services.posts.getById(id);
251
- if (!page || page.type !== "page") return c.notFound();
234
+ const id = parseInt(c.req.param("id"), 10);
235
+ if (isNaN(id)) return c.notFound();
236
+ const page = await c.var.services.pages.getById(id);
237
+ if (!page) return c.notFound();
252
238
  const siteName = await getSiteName(c);
253
239
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
254
240
  c: c,
@@ -262,22 +248,21 @@ pagesRoutes.get("/:id/edit", async (c)=>{
262
248
  });
263
249
  // Update page
264
250
  pagesRoutes.post("/:id", async (c)=>{
265
- const id = sqid.decode(c.req.param("id"));
266
- if (!id) return c.notFound();
251
+ const id = parseInt(c.req.param("id"), 10);
252
+ if (isNaN(id)) return c.notFound();
267
253
  const body = await c.req.json();
268
- await c.var.services.posts.update(id, {
269
- type: "page",
254
+ await c.var.services.pages.update(id, {
270
255
  title: body.title,
271
- content: body.content,
272
- visibility: body.visibility,
273
- path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-")
256
+ body: body.body,
257
+ status: body.status,
258
+ slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-")
274
259
  });
275
- return dsRedirect(`/dash/pages/${sqid.encode(id)}`);
260
+ return dsRedirect(`/dash/pages/${id}`);
276
261
  });
277
262
  // Delete page
278
263
  pagesRoutes.post("/:id/delete", async (c)=>{
279
- const id = sqid.decode(c.req.param("id"));
280
- if (!id) return c.notFound();
281
- await c.var.services.posts.delete(id);
264
+ const id = parseInt(c.req.param("id"), 10);
265
+ if (isNaN(id)) return c.notFound();
266
+ await c.var.services.pages.delete(id);
282
267
  return dsRedirect("/dash/pages");
283
268
  });
@@ -51,12 +51,7 @@ function NewPostContent({ collections }) {
51
51
  // List posts
52
52
  postsRoutes.get("/", async (c)=>{
53
53
  const posts = await c.var.services.posts.list({
54
- visibility: [
55
- "featured",
56
- "quiet",
57
- "unlisted",
58
- "draft"
59
- ]
54
+ excludeReplies: true
60
55
  });
61
56
  const siteName = await getSiteName(c);
62
57
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
@@ -87,22 +82,22 @@ postsRoutes.get("/new", async (c)=>{
87
82
  postsRoutes.post("/", async (c)=>{
88
83
  const body = await c.req.json();
89
84
  const post = await c.var.services.posts.create({
90
- type: body.type,
85
+ format: body.format,
91
86
  title: body.title || undefined,
92
- content: body.content,
93
- visibility: body.visibility,
94
- sourceUrl: body.sourceUrl || undefined,
95
- sourceName: body.sourceName || undefined,
96
- path: body.path || undefined
87
+ body: body.body,
88
+ status: body.status,
89
+ featured: body.featured,
90
+ pinned: body.pinned,
91
+ slug: body.slug || undefined,
92
+ url: body.url || undefined,
93
+ quoteText: body.quoteText || undefined,
94
+ rating: body.rating || undefined,
95
+ collectionId: body.collectionId || undefined
97
96
  });
98
97
  // Attach media if provided
99
98
  if (body.mediaIds && body.mediaIds.length > 0) {
100
99
  await c.var.services.media.attachToPost(post.id, body.mediaIds);
101
100
  }
102
- // Sync collection associations
103
- if (body.collectionIds) {
104
- await c.var.services.collections.syncPostCollections(post.id, body.collectionIds);
105
- }
106
101
  return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
107
102
  });
108
103
  function ViewPostContent({ post }) {
@@ -111,6 +106,7 @@ function ViewPostContent({ post }) {
111
106
  id: "y28hnO",
112
107
  message: "Post"
113
108
  });
109
+ const permalink = post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`;
114
110
  return /*#__PURE__*/ _jsxs(_Fragment, {
115
111
  children: [
116
112
  /*#__PURE__*/ _jsxs("div", {
@@ -126,7 +122,7 @@ function ViewPostContent({ post }) {
126
122
  id: "ePK91l",
127
123
  message: "Edit"
128
124
  }),
129
- viewHref: `/p/${sqid.encode(post.id)}`,
125
+ viewHref: permalink,
130
126
  viewLabel: $__i18n._({
131
127
  id: "jpctdh",
132
128
  message: "View"
@@ -140,7 +136,7 @@ function ViewPostContent({ post }) {
140
136
  children: /*#__PURE__*/ _jsx("div", {
141
137
  class: "prose",
142
138
  dangerouslySetInnerHTML: {
143
- __html: post.contentHtml || ""
139
+ __html: post.bodyHtml || ""
144
140
  }
145
141
  })
146
142
  })
@@ -148,7 +144,7 @@ function ViewPostContent({ post }) {
148
144
  ]
149
145
  });
150
146
  }
151
- function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, s3PublicUrl, collections, postCollectionIds }) {
147
+ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, s3PublicUrl, collections }) {
152
148
  const { i18n: $__i18n, _: $__ } = $_useLingui();
153
149
  return /*#__PURE__*/ _jsxs(_Fragment, {
154
150
  children: [
@@ -166,8 +162,7 @@ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUr
166
162
  r2PublicUrl: r2PublicUrl,
167
163
  imageTransformUrl: imageTransformUrl,
168
164
  s3PublicUrl: s3PublicUrl,
169
- collections: collections,
170
- postCollectionIds: postCollectionIds
165
+ collections: collections
171
166
  })
172
167
  ]
173
168
  });
@@ -202,8 +197,6 @@ postsRoutes.get("/:id/edit", async (c)=>{
202
197
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
203
198
  const s3PublicUrl = c.env.S3_PUBLIC_URL;
204
199
  const collections = await c.var.services.collections.list();
205
- const postCollections = await c.var.services.collections.getCollectionsForPost(post.id);
206
- const postCollectionIds = postCollections.map((col)=>col.id);
207
200
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
208
201
  c: c,
209
202
  title: `Edit: ${post.title || "Post"}`,
@@ -215,8 +208,7 @@ postsRoutes.get("/:id/edit", async (c)=>{
215
208
  r2PublicUrl: r2PublicUrl,
216
209
  imageTransformUrl: imageTransformUrl,
217
210
  s3PublicUrl: s3PublicUrl,
218
- collections: collections,
219
- postCollectionIds: postCollectionIds
211
+ collections: collections
220
212
  })
221
213
  }));
222
214
  });
@@ -226,22 +218,22 @@ postsRoutes.post("/:id", async (c)=>{
226
218
  if (!id) return c.notFound();
227
219
  const body = await c.req.json();
228
220
  await c.var.services.posts.update(id, {
229
- type: body.type,
221
+ format: body.format,
230
222
  title: body.title || null,
231
- content: body.content || null,
232
- visibility: body.visibility,
233
- sourceUrl: body.sourceUrl || null,
234
- sourceName: body.sourceName || null,
235
- path: body.path || null
223
+ body: body.body || null,
224
+ status: body.status,
225
+ featured: body.featured,
226
+ pinned: body.pinned,
227
+ slug: body.slug || null,
228
+ url: body.url || null,
229
+ quoteText: body.quoteText || null,
230
+ rating: body.rating || null,
231
+ collectionId: body.collectionId || null
236
232
  });
237
233
  // Update media attachments if provided
238
234
  if (body.mediaIds !== undefined) {
239
235
  await c.var.services.media.attachToPost(id, body.mediaIds);
240
236
  }
241
- // Sync collection associations
242
- if (body.collectionIds !== undefined) {
243
- await c.var.services.collections.syncPostCollections(id, body.collectionIds);
244
- }
245
237
  return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
246
238
  });
247
239
  // Delete post
@@ -15,10 +15,8 @@ export const rssRoutes = new Hono();
15
15
  const siteUrl = c.env.SITE_URL;
16
16
  const siteLanguage = await getSiteLanguage(c);
17
17
  const posts = await c.var.services.posts.list({
18
- visibility: [
19
- "featured",
20
- "quiet"
21
- ],
18
+ status: "published",
19
+ excludeReplies: true,
22
20
  limit: 50
23
21
  });
24
22
  // Batch load media for enclosures
@@ -2,25 +2,28 @@
2
2
  * Sitemap Routes
3
3
  */ import { Hono } from "hono";
4
4
  import { defaultSitemapRenderer } from "../../lib/feed.js";
5
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
5
+ import { createMediaContext, toPostViewsFromPosts, toPageView } from "../../lib/view.js";
6
6
  export const sitemapRoutes = new Hono();
7
7
  // XML Sitemap
8
8
  sitemapRoutes.get("/sitemap.xml", async (c)=>{
9
9
  const siteUrl = c.env.SITE_URL;
10
10
  const posts = await c.var.services.posts.list({
11
- visibility: [
12
- "featured",
13
- "quiet"
14
- ],
11
+ status: "published",
12
+ excludeReplies: true,
15
13
  limit: 1000
16
14
  });
17
- // Transform to PostView[]
15
+ // Fetch published pages
16
+ const allPages = await c.var.services.pages.list();
17
+ const publishedPages = allPages.filter((p)=>p.status === "published");
18
+ // Transform to View Models
18
19
  const mediaCtx = createMediaContext(c);
19
20
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
21
+ const pageViews = publishedPages.map(toPageView);
20
22
  const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
21
23
  const xml = renderer({
22
24
  siteUrl,
23
- posts: postViews
25
+ posts: postViews,
26
+ pages: pageViews
24
27
  });
25
28
  return new Response(xml, {
26
29
  headers: {
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Archive Page Route
4
4
  *
5
- * Shows all posts, optionally filtered by type
5
+ * Shows all posts, optionally filtered by format or featured status
6
6
  */ import { Hono } from "hono";
7
- import { POST_TYPES } from "../../types.js";
8
- import { ArchivePage as DefaultArchivePage } from "../../themes/minimal/pages/ArchivePage.js";
7
+ import { FORMATS } from "../../types.js";
8
+ import { ArchivePage as DefaultArchivePage } from "../../themes/threads/pages/ArchivePage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
11
11
  import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
@@ -13,19 +13,19 @@ const PAGE_SIZE = 50;
13
13
  export const archiveRoutes = new Hono();
14
14
  // Archive page - all posts
15
15
  archiveRoutes.get("/", async (c)=>{
16
- const typeParam = c.req.query("type");
17
- const type = typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
16
+ const formatParam = c.req.query("format");
17
+ const format = formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
18
+ const featuredParam = c.req.query("featured");
19
+ const featured = featuredParam === "true" ? true : undefined;
18
20
  // Parse cursor
19
21
  const cursorParam = c.req.query("cursor");
20
22
  const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
21
23
  const navData = await getNavigationData(c);
22
24
  // Fetch one extra to check for more
23
25
  const posts = await c.var.services.posts.list({
24
- type,
25
- visibility: [
26
- "featured",
27
- "quiet"
28
- ],
26
+ format,
27
+ status: "published",
28
+ featured,
29
29
  excludeReplies: true,
30
30
  cursor,
31
31
  limit: PAGE_SIZE + 1
@@ -57,7 +57,8 @@ archiveRoutes.get("/", async (c)=>{
57
57
  groups: groups,
58
58
  hasMore: hasMore,
59
59
  nextCursor: nextCursor,
60
- type: type,
60
+ format: format,
61
+ featured: featured,
61
62
  theme: components
62
63
  })
63
64
  });
@@ -2,16 +2,21 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Collection Page Route
4
4
  */ import { Hono } from "hono";
5
- import { CollectionPage as DefaultCollectionPage } from "../../themes/minimal/pages/CollectionPage.js";
5
+ import { CollectionPage as DefaultCollectionPage } from "../../themes/threads/pages/CollectionPage.js";
6
6
  import { getNavigationData } from "../../lib/navigation.js";
7
7
  import { renderPublicPage } from "../../lib/render.js";
8
8
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
9
9
  export const collectionRoutes = new Hono();
10
- collectionRoutes.get("/:path", async (c)=>{
11
- const path = c.req.param("path");
12
- const collection = await c.var.services.collections.getByPath(path);
10
+ collectionRoutes.get("/:slug", async (c)=>{
11
+ const slug = c.req.param("slug");
12
+ const collection = await c.var.services.collections.getBySlug(slug);
13
13
  if (!collection) return c.notFound();
14
- const posts = await c.var.services.collections.getPosts(collection.id);
14
+ // Fetch posts in this collection
15
+ const posts = await c.var.services.posts.list({
16
+ collectionId: collection.id,
17
+ status: "published",
18
+ excludeReplies: true
19
+ });
15
20
  const navData = await getNavigationData(c);
16
21
  // Transform to View Models
17
22
  const mediaCtx = createMediaContext(c);
@@ -25,6 +30,7 @@ collectionRoutes.get("/:path", async (c)=>{
25
30
  content: /*#__PURE__*/ _jsx(Page, {
26
31
  collection: collection,
27
32
  posts: postViews,
33
+ hasMore: false,
28
34
  theme: components
29
35
  })
30
36
  });
@@ -3,76 +3,67 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
3
3
  * Home Page Route
4
4
  *
5
5
  * Timeline feed with per-type card components and thread previews.
6
+ * Handles both full-page rendering and load-more SSE responses.
6
7
  */ import { Hono } from "hono";
7
- import { buildMediaMap } from "../../lib/media-helpers.js";
8
8
  import { getNavigationData } from "../../lib/navigation.js";
9
9
  import { renderPublicPage } from "../../lib/render.js";
10
- import { HomePage as DefaultHomePage } from "../../themes/minimal/pages/HomePage.js";
11
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
12
- const PAGE_SIZE = 20;
10
+ import { assembleTimeline } from "../../lib/timeline.js";
11
+ import { sse } from "../../lib/sse.js";
12
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
13
+ import { HomePage as DefaultHomePage } from "../../themes/threads/pages/HomePage.js";
13
14
  export const homeRoutes = new Hono();
14
15
  homeRoutes.get("/", async (c)=>{
15
- const navData = await getNavigationData(c);
16
- // Fetch one extra to determine if there are more
17
- const posts = await c.var.services.posts.list({
18
- visibility: [
19
- "featured",
20
- "quiet"
21
- ],
22
- excludeReplies: true,
23
- excludeTypes: [
24
- "page"
25
- ],
26
- limit: PAGE_SIZE + 1
16
+ const cursorParam = c.req.query("cursor");
17
+ const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
18
+ const lastDate = c.req.query("lastDate");
19
+ const { items, hasMore, nextCursor } = await assembleTimeline(c, {
20
+ cursor: cursor && !isNaN(cursor) ? cursor : undefined
27
21
  });
28
- const hasMore = posts.length > PAGE_SIZE;
29
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
30
- // Batch load media attachments
31
- const postIds = displayPosts.map((p)=>p.id);
32
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
33
- const mediaCtx = createMediaContext(c);
34
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
35
- // Get reply counts to identify thread roots
36
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
37
- const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
38
- // Batch load thread previews
39
- const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
40
- // Batch load media for preview replies
41
- const previewReplyIds = [];
42
- for (const replies of threadPreviews.values()){
43
- for (const reply of replies){
44
- previewReplyIds.push(reply.id);
22
+ // SSE load-more response
23
+ if (cursor && !isNaN(cursor)) {
24
+ if (items.length === 0) {
25
+ return sse(c, async (stream)=>{
26
+ stream.remove("#load-more-container");
27
+ });
45
28
  }
46
- }
47
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
48
- // Assemble timeline items with View Models
49
- const items = displayPosts.map((post)=>{
50
- const postView = toPostView({
51
- ...post,
52
- mediaAttachments: mediaMap.get(post.id) ?? []
53
- }, mediaCtx);
54
- const replyCount = replyCounts.get(post.id) ?? 0;
55
- const previewReplies = threadPreviews.get(post.id);
56
- if (replyCount > 0 && previewReplies) {
57
- return {
58
- post: postView,
59
- threadPreview: {
60
- replies: toPostViews(previewReplies.map((r)=>({
61
- ...r,
62
- mediaAttachments: previewMediaMap.get(r.id) ?? []
63
- })), mediaCtx),
64
- totalReplyCount: replyCount
65
- }
66
- };
29
+ const themeConfig = c.var.config.theme;
30
+ const renderMore = themeConfig?.timelineMore;
31
+ if (!renderMore) {
32
+ // Should never happen — default theme always provides timelineMore
33
+ return sse(c, async (stream)=>{
34
+ stream.remove("#load-more-container");
35
+ });
67
36
  }
68
- return {
69
- post: postView
70
- };
37
+ const patches = renderMore({
38
+ items,
39
+ lastDate: lastDate ?? undefined,
40
+ hasMore,
41
+ nextCursor,
42
+ theme: themeConfig?.components
43
+ });
44
+ return sse(c, async (stream)=>{
45
+ for (const patch of patches){
46
+ if (patch.mode === "remove") {
47
+ stream.remove(patch.selector);
48
+ } else {
49
+ stream.patchElements(patch.content, {
50
+ mode: patch.mode,
51
+ selector: patch.selector
52
+ });
53
+ }
54
+ }
55
+ });
56
+ }
57
+ // Full page render
58
+ const navData = await getNavigationData(c);
59
+ // Fetch pinned posts
60
+ const pinnedPosts = await c.var.services.posts.list({
61
+ pinned: true,
62
+ status: "published",
63
+ excludeReplies: true
71
64
  });
72
- // Determine next cursor
73
- const lastPost = displayPosts[displayPosts.length - 1];
74
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
75
- // Resolve page component
65
+ const mediaCtx = createMediaContext(c);
66
+ const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
76
67
  const components = c.var.config.theme?.components;
77
68
  const Page = components?.HomePage ?? DefaultHomePage;
78
69
  return renderPublicPage(c, {
@@ -80,6 +71,7 @@ homeRoutes.get("/", async (c)=>{
80
71
  navData,
81
72
  content: /*#__PURE__*/ _jsx(Page, {
82
73
  items: items,
74
+ pinnedItems: pinnedItems,
83
75
  hasMore: hasMore,
84
76
  nextCursor: nextCursor,
85
77
  theme: components