@jant/core 0.3.7 → 0.3.9

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 (258) hide show
  1. package/dist/app.js +11 -4
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -1
  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/lib/image.js +39 -15
  8. package/dist/lib/media-helpers.js +49 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/storage.js +164 -0
  12. package/dist/lib/theme-components.js +49 -0
  13. package/dist/routes/api/posts.js +12 -7
  14. package/dist/routes/api/timeline.js +116 -0
  15. package/dist/routes/api/upload.js +35 -24
  16. package/dist/routes/dash/media.js +24 -14
  17. package/dist/routes/dash/navigation.js +274 -0
  18. package/dist/routes/dash/posts.js +4 -1
  19. package/dist/routes/feed/rss.js +3 -2
  20. package/dist/routes/pages/archive.js +14 -27
  21. package/dist/routes/pages/collection.js +10 -19
  22. package/dist/routes/pages/home.js +84 -126
  23. package/dist/routes/pages/page.js +19 -38
  24. package/dist/routes/pages/post.js +47 -56
  25. package/dist/routes/pages/search.js +13 -26
  26. package/dist/services/index.js +3 -1
  27. package/dist/services/media.js +8 -6
  28. package/dist/services/navigation.js +115 -0
  29. package/dist/services/post.js +26 -1
  30. package/dist/theme/components/PostForm.js +4 -3
  31. package/dist/theme/components/PostList.js +5 -0
  32. package/dist/theme/components/index.js +2 -0
  33. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  34. package/dist/theme/components/timeline/ImageCard.js +86 -0
  35. package/dist/theme/components/timeline/LinkCard.js +62 -0
  36. package/dist/theme/components/timeline/NoteCard.js +37 -0
  37. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  38. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  39. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  40. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  41. package/dist/theme/components/timeline/index.js +8 -0
  42. package/dist/theme/layouts/DashLayout.js +8 -0
  43. package/dist/theme/layouts/SiteLayout.js +160 -0
  44. package/dist/theme/layouts/index.js +1 -0
  45. package/dist/types/sortablejs.d.js +5 -0
  46. package/dist/types.js +32 -0
  47. package/package.json +4 -2
  48. package/src/__tests__/helpers/app.ts +1 -0
  49. package/src/__tests__/helpers/db.ts +20 -0
  50. package/src/app.tsx +12 -7
  51. package/src/client.ts +1 -0
  52. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  53. package/src/db/migrations/0004_add_storage_provider.sql +3 -0
  54. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  55. package/src/db/migrations/meta/_journal.json +21 -0
  56. package/src/db/schema.ts +15 -1
  57. package/src/i18n/locales/en.po +148 -80
  58. package/src/i18n/locales/en.ts +1 -1
  59. package/src/i18n/locales/zh-Hans.po +150 -103
  60. package/src/i18n/locales/zh-Hans.ts +1 -1
  61. package/src/i18n/locales/zh-Hant.po +150 -103
  62. package/src/i18n/locales/zh-Hant.ts +1 -1
  63. package/src/index.ts +5 -0
  64. package/src/lib/__tests__/image.test.ts +96 -0
  65. package/src/lib/__tests__/storage.test.ts +162 -0
  66. package/src/lib/__tests__/theme-components.test.ts +107 -0
  67. package/src/lib/image.ts +46 -16
  68. package/src/lib/media-helpers.ts +65 -0
  69. package/src/lib/nav-reorder.ts +26 -0
  70. package/src/lib/navigation.ts +46 -0
  71. package/src/lib/storage.ts +236 -0
  72. package/src/lib/theme-components.ts +76 -0
  73. package/src/routes/api/__tests__/posts.test.ts +8 -8
  74. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  75. package/src/routes/api/posts.ts +20 -6
  76. package/src/routes/api/timeline.tsx +152 -0
  77. package/src/routes/api/upload.ts +52 -25
  78. package/src/routes/dash/media.tsx +40 -8
  79. package/src/routes/dash/navigation.tsx +306 -0
  80. package/src/routes/dash/posts.tsx +5 -0
  81. package/src/routes/feed/rss.ts +3 -2
  82. package/src/routes/pages/archive.tsx +15 -23
  83. package/src/routes/pages/collection.tsx +8 -15
  84. package/src/routes/pages/home.tsx +118 -122
  85. package/src/routes/pages/page.tsx +17 -30
  86. package/src/routes/pages/post.tsx +63 -60
  87. package/src/routes/pages/search.tsx +18 -22
  88. package/src/services/__tests__/media.test.ts +73 -28
  89. package/src/services/__tests__/navigation.test.ts +213 -0
  90. package/src/services/__tests__/post-timeline.test.ts +220 -0
  91. package/src/services/index.ts +7 -0
  92. package/src/services/media.ts +12 -8
  93. package/src/services/navigation.ts +165 -0
  94. package/src/services/post.ts +48 -1
  95. package/src/styles/components.css +59 -0
  96. package/src/theme/components/PostForm.tsx +13 -2
  97. package/src/theme/components/PostList.tsx +7 -0
  98. package/src/theme/components/index.ts +12 -0
  99. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  100. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  101. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  102. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  103. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  104. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  105. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  106. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  107. package/src/theme/components/timeline/index.ts +8 -0
  108. package/src/theme/layouts/DashLayout.tsx +10 -0
  109. package/src/theme/layouts/SiteLayout.tsx +184 -0
  110. package/src/theme/layouts/index.ts +1 -0
  111. package/src/types/sortablejs.d.ts +23 -0
  112. package/src/types.ts +102 -1
  113. package/dist/app.d.ts +0 -38
  114. package/dist/app.d.ts.map +0 -1
  115. package/dist/auth.d.ts +0 -25
  116. package/dist/auth.d.ts.map +0 -1
  117. package/dist/db/index.d.ts +0 -10
  118. package/dist/db/index.d.ts.map +0 -1
  119. package/dist/db/schema.d.ts +0 -1543
  120. package/dist/db/schema.d.ts.map +0 -1
  121. package/dist/i18n/Trans.d.ts +0 -25
  122. package/dist/i18n/Trans.d.ts.map +0 -1
  123. package/dist/i18n/context.d.ts +0 -69
  124. package/dist/i18n/context.d.ts.map +0 -1
  125. package/dist/i18n/detect.d.ts +0 -20
  126. package/dist/i18n/detect.d.ts.map +0 -1
  127. package/dist/i18n/i18n.d.ts +0 -32
  128. package/dist/i18n/i18n.d.ts.map +0 -1
  129. package/dist/i18n/index.d.ts +0 -41
  130. package/dist/i18n/index.d.ts.map +0 -1
  131. package/dist/i18n/locales/en.d.ts +0 -3
  132. package/dist/i18n/locales/en.d.ts.map +0 -1
  133. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  134. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  135. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  136. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  137. package/dist/i18n/locales.d.ts +0 -11
  138. package/dist/i18n/locales.d.ts.map +0 -1
  139. package/dist/i18n/middleware.d.ts +0 -21
  140. package/dist/i18n/middleware.d.ts.map +0 -1
  141. package/dist/index.d.ts +0 -16
  142. package/dist/index.d.ts.map +0 -1
  143. package/dist/lib/config.d.ts +0 -83
  144. package/dist/lib/config.d.ts.map +0 -1
  145. package/dist/lib/constants.d.ts +0 -37
  146. package/dist/lib/constants.d.ts.map +0 -1
  147. package/dist/lib/image.d.ts +0 -73
  148. package/dist/lib/image.d.ts.map +0 -1
  149. package/dist/lib/index.d.ts +0 -9
  150. package/dist/lib/index.d.ts.map +0 -1
  151. package/dist/lib/markdown.d.ts +0 -60
  152. package/dist/lib/markdown.d.ts.map +0 -1
  153. package/dist/lib/schemas.d.ts +0 -130
  154. package/dist/lib/schemas.d.ts.map +0 -1
  155. package/dist/lib/sqid.d.ts +0 -60
  156. package/dist/lib/sqid.d.ts.map +0 -1
  157. package/dist/lib/sse.d.ts +0 -192
  158. package/dist/lib/sse.d.ts.map +0 -1
  159. package/dist/lib/theme.d.ts +0 -44
  160. package/dist/lib/theme.d.ts.map +0 -1
  161. package/dist/lib/time.d.ts +0 -90
  162. package/dist/lib/time.d.ts.map +0 -1
  163. package/dist/lib/url.d.ts +0 -82
  164. package/dist/lib/url.d.ts.map +0 -1
  165. package/dist/middleware/auth.d.ts +0 -24
  166. package/dist/middleware/auth.d.ts.map +0 -1
  167. package/dist/middleware/onboarding.d.ts +0 -26
  168. package/dist/middleware/onboarding.d.ts.map +0 -1
  169. package/dist/routes/api/posts.d.ts +0 -13
  170. package/dist/routes/api/posts.d.ts.map +0 -1
  171. package/dist/routes/api/search.d.ts +0 -13
  172. package/dist/routes/api/search.d.ts.map +0 -1
  173. package/dist/routes/api/upload.d.ts +0 -16
  174. package/dist/routes/api/upload.d.ts.map +0 -1
  175. package/dist/routes/dash/collections.d.ts +0 -13
  176. package/dist/routes/dash/collections.d.ts.map +0 -1
  177. package/dist/routes/dash/index.d.ts +0 -15
  178. package/dist/routes/dash/index.d.ts.map +0 -1
  179. package/dist/routes/dash/media.d.ts +0 -16
  180. package/dist/routes/dash/media.d.ts.map +0 -1
  181. package/dist/routes/dash/pages.d.ts +0 -15
  182. package/dist/routes/dash/pages.d.ts.map +0 -1
  183. package/dist/routes/dash/posts.d.ts +0 -13
  184. package/dist/routes/dash/posts.d.ts.map +0 -1
  185. package/dist/routes/dash/redirects.d.ts +0 -13
  186. package/dist/routes/dash/redirects.d.ts.map +0 -1
  187. package/dist/routes/dash/settings.d.ts +0 -15
  188. package/dist/routes/dash/settings.d.ts.map +0 -1
  189. package/dist/routes/feed/rss.d.ts +0 -13
  190. package/dist/routes/feed/rss.d.ts.map +0 -1
  191. package/dist/routes/feed/sitemap.d.ts +0 -13
  192. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  193. package/dist/routes/pages/archive.d.ts +0 -15
  194. package/dist/routes/pages/archive.d.ts.map +0 -1
  195. package/dist/routes/pages/collection.d.ts +0 -13
  196. package/dist/routes/pages/collection.d.ts.map +0 -1
  197. package/dist/routes/pages/home.d.ts +0 -13
  198. package/dist/routes/pages/home.d.ts.map +0 -1
  199. package/dist/routes/pages/page.d.ts +0 -15
  200. package/dist/routes/pages/page.d.ts.map +0 -1
  201. package/dist/routes/pages/post.d.ts +0 -13
  202. package/dist/routes/pages/post.d.ts.map +0 -1
  203. package/dist/routes/pages/search.d.ts +0 -13
  204. package/dist/routes/pages/search.d.ts.map +0 -1
  205. package/dist/services/collection.d.ts +0 -32
  206. package/dist/services/collection.d.ts.map +0 -1
  207. package/dist/services/index.d.ts +0 -28
  208. package/dist/services/index.d.ts.map +0 -1
  209. package/dist/services/media.d.ts +0 -34
  210. package/dist/services/media.d.ts.map +0 -1
  211. package/dist/services/post.d.ts +0 -31
  212. package/dist/services/post.d.ts.map +0 -1
  213. package/dist/services/redirect.d.ts +0 -15
  214. package/dist/services/redirect.d.ts.map +0 -1
  215. package/dist/services/search.d.ts +0 -26
  216. package/dist/services/search.d.ts.map +0 -1
  217. package/dist/services/settings.d.ts +0 -18
  218. package/dist/services/settings.d.ts.map +0 -1
  219. package/dist/theme/color-themes.d.ts +0 -30
  220. package/dist/theme/color-themes.d.ts.map +0 -1
  221. package/dist/theme/components/ActionButtons.d.ts +0 -43
  222. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  223. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  224. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  225. package/dist/theme/components/DangerZone.d.ts +0 -36
  226. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  227. package/dist/theme/components/EmptyState.d.ts +0 -27
  228. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  229. package/dist/theme/components/ListItemRow.d.ts +0 -15
  230. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  231. package/dist/theme/components/MediaGallery.d.ts +0 -13
  232. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  233. package/dist/theme/components/PageForm.d.ts +0 -14
  234. package/dist/theme/components/PageForm.d.ts.map +0 -1
  235. package/dist/theme/components/Pagination.d.ts +0 -46
  236. package/dist/theme/components/Pagination.d.ts.map +0 -1
  237. package/dist/theme/components/PostForm.d.ts +0 -16
  238. package/dist/theme/components/PostForm.d.ts.map +0 -1
  239. package/dist/theme/components/PostList.d.ts +0 -10
  240. package/dist/theme/components/PostList.d.ts.map +0 -1
  241. package/dist/theme/components/ThreadView.d.ts +0 -15
  242. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  243. package/dist/theme/components/TypeBadge.d.ts +0 -12
  244. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  245. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  246. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  247. package/dist/theme/components/index.d.ts +0 -14
  248. package/dist/theme/components/index.d.ts.map +0 -1
  249. package/dist/theme/index.d.ts +0 -21
  250. package/dist/theme/index.d.ts.map +0 -1
  251. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  252. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  253. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  254. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  255. package/dist/theme/layouts/index.d.ts +0 -3
  256. package/dist/theme/layouts/index.d.ts.map +0 -1
  257. package/dist/types.d.ts +0 -237
  258. package/dist/types.d.ts.map +0 -1
@@ -1,152 +1,110 @@
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 { MediaGallery } from "../../theme/components/index.js";
8
- import * as sqid from "../../lib/sqid.js";
9
- import * as time from "../../lib/time.js";
10
- import { getSiteName } from "../../lib/config.js";
11
- import { getMediaUrl, getImageUrl } from "../../lib/image.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;
12
14
  export const homeRoutes = new Hono();
13
- function HomeContent({ siteName, posts, mediaMap }) {
15
+ function HomeContent({ FeedComponent, feedProps }) {
14
16
  const { i18n: $__i18n, _: $__ } = $_useLingui();
15
- return /*#__PURE__*/ _jsxs("div", {
16
- class: "container py-8",
17
- children: [
18
- /*#__PURE__*/ _jsxs("header", {
19
- class: "mb-8 flex items-center justify-between",
20
- children: [
21
- /*#__PURE__*/ _jsx("h1", {
22
- class: "text-2xl font-semibold",
23
- children: siteName
24
- }),
25
- /*#__PURE__*/ _jsxs("nav", {
26
- class: "flex items-center gap-4 text-sm",
27
- children: [
28
- /*#__PURE__*/ _jsx("a", {
29
- href: "/archive",
30
- class: "text-muted-foreground hover:text-foreground",
31
- children: $__i18n._({
32
- id: "B495Gs",
33
- message: "Archive"
34
- })
35
- }),
36
- /*#__PURE__*/ _jsx("a", {
37
- href: "/feed",
38
- class: "text-muted-foreground hover:text-foreground",
39
- children: "RSS"
40
- })
41
- ]
42
- })
43
- ]
44
- }),
45
- /*#__PURE__*/ _jsx("main", {
46
- class: "flex flex-col gap-6",
47
- children: posts.length === 0 ? /*#__PURE__*/ _jsx("p", {
48
- class: "text-muted-foreground",
49
- children: $__i18n._({
50
- id: "ODiSoW",
51
- message: "No posts yet."
52
- })
53
- }) : posts.map((post)=>{
54
- const attachments = mediaMap.get(post.id) ?? [];
55
- return /*#__PURE__*/ _jsxs("article", {
56
- class: "h-entry",
57
- children: [
58
- post.title && /*#__PURE__*/ _jsx("h2", {
59
- class: "p-name text-lg font-medium mb-2",
60
- children: /*#__PURE__*/ _jsx("a", {
61
- href: `/p/${sqid.encode(post.id)}`,
62
- class: "u-url hover:underline",
63
- children: post.title
64
- })
65
- }),
66
- /*#__PURE__*/ _jsx("div", {
67
- class: "e-content prose prose-sm",
68
- dangerouslySetInnerHTML: {
69
- __html: post.contentHtml || ""
70
- }
71
- }),
72
- attachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
73
- attachments: attachments
74
- }),
75
- /*#__PURE__*/ _jsxs("footer", {
76
- class: "mt-2 text-sm text-muted-foreground",
77
- children: [
78
- /*#__PURE__*/ _jsx("time", {
79
- class: "dt-published",
80
- datetime: time.toISOString(post.publishedAt),
81
- children: time.formatDate(post.publishedAt)
82
- }),
83
- post.visibility === "featured" && /*#__PURE__*/ _jsx("span", {
84
- class: "ml-2 text-xs",
85
- children: $__i18n._({
86
- id: "FkMol5",
87
- message: "Featured"
88
- })
89
- })
90
- ]
91
- })
92
- ]
93
- }, post.id);
94
- })
95
- }),
96
- posts.length >= 20 && /*#__PURE__*/ _jsx("nav", {
97
- class: "mt-8 text-center",
98
- children: /*#__PURE__*/ _jsx("a", {
99
- href: "/archive",
100
- class: "text-sm text-muted-foreground hover:text-foreground",
101
- children: $__i18n._({
102
- id: "mTOYla",
103
- message: "View all posts →"
104
- })
105
- })
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."
106
23
  })
107
- ]
24
+ }) : /*#__PURE__*/ _jsx(FeedComponent, {
25
+ ...feedProps
26
+ })
108
27
  });
109
28
  }
110
29
  homeRoutes.get("/", async (c)=>{
111
- const siteName = await getSiteName(c);
30
+ const navData = await getNavigationData(c);
31
+ // Fetch one extra to determine if there are more
112
32
  const posts = await c.var.services.posts.list({
113
33
  visibility: [
114
34
  "featured",
115
35
  "quiet"
116
36
  ],
117
- limit: 20
37
+ excludeReplies: true,
38
+ excludeTypes: [
39
+ "page"
40
+ ],
41
+ limit: PAGE_SIZE + 1
118
42
  });
43
+ const hasMore = posts.length > PAGE_SIZE;
44
+ const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
119
45
  // Batch load media attachments
120
- const postIds = posts.map((p)=>p.id);
46
+ const postIds = displayPosts.map((p)=>p.id);
121
47
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
122
48
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
123
49
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
124
- const mediaMap = new Map();
125
- for (const [postId, mediaList] of rawMediaMap){
126
- mediaMap.set(postId, mediaList.map((m)=>({
127
- id: m.id,
128
- url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
129
- previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
130
- width: 400,
131
- quality: 80,
132
- format: "auto",
133
- fit: "cover"
134
- }),
135
- alt: m.alt,
136
- blurhash: m.blurhash,
137
- width: m.width,
138
- height: m.height,
139
- position: m.position,
140
- mimeType: m.mimeType
141
- })));
50
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
51
+ const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl);
52
+ // Get reply counts to identify thread roots
53
+ const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
54
+ const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
55
+ // Batch load thread previews
56
+ const threadPreviews = await c.var.services.posts.getThreadPreviews(threadRootIds, 3);
57
+ // Batch load media for preview replies
58
+ const previewReplyIds = [];
59
+ for (const replies of threadPreviews.values()){
60
+ for (const reply of replies){
61
+ previewReplyIds.push(reply.id);
62
+ }
142
63
  }
64
+ const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl, s3PublicUrl) : new Map();
65
+ // Assemble timeline items
66
+ const items = displayPosts.map((post)=>{
67
+ const postWithMedia = {
68
+ ...post,
69
+ mediaAttachments: mediaMap.get(post.id) ?? []
70
+ };
71
+ const replyCount = replyCounts.get(post.id) ?? 0;
72
+ const previewReplies = threadPreviews.get(post.id);
73
+ if (replyCount > 0 && previewReplies) {
74
+ return {
75
+ post: postWithMedia,
76
+ threadPreview: {
77
+ replies: previewReplies.map((r)=>({
78
+ ...r,
79
+ mediaAttachments: previewMediaMap.get(r.id) ?? []
80
+ })),
81
+ totalReplyCount: replyCount
82
+ }
83
+ };
84
+ }
85
+ return {
86
+ post: postWithMedia
87
+ };
88
+ });
89
+ // Determine next cursor
90
+ const lastPost = displayPosts[displayPosts.length - 1];
91
+ const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
92
+ // Resolve theme components
93
+ const Feed = resolveTimelineFeed(DefaultTimelineFeed, c.var.config.theme?.components);
94
+ const feedProps = {
95
+ items,
96
+ hasMore,
97
+ nextCursor
98
+ };
143
99
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
144
- title: siteName,
100
+ title: navData.siteName,
145
101
  c: c,
146
- children: /*#__PURE__*/ _jsx(HomeContent, {
147
- siteName: siteName,
148
- posts: posts,
149
- mediaMap: mediaMap
102
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
103
+ ...navData,
104
+ children: /*#__PURE__*/ _jsx(HomeContent, {
105
+ FeedComponent: Feed,
106
+ feedProps: feedProps
107
+ })
150
108
  })
151
109
  }));
152
110
  });
@@ -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
  });
@@ -1,66 +1,50 @@
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
  * Single Post 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 { MediaGallery } from "../../theme/components/index.js";
9
8
  import * as sqid from "../../lib/sqid.js";
10
9
  import * as time from "../../lib/time.js";
11
- import { getMediaUrl, getImageUrl } from "../../lib/image.js";
10
+ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
11
+ import { getNavigationData } from "../../lib/navigation.js";
12
12
  export const postRoutes = new Hono();
13
13
  function PostContent({ post, mediaAttachments }) {
14
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
15
- return /*#__PURE__*/ _jsxs("div", {
16
- class: "container py-8",
15
+ return /*#__PURE__*/ _jsxs("article", {
16
+ class: "h-entry",
17
17
  children: [
18
- /*#__PURE__*/ _jsxs("article", {
19
- class: "h-entry",
18
+ post.title && /*#__PURE__*/ _jsx("h1", {
19
+ class: "p-name text-2xl font-semibold mb-4",
20
+ children: post.title
21
+ }),
22
+ /*#__PURE__*/ _jsx("div", {
23
+ class: "e-content prose",
24
+ dangerouslySetInnerHTML: {
25
+ __html: post.contentHtml || ""
26
+ }
27
+ }),
28
+ mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
29
+ attachments: mediaAttachments
30
+ }),
31
+ /*#__PURE__*/ _jsxs("footer", {
32
+ class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
20
33
  children: [
21
- post.title && /*#__PURE__*/ _jsx("h1", {
22
- class: "p-name text-2xl font-semibold mb-4",
23
- children: post.title
24
- }),
25
- /*#__PURE__*/ _jsx("div", {
26
- class: "e-content prose",
27
- dangerouslySetInnerHTML: {
28
- __html: post.contentHtml || ""
29
- }
30
- }),
31
- mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
32
- attachments: mediaAttachments
34
+ /*#__PURE__*/ _jsx("time", {
35
+ class: "dt-published",
36
+ datetime: time.toISOString(post.publishedAt),
37
+ children: time.formatDate(post.publishedAt)
33
38
  }),
34
- /*#__PURE__*/ _jsxs("footer", {
35
- class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
36
- children: [
37
- /*#__PURE__*/ _jsx("time", {
38
- class: "dt-published",
39
- datetime: time.toISOString(post.publishedAt),
40
- children: time.formatDate(post.publishedAt)
41
- }),
42
- /*#__PURE__*/ _jsx("a", {
43
- href: `/p/${sqid.encode(post.id)}`,
44
- class: "u-url ml-4",
45
- children: $__i18n._({
46
- id: "D9Oea+",
47
- message: "Permalink"
48
- })
49
- })
50
- ]
39
+ /*#__PURE__*/ _jsx("a", {
40
+ href: `/p/${sqid.encode(post.id)}`,
41
+ class: "u-url ml-4",
42
+ children: $__i18n._({
43
+ id: "D9Oea+",
44
+ message: "Permalink"
45
+ })
51
46
  })
52
47
  ]
53
- }),
54
- /*#__PURE__*/ _jsx("nav", {
55
- class: "mt-8",
56
- children: /*#__PURE__*/ _jsx("a", {
57
- href: "/",
58
- class: "text-sm hover:underline",
59
- children: $__i18n._({
60
- id: "biOepV",
61
- message: "← Back to home"
62
- })
63
- })
64
48
  })
65
49
  ]
66
50
  });
@@ -87,10 +71,13 @@ postRoutes.get("/:id", async (c)=>{
87
71
  const rawMedia = await c.var.services.media.getByPostId(post.id);
88
72
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
89
73
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
90
- const mediaAttachments = rawMedia.map((m)=>({
74
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
75
+ const mediaAttachments = rawMedia.map((m)=>{
76
+ const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
77
+ return {
91
78
  id: m.id,
92
- url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
93
- previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
79
+ url: getMediaUrl(m.id, m.storageKey, publicUrl),
80
+ previewUrl: getImageUrl(getMediaUrl(m.id, m.storageKey, publicUrl), imageTransformUrl, {
94
81
  width: 400,
95
82
  quality: 80,
96
83
  format: "auto",
@@ -102,16 +89,20 @@ postRoutes.get("/:id", async (c)=>{
102
89
  height: m.height,
103
90
  position: m.position,
104
91
  mimeType: m.mimeType
105
- }));
106
- const siteName = await getSiteName(c);
107
- const title = post.title || siteName;
92
+ };
93
+ });
94
+ const navData = await getNavigationData(c);
95
+ const title = post.title || navData.siteName;
108
96
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
109
97
  title: title,
110
98
  description: post.content?.slice(0, 160),
111
99
  c: c,
112
- children: /*#__PURE__*/ _jsx(PostContent, {
113
- post: post,
114
- mediaAttachments: mediaAttachments
100
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
101
+ ...navData,
102
+ children: /*#__PURE__*/ _jsx(PostContent, {
103
+ post: post,
104
+ mediaAttachments: mediaAttachments
105
+ })
115
106
  })
116
107
  }));
117
108
  });
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Search 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 { PagePagination } from "../../theme/components/index.js";
9
8
  import * as sqid from "../../lib/sqid.js";
10
9
  import * as time from "../../lib/time.js";
10
+ import { getNavigationData } from "../../lib/navigation.js";
11
11
  const PAGE_SIZE = 10;
12
12
  export const searchRoutes = new Hono();
13
13
  function SearchContent({ query, results, error, hasMore, page }) {
@@ -17,7 +17,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
17
17
  message: "Search"
18
18
  });
19
19
  return /*#__PURE__*/ _jsxs("div", {
20
- class: "container py-8 max-w-2xl",
21
20
  children: [
22
21
  /*#__PURE__*/ _jsx("h1", {
23
22
  class: "text-2xl font-semibold mb-6",
@@ -118,21 +117,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
118
117
  ]
119
118
  })
120
119
  ]
121
- }),
122
- /*#__PURE__*/ _jsx("nav", {
123
- class: "mt-8 pt-6 border-t",
124
- children: /*#__PURE__*/ _jsxs("a", {
125
- href: "/",
126
- class: "text-sm hover:underline",
127
- children: [
128
- "←",
129
- " ",
130
- $__i18n._({
131
- id: "x4RuFo",
132
- message: "Back to home"
133
- })
134
- ]
135
- })
136
120
  })
137
121
  ]
138
122
  });
@@ -141,7 +125,7 @@ searchRoutes.get("/", async (c)=>{
141
125
  const query = c.req.query("q") || "";
142
126
  const pageParam = c.req.query("page");
143
127
  const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
144
- const siteName = await getSiteName(c);
128
+ const navData = await getNavigationData(c);
145
129
  // Only search if there's a query
146
130
  let results = [];
147
131
  let error = null;
@@ -168,14 +152,17 @@ searchRoutes.get("/", async (c)=>{
168
152
  }
169
153
  }
170
154
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
171
- title: query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`,
155
+ title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
172
156
  c: c,
173
- children: /*#__PURE__*/ _jsx(SearchContent, {
174
- query: query,
175
- results: results,
176
- error: error,
177
- hasMore: hasMore,
178
- page: page
157
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
158
+ ...navData,
159
+ children: /*#__PURE__*/ _jsx(SearchContent, {
160
+ query: query,
161
+ results: results,
162
+ error: error,
163
+ hasMore: hasMore,
164
+ page: page
165
+ })
179
166
  })
180
167
  }));
181
168
  });
@@ -8,6 +8,7 @@ import { createRedirectService } from "./redirect.js";
8
8
  import { createMediaService } from "./media.js";
9
9
  import { createCollectionService } from "./collection.js";
10
10
  import { createSearchService } from "./search.js";
11
+ import { createNavigationLinkService } from "./navigation.js";
11
12
  export function createServices(db, d1) {
12
13
  return {
13
14
  settings: createSettingsService(db),
@@ -15,6 +16,7 @@ export function createServices(db, d1) {
15
16
  redirects: createRedirectService(db),
16
17
  media: createMediaService(db),
17
18
  collections: createCollectionService(db),
18
- search: createSearchService(d1)
19
+ search: createSearchService(d1),
20
+ navigationLinks: createNavigationLinkService(db)
19
21
  };
20
22
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Media Service
3
3
  *
4
- * Handles media upload and management with R2 storage
4
+ * Handles media upload and management with pluggable storage backends.
5
5
  */ import { eq, desc, inArray, asc } from "drizzle-orm";
6
6
  import { uuidv7 } from "uuidv7";
7
7
  import { media } from "../db/schema.js";
@@ -15,7 +15,8 @@ export function createMediaService(db) {
15
15
  originalName: row.originalName,
16
16
  mimeType: row.mimeType,
17
17
  size: row.size,
18
- r2Key: row.r2Key,
18
+ storageKey: row.storageKey,
19
+ provider: row.provider,
19
20
  width: row.width,
20
21
  height: row.height,
21
22
  alt: row.alt,
@@ -56,8 +57,8 @@ export function createMediaService(db) {
56
57
  }
57
58
  return result;
58
59
  },
59
- async getByR2Key (r2Key) {
60
- const result = await db.select().from(media).where(eq(media.r2Key, r2Key)).limit(1);
60
+ async getByStorageKey (storageKey) {
61
+ const result = await db.select().from(media).where(eq(media.storageKey, storageKey)).limit(1);
61
62
  return result[0] ? toMedia(result[0]) : null;
62
63
  },
63
64
  async list (limit = 100) {
@@ -65,7 +66,7 @@ export function createMediaService(db) {
65
66
  return rows.map(toMedia);
66
67
  },
67
68
  async create (data) {
68
- const id = uuidv7();
69
+ const id = data.id ?? uuidv7();
69
70
  const timestamp = now();
70
71
  const result = await db.insert(media).values({
71
72
  id,
@@ -74,7 +75,8 @@ export function createMediaService(db) {
74
75
  originalName: data.originalName,
75
76
  mimeType: data.mimeType,
76
77
  size: data.size,
77
- r2Key: data.r2Key,
78
+ storageKey: data.storageKey,
79
+ provider: data.provider ?? "r2",
78
80
  width: data.width ?? null,
79
81
  height: data.height ?? null,
80
82
  alt: data.alt ?? null,