@jant/core 0.3.6 → 0.3.8

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 (264) hide show
  1. package/dist/app.js +11 -21
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -0
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/lib/image.js +3 -3
  9. package/dist/lib/media-helpers.js +43 -0
  10. package/dist/lib/nav-reorder.js +27 -0
  11. package/dist/lib/navigation.js +35 -0
  12. package/dist/lib/schemas.js +32 -2
  13. package/dist/lib/sse.js +7 -8
  14. package/dist/lib/theme-components.js +49 -0
  15. package/dist/routes/api/posts.js +101 -5
  16. package/dist/routes/api/timeline.js +115 -0
  17. package/dist/routes/api/upload.js +9 -5
  18. package/dist/routes/dash/media.js +38 -0
  19. package/dist/routes/dash/navigation.js +274 -0
  20. package/dist/routes/dash/posts.js +45 -6
  21. package/dist/routes/feed/rss.js +10 -1
  22. package/dist/routes/pages/archive.js +14 -27
  23. package/dist/routes/pages/collection.js +10 -19
  24. package/dist/routes/pages/home.js +88 -98
  25. package/dist/routes/pages/page.js +19 -38
  26. package/dist/routes/pages/post.js +61 -48
  27. package/dist/routes/pages/search.js +13 -26
  28. package/dist/services/collection.js +13 -0
  29. package/dist/services/index.js +3 -1
  30. package/dist/services/media.js +55 -2
  31. package/dist/services/navigation.js +115 -0
  32. package/dist/services/post.js +26 -1
  33. package/dist/theme/components/MediaGallery.js +107 -0
  34. package/dist/theme/components/PostForm.js +158 -2
  35. package/dist/theme/components/PostList.js +5 -0
  36. package/dist/theme/components/index.js +3 -0
  37. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  38. package/dist/theme/components/timeline/ImageCard.js +86 -0
  39. package/dist/theme/components/timeline/LinkCard.js +62 -0
  40. package/dist/theme/components/timeline/NoteCard.js +37 -0
  41. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  42. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  43. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  44. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  45. package/dist/theme/components/timeline/index.js +8 -0
  46. package/dist/theme/layouts/DashLayout.js +8 -0
  47. package/dist/theme/layouts/SiteLayout.js +160 -0
  48. package/dist/theme/layouts/index.js +1 -0
  49. package/dist/types/sortablejs.d.js +5 -0
  50. package/dist/types.js +27 -0
  51. package/package.json +3 -2
  52. package/src/__tests__/helpers/app.ts +6 -1
  53. package/src/__tests__/helpers/db.ts +20 -0
  54. package/src/app.tsx +11 -25
  55. package/src/client.ts +1 -0
  56. package/src/db/migrations/0002_add_media_attachments.sql +3 -0
  57. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  58. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  59. package/src/db/migrations/meta/_journal.json +14 -0
  60. package/src/db/schema.ts +15 -0
  61. package/src/i18n/locales/en.po +170 -58
  62. package/src/i18n/locales/en.ts +1 -1
  63. package/src/i18n/locales/zh-Hans.po +162 -71
  64. package/src/i18n/locales/zh-Hans.ts +1 -1
  65. package/src/i18n/locales/zh-Hant.po +162 -71
  66. package/src/i18n/locales/zh-Hant.ts +1 -1
  67. package/src/index.ts +13 -1
  68. package/src/lib/__tests__/schemas.test.ts +89 -1
  69. package/src/lib/__tests__/sse.test.ts +13 -1
  70. package/src/lib/__tests__/theme-components.test.ts +107 -0
  71. package/src/lib/image.ts +3 -3
  72. package/src/lib/media-helpers.ts +54 -0
  73. package/src/lib/nav-reorder.ts +26 -0
  74. package/src/lib/navigation.ts +46 -0
  75. package/src/lib/schemas.ts +47 -1
  76. package/src/lib/sse.ts +10 -11
  77. package/src/lib/theme-components.ts +76 -0
  78. package/src/routes/api/__tests__/posts.test.ts +239 -0
  79. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  80. package/src/routes/api/posts.ts +134 -5
  81. package/src/routes/api/timeline.tsx +145 -0
  82. package/src/routes/api/upload.ts +9 -5
  83. package/src/routes/dash/media.tsx +50 -0
  84. package/src/routes/dash/navigation.tsx +306 -0
  85. package/src/routes/dash/posts.tsx +79 -7
  86. package/src/routes/feed/rss.ts +14 -1
  87. package/src/routes/pages/archive.tsx +15 -23
  88. package/src/routes/pages/collection.tsx +8 -15
  89. package/src/routes/pages/home.tsx +121 -88
  90. package/src/routes/pages/page.tsx +17 -30
  91. package/src/routes/pages/post.tsx +64 -40
  92. package/src/routes/pages/search.tsx +18 -22
  93. package/src/services/__tests__/collection.test.ts +102 -0
  94. package/src/services/__tests__/media.test.ts +282 -7
  95. package/src/services/__tests__/navigation.test.ts +213 -0
  96. package/src/services/__tests__/post-timeline.test.ts +220 -0
  97. package/src/services/collection.ts +19 -0
  98. package/src/services/index.ts +7 -0
  99. package/src/services/media.ts +78 -2
  100. package/src/services/navigation.ts +165 -0
  101. package/src/services/post.ts +48 -1
  102. package/src/styles/components.css +59 -0
  103. package/src/theme/components/MediaGallery.tsx +128 -0
  104. package/src/theme/components/PostForm.tsx +170 -2
  105. package/src/theme/components/PostList.tsx +7 -0
  106. package/src/theme/components/index.ts +13 -0
  107. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  108. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  109. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  110. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  111. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  112. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  113. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  114. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  115. package/src/theme/components/timeline/index.ts +8 -0
  116. package/src/theme/layouts/DashLayout.tsx +10 -0
  117. package/src/theme/layouts/SiteLayout.tsx +184 -0
  118. package/src/theme/layouts/index.ts +1 -0
  119. package/src/types/sortablejs.d.ts +23 -0
  120. package/src/types.ts +97 -0
  121. package/dist/app.d.ts +0 -38
  122. package/dist/app.d.ts.map +0 -1
  123. package/dist/auth.d.ts +0 -25
  124. package/dist/auth.d.ts.map +0 -1
  125. package/dist/db/index.d.ts +0 -10
  126. package/dist/db/index.d.ts.map +0 -1
  127. package/dist/db/schema.d.ts +0 -1507
  128. package/dist/db/schema.d.ts.map +0 -1
  129. package/dist/i18n/Trans.d.ts +0 -25
  130. package/dist/i18n/Trans.d.ts.map +0 -1
  131. package/dist/i18n/context.d.ts +0 -69
  132. package/dist/i18n/context.d.ts.map +0 -1
  133. package/dist/i18n/detect.d.ts +0 -20
  134. package/dist/i18n/detect.d.ts.map +0 -1
  135. package/dist/i18n/i18n.d.ts +0 -32
  136. package/dist/i18n/i18n.d.ts.map +0 -1
  137. package/dist/i18n/index.d.ts +0 -41
  138. package/dist/i18n/index.d.ts.map +0 -1
  139. package/dist/i18n/locales/en.d.ts +0 -3
  140. package/dist/i18n/locales/en.d.ts.map +0 -1
  141. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  142. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  143. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  144. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  145. package/dist/i18n/locales.d.ts +0 -11
  146. package/dist/i18n/locales.d.ts.map +0 -1
  147. package/dist/i18n/middleware.d.ts +0 -21
  148. package/dist/i18n/middleware.d.ts.map +0 -1
  149. package/dist/index.d.ts +0 -16
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/lib/config.d.ts +0 -83
  152. package/dist/lib/config.d.ts.map +0 -1
  153. package/dist/lib/constants.d.ts +0 -37
  154. package/dist/lib/constants.d.ts.map +0 -1
  155. package/dist/lib/image.d.ts +0 -73
  156. package/dist/lib/image.d.ts.map +0 -1
  157. package/dist/lib/index.d.ts +0 -9
  158. package/dist/lib/index.d.ts.map +0 -1
  159. package/dist/lib/markdown.d.ts +0 -60
  160. package/dist/lib/markdown.d.ts.map +0 -1
  161. package/dist/lib/schemas.d.ts +0 -113
  162. package/dist/lib/schemas.d.ts.map +0 -1
  163. package/dist/lib/sqid.d.ts +0 -60
  164. package/dist/lib/sqid.d.ts.map +0 -1
  165. package/dist/lib/sse.d.ts +0 -192
  166. package/dist/lib/sse.d.ts.map +0 -1
  167. package/dist/lib/theme.d.ts +0 -44
  168. package/dist/lib/theme.d.ts.map +0 -1
  169. package/dist/lib/time.d.ts +0 -90
  170. package/dist/lib/time.d.ts.map +0 -1
  171. package/dist/lib/url.d.ts +0 -82
  172. package/dist/lib/url.d.ts.map +0 -1
  173. package/dist/middleware/auth.d.ts +0 -24
  174. package/dist/middleware/auth.d.ts.map +0 -1
  175. package/dist/middleware/onboarding.d.ts +0 -26
  176. package/dist/middleware/onboarding.d.ts.map +0 -1
  177. package/dist/routes/api/posts.d.ts +0 -13
  178. package/dist/routes/api/posts.d.ts.map +0 -1
  179. package/dist/routes/api/search.d.ts +0 -13
  180. package/dist/routes/api/search.d.ts.map +0 -1
  181. package/dist/routes/api/upload.d.ts +0 -16
  182. package/dist/routes/api/upload.d.ts.map +0 -1
  183. package/dist/routes/dash/collections.d.ts +0 -13
  184. package/dist/routes/dash/collections.d.ts.map +0 -1
  185. package/dist/routes/dash/index.d.ts +0 -15
  186. package/dist/routes/dash/index.d.ts.map +0 -1
  187. package/dist/routes/dash/media.d.ts +0 -16
  188. package/dist/routes/dash/media.d.ts.map +0 -1
  189. package/dist/routes/dash/pages.d.ts +0 -15
  190. package/dist/routes/dash/pages.d.ts.map +0 -1
  191. package/dist/routes/dash/posts.d.ts +0 -13
  192. package/dist/routes/dash/posts.d.ts.map +0 -1
  193. package/dist/routes/dash/redirects.d.ts +0 -13
  194. package/dist/routes/dash/redirects.d.ts.map +0 -1
  195. package/dist/routes/dash/settings.d.ts +0 -15
  196. package/dist/routes/dash/settings.d.ts.map +0 -1
  197. package/dist/routes/feed/rss.d.ts +0 -13
  198. package/dist/routes/feed/rss.d.ts.map +0 -1
  199. package/dist/routes/feed/sitemap.d.ts +0 -13
  200. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  201. package/dist/routes/pages/archive.d.ts +0 -15
  202. package/dist/routes/pages/archive.d.ts.map +0 -1
  203. package/dist/routes/pages/collection.d.ts +0 -13
  204. package/dist/routes/pages/collection.d.ts.map +0 -1
  205. package/dist/routes/pages/home.d.ts +0 -13
  206. package/dist/routes/pages/home.d.ts.map +0 -1
  207. package/dist/routes/pages/page.d.ts +0 -15
  208. package/dist/routes/pages/page.d.ts.map +0 -1
  209. package/dist/routes/pages/post.d.ts +0 -13
  210. package/dist/routes/pages/post.d.ts.map +0 -1
  211. package/dist/routes/pages/search.d.ts +0 -13
  212. package/dist/routes/pages/search.d.ts.map +0 -1
  213. package/dist/services/collection.d.ts +0 -31
  214. package/dist/services/collection.d.ts.map +0 -1
  215. package/dist/services/index.d.ts +0 -28
  216. package/dist/services/index.d.ts.map +0 -1
  217. package/dist/services/media.d.ts +0 -27
  218. package/dist/services/media.d.ts.map +0 -1
  219. package/dist/services/post.d.ts +0 -31
  220. package/dist/services/post.d.ts.map +0 -1
  221. package/dist/services/redirect.d.ts +0 -15
  222. package/dist/services/redirect.d.ts.map +0 -1
  223. package/dist/services/search.d.ts +0 -26
  224. package/dist/services/search.d.ts.map +0 -1
  225. package/dist/services/settings.d.ts +0 -18
  226. package/dist/services/settings.d.ts.map +0 -1
  227. package/dist/theme/color-themes.d.ts +0 -30
  228. package/dist/theme/color-themes.d.ts.map +0 -1
  229. package/dist/theme/components/ActionButtons.d.ts +0 -43
  230. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  231. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  232. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  233. package/dist/theme/components/DangerZone.d.ts +0 -36
  234. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  235. package/dist/theme/components/EmptyState.d.ts +0 -27
  236. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  237. package/dist/theme/components/ListItemRow.d.ts +0 -15
  238. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  239. package/dist/theme/components/PageForm.d.ts +0 -14
  240. package/dist/theme/components/PageForm.d.ts.map +0 -1
  241. package/dist/theme/components/Pagination.d.ts +0 -46
  242. package/dist/theme/components/Pagination.d.ts.map +0 -1
  243. package/dist/theme/components/PostForm.d.ts +0 -11
  244. package/dist/theme/components/PostForm.d.ts.map +0 -1
  245. package/dist/theme/components/PostList.d.ts +0 -10
  246. package/dist/theme/components/PostList.d.ts.map +0 -1
  247. package/dist/theme/components/ThreadView.d.ts +0 -15
  248. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  249. package/dist/theme/components/TypeBadge.d.ts +0 -12
  250. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  251. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  252. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  253. package/dist/theme/components/index.d.ts +0 -13
  254. package/dist/theme/components/index.d.ts.map +0 -1
  255. package/dist/theme/index.d.ts +0 -21
  256. package/dist/theme/index.d.ts.map +0 -1
  257. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  258. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  259. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  260. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  261. package/dist/theme/layouts/index.d.ts +0 -3
  262. package/dist/theme/layouts/index.d.ts.map +0 -1
  263. package/dist/types.d.ts +0 -213
  264. package/dist/types.d.ts.map +0 -1
@@ -30,7 +30,7 @@ function PostsListContent({ posts }) {
30
30
  ]
31
31
  });
32
32
  }
33
- function NewPostContent() {
33
+ function NewPostContent({ collections }) {
34
34
  const { i18n: $__i18n, _: $__ } = $_useLingui();
35
35
  return /*#__PURE__*/ _jsxs(_Fragment, {
36
36
  children: [
@@ -42,7 +42,8 @@ function NewPostContent() {
42
42
  })
43
43
  }),
44
44
  /*#__PURE__*/ _jsx(PostForm, {
45
- action: "/dash/posts"
45
+ action: "/dash/posts",
46
+ collections: collections
46
47
  })
47
48
  ]
48
49
  });
@@ -71,12 +72,15 @@ postsRoutes.get("/", async (c)=>{
71
72
  // New post form
72
73
  postsRoutes.get("/new", async (c)=>{
73
74
  const siteName = await getSiteName(c);
75
+ const collections = await c.var.services.collections.list();
74
76
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
75
77
  c: c,
76
78
  title: "New Post",
77
79
  siteName: siteName,
78
80
  currentPath: "/dash/posts",
79
- children: /*#__PURE__*/ _jsx(NewPostContent, {})
81
+ children: /*#__PURE__*/ _jsx(NewPostContent, {
82
+ collections: collections
83
+ })
80
84
  }));
81
85
  });
82
86
  // Create post
@@ -88,8 +92,17 @@ postsRoutes.post("/", async (c)=>{
88
92
  content: body.content,
89
93
  visibility: body.visibility,
90
94
  sourceUrl: body.sourceUrl || undefined,
95
+ sourceName: body.sourceName || undefined,
91
96
  path: body.path || undefined
92
97
  });
98
+ // Attach media if provided
99
+ if (body.mediaIds && body.mediaIds.length > 0) {
100
+ await c.var.services.media.attachToPost(post.id, body.mediaIds);
101
+ }
102
+ // Sync collection associations
103
+ if (body.collectionIds) {
104
+ await c.var.services.collections.syncPostCollections(post.id, body.collectionIds);
105
+ }
93
106
  return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
94
107
  });
95
108
  function ViewPostContent({ post }) {
@@ -135,7 +148,7 @@ function ViewPostContent({ post }) {
135
148
  ]
136
149
  });
137
150
  }
138
- function EditPostContent({ post }) {
151
+ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, collections, postCollectionIds }) {
139
152
  const { i18n: $__i18n, _: $__ } = $_useLingui();
140
153
  return /*#__PURE__*/ _jsxs(_Fragment, {
141
154
  children: [
@@ -148,7 +161,12 @@ function EditPostContent({ post }) {
148
161
  }),
149
162
  /*#__PURE__*/ _jsx(PostForm, {
150
163
  post: post,
151
- action: `/dash/posts/${sqid.encode(post.id)}`
164
+ action: `/dash/posts/${sqid.encode(post.id)}`,
165
+ mediaAttachments: mediaAttachments,
166
+ r2PublicUrl: r2PublicUrl,
167
+ imageTransformUrl: imageTransformUrl,
168
+ collections: collections,
169
+ postCollectionIds: postCollectionIds
152
170
  })
153
171
  ]
154
172
  });
@@ -178,13 +196,24 @@ postsRoutes.get("/:id/edit", async (c)=>{
178
196
  const post = await c.var.services.posts.getById(id);
179
197
  if (!post) return c.notFound();
180
198
  const siteName = await getSiteName(c);
199
+ const mediaAttachments = await c.var.services.media.getByPostId(post.id);
200
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
201
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
202
+ const collections = await c.var.services.collections.list();
203
+ const postCollections = await c.var.services.collections.getCollectionsForPost(post.id);
204
+ const postCollectionIds = postCollections.map((col)=>col.id);
181
205
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
182
206
  c: c,
183
207
  title: `Edit: ${post.title || "Post"}`,
184
208
  siteName: siteName,
185
209
  currentPath: "/dash/posts",
186
210
  children: /*#__PURE__*/ _jsx(EditPostContent, {
187
- post: post
211
+ post: post,
212
+ mediaAttachments: mediaAttachments,
213
+ r2PublicUrl: r2PublicUrl,
214
+ imageTransformUrl: imageTransformUrl,
215
+ collections: collections,
216
+ postCollectionIds: postCollectionIds
188
217
  })
189
218
  }));
190
219
  });
@@ -199,14 +228,24 @@ postsRoutes.post("/:id", async (c)=>{
199
228
  content: body.content || null,
200
229
  visibility: body.visibility,
201
230
  sourceUrl: body.sourceUrl || null,
231
+ sourceName: body.sourceName || null,
202
232
  path: body.path || null
203
233
  });
234
+ // Update media attachments if provided
235
+ if (body.mediaIds !== undefined) {
236
+ await c.var.services.media.attachToPost(id, body.mediaIds);
237
+ }
238
+ // Sync collection associations
239
+ if (body.collectionIds !== undefined) {
240
+ await c.var.services.collections.syncPostCollections(id, body.collectionIds);
241
+ }
204
242
  return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
205
243
  });
206
244
  // Delete post
207
245
  postsRoutes.post("/:id/delete", async (c)=>{
208
246
  const id = sqid.decode(c.req.param("id"));
209
247
  if (!id) return c.notFound();
248
+ await c.var.services.media.detachFromPost(id);
210
249
  await c.var.services.posts.delete(id);
211
250
  return dsRedirect("/dash/posts");
212
251
  });
@@ -3,6 +3,7 @@
3
3
  */ import { Hono } from "hono";
4
4
  import * as sqid from "../../lib/sqid.js";
5
5
  import * as time from "../../lib/time.js";
6
+ import { getMediaUrl } from "../../lib/image.js";
6
7
  export const rssRoutes = new Hono();
7
8
  // RSS 2.0 Feed - main feed at /feed
8
9
  rssRoutes.get("/", async (c)=>{
@@ -10,6 +11,7 @@ rssRoutes.get("/", async (c)=>{
10
11
  const siteName = all["SITE_NAME"] ?? "Jant";
11
12
  const siteDescription = all["SITE_DESCRIPTION"] ?? "";
12
13
  const siteUrl = c.env.SITE_URL;
14
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
13
15
  const posts = await c.var.services.posts.list({
14
16
  visibility: [
15
17
  "featured",
@@ -17,17 +19,24 @@ rssRoutes.get("/", async (c)=>{
17
19
  ],
18
20
  limit: 50
19
21
  });
22
+ // Batch load media for enclosures
23
+ const postIds = posts.map((p)=>p.id);
24
+ const mediaMap = await c.var.services.media.getByPostIds(postIds);
20
25
  const items = posts.map((post)=>{
21
26
  const link = `${siteUrl}/p/${sqid.encode(post.id)}`;
22
27
  const title = post.title || `Post #${post.id}`;
23
28
  const pubDate = new Date(post.publishedAt * 1000).toUTCString();
29
+ // Add enclosure for first media attachment
30
+ const postMedia = mediaMap.get(post.id);
31
+ const firstMedia = postMedia?.[0];
32
+ const enclosure = firstMedia ? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.r2Key, r2PublicUrl)}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>` : "";
24
33
  return `
25
34
  <item>
26
35
  <title><![CDATA[${escapeXml(title)}]]></title>
27
36
  <link>${link}</link>
28
37
  <guid isPermaLink="true">${link}</guid>
29
38
  <pubDate>${pubDate}</pubDate>
30
- <description><![CDATA[${post.contentHtml || ""}]]></description>
39
+ <description><![CDATA[${post.contentHtml || ""}]]></description>${enclosure}
31
40
  </item>`;
32
41
  }).join("");
33
42
  const rss = `<?xml version="1.0" encoding="UTF-8"?>
@@ -1,16 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Archive Page Route
5
4
  *
6
5
  * Shows all posts, optionally filtered by type
7
6
  */ import { Hono } from "hono";
8
7
  import { useLingui as $_useLingui } from "@jant/core/i18n";
9
- import { BaseLayout } from "../../theme/layouts/index.js";
8
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
10
9
  import { Pagination } from "../../theme/components/index.js";
11
10
  import { POST_TYPES } from "../../types.js";
12
11
  import * as sqid from "../../lib/sqid.js";
13
12
  import * as time from "../../lib/time.js";
13
+ import { getNavigationData } from "../../lib/navigation.js";
14
14
  const PAGE_SIZE = 50;
15
15
  export const archiveRoutes = new Hono();
16
16
  function getTypeLabel(type) {
@@ -89,7 +89,6 @@ function ArchiveContent({ displayPosts, hasMore, nextCursor, type, grouped, repl
89
89
  message: "Archive"
90
90
  });
91
91
  return /*#__PURE__*/ _jsxs("div", {
92
- class: "container py-8",
93
92
  children: [
94
93
  /*#__PURE__*/ _jsxs("header", {
95
94
  class: "mb-8",
@@ -183,21 +182,6 @@ function ArchiveContent({ displayPosts, hasMore, nextCursor, type, grouped, repl
183
182
  baseUrl: type ? `/archive?type=${type}` : "/archive",
184
183
  hasMore: hasMore,
185
184
  nextCursor: nextCursor
186
- }),
187
- /*#__PURE__*/ _jsx("nav", {
188
- class: "mt-4",
189
- children: /*#__PURE__*/ _jsxs("a", {
190
- href: "/",
191
- class: "text-sm hover:underline",
192
- children: [
193
- "←",
194
- " ",
195
- $__i18n._({
196
- id: "x4RuFo",
197
- message: "Back to home"
198
- })
199
- ]
200
- })
201
185
  })
202
186
  ]
203
187
  });
@@ -209,7 +193,7 @@ archiveRoutes.get("/", async (c)=>{
209
193
  // Parse cursor
210
194
  const cursorParam = c.req.query("cursor");
211
195
  const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
212
- const siteName = await getSiteName(c);
196
+ const navData = await getNavigationData(c);
213
197
  // Fetch one extra to check for more
214
198
  const posts = await c.var.services.posts.list({
215
199
  type,
@@ -240,15 +224,18 @@ archiveRoutes.get("/", async (c)=>{
240
224
  grouped.get(key).push(post);
241
225
  }
242
226
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
243
- title: `Archive - ${siteName}`,
227
+ title: `Archive - ${navData.siteName}`,
244
228
  c: c,
245
- children: /*#__PURE__*/ _jsx(ArchiveContent, {
246
- displayPosts: displayPosts,
247
- hasMore: hasMore,
248
- nextCursor: nextCursor,
249
- type: type,
250
- grouped: grouped,
251
- replyCounts: replyCounts
229
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
230
+ ...navData,
231
+ children: /*#__PURE__*/ _jsx(ArchiveContent, {
232
+ displayPosts: displayPosts,
233
+ hasMore: hasMore,
234
+ nextCursor: nextCursor,
235
+ type: type,
236
+ grouped: grouped,
237
+ replyCounts: replyCounts
238
+ })
252
239
  })
253
240
  }));
254
241
  });
@@ -1,17 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Collection Page Route
5
4
  */ import { Hono } from "hono";
6
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { BaseLayout } from "../../theme/layouts/index.js";
6
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
8
7
  import * as sqid from "../../lib/sqid.js";
9
8
  import * as time from "../../lib/time.js";
9
+ import { getNavigationData } from "../../lib/navigation.js";
10
10
  export const collectionRoutes = new Hono();
11
11
  function CollectionContent({ collection, posts }) {
12
12
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
13
  return /*#__PURE__*/ _jsxs("div", {
14
- class: "container py-8",
15
14
  children: [
16
15
  /*#__PURE__*/ _jsxs("header", {
17
16
  class: "mb-8",
@@ -61,17 +60,6 @@ function CollectionContent({ collection, posts }) {
61
60
  })
62
61
  ]
63
62
  }, post.id))
64
- }),
65
- /*#__PURE__*/ _jsx("nav", {
66
- class: "mt-8",
67
- children: /*#__PURE__*/ _jsx("a", {
68
- href: "/",
69
- class: "text-sm hover:underline",
70
- children: $__i18n._({
71
- id: "biOepV",
72
- message: "← Back to home"
73
- })
74
- })
75
63
  })
76
64
  ]
77
65
  });
@@ -81,14 +69,17 @@ collectionRoutes.get("/:path", async (c)=>{
81
69
  const collection = await c.var.services.collections.getByPath(path);
82
70
  if (!collection) return c.notFound();
83
71
  const posts = await c.var.services.collections.getPosts(collection.id);
84
- const siteName = await getSiteName(c);
72
+ const navData = await getNavigationData(c);
85
73
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
86
- title: `${collection.title} - ${siteName}`,
74
+ title: `${collection.title} - ${navData.siteName}`,
87
75
  description: collection.description ?? undefined,
88
76
  c: c,
89
- children: /*#__PURE__*/ _jsx(CollectionContent, {
90
- collection: collection,
91
- posts: posts
77
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
78
+ ...navData,
79
+ children: /*#__PURE__*/ _jsx(CollectionContent, {
80
+ collection: collection,
81
+ posts: posts
82
+ })
92
83
  })
93
84
  }));
94
85
  });
@@ -1,119 +1,109 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Home Page Route
4
+ *
5
+ * Timeline feed with per-type card components and thread previews.
4
6
  */ import { Hono } from "hono";
5
7
  import { useLingui as $_useLingui } from "@jant/core/i18n";
6
- import { BaseLayout } from "../../theme/layouts/index.js";
7
- import * as sqid from "../../lib/sqid.js";
8
- import * as time from "../../lib/time.js";
9
- import { getSiteName } from "../../lib/config.js";
8
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
9
+ import { buildMediaMap } from "../../lib/media-helpers.js";
10
+ import { resolveTimelineFeed } from "../../lib/theme-components.js";
11
+ import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
12
+ import { getNavigationData } from "../../lib/navigation.js";
13
+ const PAGE_SIZE = 20;
10
14
  export const homeRoutes = new Hono();
11
- function HomeContent({ siteName, posts }) {
15
+ function HomeContent({ FeedComponent, feedProps }) {
12
16
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs("div", {
14
- class: "container py-8",
15
- children: [
16
- /*#__PURE__*/ _jsxs("header", {
17
- class: "mb-8 flex items-center justify-between",
18
- children: [
19
- /*#__PURE__*/ _jsx("h1", {
20
- class: "text-2xl font-semibold",
21
- children: siteName
22
- }),
23
- /*#__PURE__*/ _jsxs("nav", {
24
- class: "flex items-center gap-4 text-sm",
25
- children: [
26
- /*#__PURE__*/ _jsx("a", {
27
- href: "/archive",
28
- class: "text-muted-foreground hover:text-foreground",
29
- children: $__i18n._({
30
- id: "B495Gs",
31
- message: "Archive"
32
- })
33
- }),
34
- /*#__PURE__*/ _jsx("a", {
35
- href: "/feed",
36
- class: "text-muted-foreground hover:text-foreground",
37
- children: "RSS"
38
- })
39
- ]
40
- })
41
- ]
42
- }),
43
- /*#__PURE__*/ _jsx("main", {
44
- class: "flex flex-col gap-6",
45
- children: posts.length === 0 ? /*#__PURE__*/ _jsx("p", {
46
- class: "text-muted-foreground",
47
- children: $__i18n._({
48
- id: "ODiSoW",
49
- message: "No posts yet."
50
- })
51
- }) : posts.map((post)=>/*#__PURE__*/ _jsxs("article", {
52
- class: "h-entry",
53
- children: [
54
- post.title && /*#__PURE__*/ _jsx("h2", {
55
- class: "p-name text-lg font-medium mb-2",
56
- children: /*#__PURE__*/ _jsx("a", {
57
- href: `/p/${sqid.encode(post.id)}`,
58
- class: "u-url hover:underline",
59
- children: post.title
60
- })
61
- }),
62
- /*#__PURE__*/ _jsx("div", {
63
- class: "e-content prose prose-sm",
64
- dangerouslySetInnerHTML: {
65
- __html: post.contentHtml || ""
66
- }
67
- }),
68
- /*#__PURE__*/ _jsxs("footer", {
69
- class: "mt-2 text-sm text-muted-foreground",
70
- children: [
71
- /*#__PURE__*/ _jsx("time", {
72
- class: "dt-published",
73
- datetime: time.toISOString(post.publishedAt),
74
- children: time.formatDate(post.publishedAt)
75
- }),
76
- post.visibility === "featured" && /*#__PURE__*/ _jsx("span", {
77
- class: "ml-2 text-xs",
78
- children: $__i18n._({
79
- id: "FkMol5",
80
- message: "Featured"
81
- })
82
- })
83
- ]
84
- })
85
- ]
86
- }, post.id))
87
- }),
88
- posts.length >= 20 && /*#__PURE__*/ _jsx("nav", {
89
- class: "mt-8 text-center",
90
- children: /*#__PURE__*/ _jsx("a", {
91
- href: "/archive",
92
- class: "text-sm text-muted-foreground hover:text-foreground",
93
- children: $__i18n._({
94
- id: "mTOYla",
95
- message: "View all posts →"
96
- })
97
- })
17
+ return /*#__PURE__*/ _jsx(_Fragment, {
18
+ children: feedProps.items.length === 0 ? /*#__PURE__*/ _jsx("p", {
19
+ class: "text-muted-foreground",
20
+ children: $__i18n._({
21
+ id: "ODiSoW",
22
+ message: "No posts yet."
98
23
  })
99
- ]
24
+ }) : /*#__PURE__*/ _jsx(FeedComponent, {
25
+ ...feedProps
26
+ })
100
27
  });
101
28
  }
102
29
  homeRoutes.get("/", async (c)=>{
103
- const siteName = await getSiteName(c);
30
+ const navData = await getNavigationData(c);
31
+ // Fetch one extra to determine if there are more
104
32
  const posts = await c.var.services.posts.list({
105
33
  visibility: [
106
34
  "featured",
107
35
  "quiet"
108
36
  ],
109
- limit: 20
37
+ excludeReplies: true,
38
+ excludeTypes: [
39
+ "page"
40
+ ],
41
+ limit: PAGE_SIZE + 1
110
42
  });
43
+ const hasMore = posts.length > PAGE_SIZE;
44
+ const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
45
+ // Batch load media attachments
46
+ const postIds = displayPosts.map((p)=>p.id);
47
+ const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
48
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
49
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
50
+ const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl);
51
+ // Get reply counts to identify thread roots
52
+ const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
53
+ const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
54
+ // Batch load thread previews
55
+ const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
56
+ // Batch load media for preview replies
57
+ const previewReplyIds = [];
58
+ for (const replies of threadPreviews.values()){
59
+ for (const reply of replies){
60
+ previewReplyIds.push(reply.id);
61
+ }
62
+ }
63
+ const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl) : new Map();
64
+ // Assemble timeline items
65
+ const items = displayPosts.map((post)=>{
66
+ const postWithMedia = {
67
+ ...post,
68
+ mediaAttachments: mediaMap.get(post.id) ?? []
69
+ };
70
+ const replyCount = replyCounts.get(post.id) ?? 0;
71
+ const previewReplies = threadPreviews.get(post.id);
72
+ if (replyCount > 0 && previewReplies) {
73
+ return {
74
+ post: postWithMedia,
75
+ threadPreview: {
76
+ replies: previewReplies.map((r)=>({
77
+ ...r,
78
+ mediaAttachments: previewMediaMap.get(r.id) ?? []
79
+ })),
80
+ totalReplyCount: replyCount
81
+ }
82
+ };
83
+ }
84
+ return {
85
+ post: postWithMedia
86
+ };
87
+ });
88
+ // Determine next cursor
89
+ const lastPost = displayPosts[displayPosts.length - 1];
90
+ const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
91
+ // Resolve theme components
92
+ const Feed = resolveTimelineFeed(DefaultTimelineFeed, c.var.config.theme?.components);
93
+ const feedProps = {
94
+ items,
95
+ hasMore,
96
+ nextCursor
97
+ };
111
98
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
112
- title: siteName,
99
+ title: navData.siteName,
113
100
  c: c,
114
- children: /*#__PURE__*/ _jsx(HomeContent, {
115
- siteName: siteName,
116
- posts: posts
101
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
102
+ ...navData,
103
+ children: /*#__PURE__*/ _jsx(HomeContent, {
104
+ FeedComponent: Feed,
105
+ feedProps: feedProps
106
+ })
117
107
  })
118
108
  }));
119
109
  });
@@ -1,47 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Custom Page Route
5
4
  *
6
5
  * Catch-all route for custom pages accessible via their path field
7
6
  */ import { Hono } from "hono";
8
- import { useLingui as $_useLingui } from "@jant/core/i18n";
9
- import { BaseLayout } from "../../theme/layouts/index.js";
7
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
8
+ import { getNavigationData } from "../../lib/navigation.js";
10
9
  export const pageRoutes = new Hono();
11
10
  function PageContent({ page }) {
12
- const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs("div", {
14
- class: "container py-8 max-w-2xl",
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: "h-entry",
15
13
  children: [
16
- /*#__PURE__*/ _jsxs("article", {
17
- class: "h-entry",
18
- children: [
19
- page.title && /*#__PURE__*/ _jsx("h1", {
20
- class: "p-name text-3xl font-semibold mb-6",
21
- children: page.title
22
- }),
23
- /*#__PURE__*/ _jsx("div", {
24
- class: "e-content prose",
25
- dangerouslySetInnerHTML: {
26
- __html: page.contentHtml || ""
27
- }
28
- })
29
- ]
14
+ page.title && /*#__PURE__*/ _jsx("h1", {
15
+ class: "p-name text-3xl font-semibold mb-6",
16
+ children: page.title
30
17
  }),
31
- /*#__PURE__*/ _jsx("nav", {
32
- class: "mt-8 pt-6 border-t",
33
- children: /*#__PURE__*/ _jsxs("a", {
34
- href: "/",
35
- class: "text-sm hover:underline",
36
- children: [
37
- "←",
38
- " ",
39
- $__i18n._({
40
- id: "x4RuFo",
41
- message: "Back to home"
42
- })
43
- ]
44
- })
18
+ /*#__PURE__*/ _jsx("div", {
19
+ class: "e-content prose",
20
+ dangerouslySetInnerHTML: {
21
+ __html: page.contentHtml || ""
22
+ }
45
23
  })
46
24
  ]
47
25
  });
@@ -59,13 +37,16 @@ pageRoutes.get("/:path", async (c)=>{
59
37
  if (page.visibility === "draft") {
60
38
  return c.notFound();
61
39
  }
62
- const siteName = await getSiteName(c);
40
+ const navData = await getNavigationData(c);
63
41
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
64
- title: `${page.title} - ${siteName}`,
42
+ title: `${page.title} - ${navData.siteName}`,
65
43
  description: page.content?.slice(0, 160),
66
44
  c: c,
67
- children: /*#__PURE__*/ _jsx(PageContent, {
68
- page: page
45
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
46
+ ...navData,
47
+ children: /*#__PURE__*/ _jsx(PageContent, {
48
+ page: page
49
+ })
69
50
  })
70
51
  }));
71
52
  });