@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,161 +1,157 @@
1
1
  /**
2
2
  * Home Page Route
3
+ *
4
+ * Timeline feed with per-type card components and thread previews.
3
5
  */
4
6
 
5
7
  import { Hono } from "hono";
6
8
  import { useLingui } from "@lingui/react/macro";
7
- import type { Bindings, Post, MediaAttachment } from "../../types.js";
9
+ import type { FC } from "hono/jsx";
10
+ import type {
11
+ Bindings,
12
+ PostWithMedia,
13
+ TimelineItemData,
14
+ TimelineFeedProps,
15
+ } from "../../types.js";
8
16
  import type { AppVariables } from "../../app.js";
9
- import { BaseLayout } from "../../theme/layouts/index.js";
10
- import { MediaGallery } from "../../theme/components/index.js";
11
- import * as sqid from "../../lib/sqid.js";
12
- import * as time from "../../lib/time.js";
13
- import { getSiteName } from "../../lib/config.js";
14
- import { getMediaUrl, getImageUrl } from "../../lib/image.js";
17
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
18
+ import { buildMediaMap } from "../../lib/media-helpers.js";
19
+ import { resolveTimelineFeed } from "../../lib/theme-components.js";
20
+ import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
21
+ import { getNavigationData } from "../../lib/navigation.js";
15
22
 
16
23
  type Env = { Bindings: Bindings; Variables: AppVariables };
17
24
 
25
+ const PAGE_SIZE = 20;
26
+
18
27
  export const homeRoutes = new Hono<Env>();
19
28
 
20
29
  function HomeContent({
21
- siteName,
22
- posts,
23
- mediaMap,
30
+ FeedComponent,
31
+ feedProps,
24
32
  }: {
25
- siteName: string;
26
- posts: Post[];
27
- mediaMap: Map<number, MediaAttachment[]>;
33
+ FeedComponent: FC<TimelineFeedProps>;
34
+ feedProps: TimelineFeedProps;
28
35
  }) {
29
36
  const { t } = useLingui();
30
37
 
31
38
  return (
32
- <div class="container py-8">
33
- <header class="mb-8 flex items-center justify-between">
34
- <h1 class="text-2xl font-semibold">{siteName}</h1>
35
- <nav class="flex items-center gap-4 text-sm">
36
- <a
37
- href="/archive"
38
- class="text-muted-foreground hover:text-foreground"
39
- >
40
- {t({
41
- message: "Archive",
42
- comment: "@context: Navigation link to archive page",
43
- })}
44
- </a>
45
- <a href="/feed" class="text-muted-foreground hover:text-foreground">
46
- RSS
47
- </a>
48
- </nav>
49
- </header>
50
-
51
- <main class="flex flex-col gap-6">
52
- {posts.length === 0 ? (
53
- <p class="text-muted-foreground">
54
- {t({
55
- message: "No posts yet.",
56
- comment: "@context: Empty state message on home page",
57
- })}
58
- </p>
59
- ) : (
60
- posts.map((post) => {
61
- const attachments = mediaMap.get(post.id) ?? [];
62
- return (
63
- <article key={post.id} class="h-entry">
64
- {post.title && (
65
- <h2 class="p-name text-lg font-medium mb-2">
66
- <a
67
- href={`/p/${sqid.encode(post.id)}`}
68
- class="u-url hover:underline"
69
- >
70
- {post.title}
71
- </a>
72
- </h2>
73
- )}
74
- <div
75
- class="e-content prose prose-sm"
76
- dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
77
- />
78
- {attachments.length > 0 && (
79
- <MediaGallery attachments={attachments} />
80
- )}
81
- <footer class="mt-2 text-sm text-muted-foreground">
82
- <time
83
- class="dt-published"
84
- datetime={time.toISOString(post.publishedAt)}
85
- >
86
- {time.formatDate(post.publishedAt)}
87
- </time>
88
- {post.visibility === "featured" && (
89
- <span class="ml-2 text-xs">
90
- {t({
91
- message: "Featured",
92
- comment: "@context: Post visibility badge",
93
- })}
94
- </span>
95
- )}
96
- </footer>
97
- </article>
98
- );
99
- })
100
- )}
101
- </main>
102
-
103
- {posts.length >= 20 && (
104
- <nav class="mt-8 text-center">
105
- <a
106
- href="/archive"
107
- class="text-sm text-muted-foreground hover:text-foreground"
108
- >
109
- {t({
110
- message: "View all posts →",
111
- comment: "@context: Link to view all posts on archive page",
112
- })}
113
- </a>
114
- </nav>
39
+ <>
40
+ {feedProps.items.length === 0 ? (
41
+ <p class="text-muted-foreground">
42
+ {t({
43
+ message: "No posts yet.",
44
+ comment: "@context: Empty state message on home page",
45
+ })}
46
+ </p>
47
+ ) : (
48
+ <FeedComponent {...feedProps} />
115
49
  )}
116
- </div>
50
+ </>
117
51
  );
118
52
  }
119
53
 
120
54
  homeRoutes.get("/", async (c) => {
121
- const siteName = await getSiteName(c);
55
+ const navData = await getNavigationData(c);
122
56
 
57
+ // Fetch one extra to determine if there are more
123
58
  const posts = await c.var.services.posts.list({
124
59
  visibility: ["featured", "quiet"],
125
- limit: 20,
60
+ excludeReplies: true,
61
+ excludeTypes: ["page"],
62
+ limit: PAGE_SIZE + 1,
126
63
  });
127
64
 
65
+ const hasMore = posts.length > PAGE_SIZE;
66
+ const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
67
+
128
68
  // Batch load media attachments
129
- const postIds = posts.map((p) => p.id);
69
+ const postIds = displayPosts.map((p) => p.id);
130
70
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
131
71
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
132
72
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
73
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
74
+ const mediaMap = buildMediaMap(
75
+ rawMediaMap,
76
+ r2PublicUrl,
77
+ imageTransformUrl,
78
+ s3PublicUrl,
79
+ );
133
80
 
134
- const mediaMap = new Map<number, MediaAttachment[]>();
135
- for (const [postId, mediaList] of rawMediaMap) {
136
- mediaMap.set(
137
- postId,
138
- mediaList.map((m) => ({
139
- id: m.id,
140
- url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
141
- previewUrl: getImageUrl(
142
- getMediaUrl(m.id, m.r2Key, r2PublicUrl),
143
- imageTransformUrl,
144
- { width: 400, quality: 80, format: "auto", fit: "cover" },
145
- ),
146
- alt: m.alt,
147
- blurhash: m.blurhash,
148
- width: m.width,
149
- height: m.height,
150
- position: m.position,
151
- mimeType: m.mimeType,
152
- })),
153
- );
81
+ // Get reply counts to identify thread roots
82
+ const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
83
+ const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
84
+
85
+ // Batch load thread previews
86
+ const threadPreviews = await c.var.services.posts.getThreadPreviews(
87
+ threadRootIds,
88
+ 3,
89
+ );
90
+
91
+ // Batch load media for preview replies
92
+ const previewReplyIds: number[] = [];
93
+ for (const replies of threadPreviews.values()) {
94
+ for (const reply of replies) {
95
+ previewReplyIds.push(reply.id);
96
+ }
154
97
  }
98
+ const previewMediaMap =
99
+ previewReplyIds.length > 0
100
+ ? buildMediaMap(
101
+ await c.var.services.media.getByPostIds(previewReplyIds),
102
+ r2PublicUrl,
103
+ imageTransformUrl,
104
+ s3PublicUrl,
105
+ )
106
+ : new Map();
107
+
108
+ // Assemble timeline items
109
+ const items: TimelineItemData[] = displayPosts.map((post) => {
110
+ const postWithMedia: PostWithMedia = {
111
+ ...post,
112
+ mediaAttachments: mediaMap.get(post.id) ?? [],
113
+ };
114
+
115
+ const replyCount = replyCounts.get(post.id) ?? 0;
116
+ const previewReplies = threadPreviews.get(post.id);
117
+
118
+ if (replyCount > 0 && previewReplies) {
119
+ return {
120
+ post: postWithMedia,
121
+ threadPreview: {
122
+ replies: previewReplies.map((r) => ({
123
+ ...r,
124
+ mediaAttachments: previewMediaMap.get(r.id) ?? [],
125
+ })),
126
+ totalReplyCount: replyCount,
127
+ },
128
+ };
129
+ }
130
+
131
+ return { post: postWithMedia };
132
+ });
133
+
134
+ // Determine next cursor
135
+ const lastPost = displayPosts[displayPosts.length - 1];
136
+ const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
137
+
138
+ // Resolve theme components
139
+ const Feed = resolveTimelineFeed(
140
+ DefaultTimelineFeed,
141
+ c.var.config.theme?.components,
142
+ );
143
+
144
+ const feedProps: TimelineFeedProps = {
145
+ items,
146
+ hasMore,
147
+ nextCursor,
148
+ };
155
149
 
156
150
  return c.html(
157
- <BaseLayout title={siteName} c={c}>
158
- <HomeContent siteName={siteName} posts={posts} mediaMap={mediaMap} />
151
+ <BaseLayout title={navData.siteName} c={c}>
152
+ <SiteLayout {...navData}>
153
+ <HomeContent FeedComponent={Feed} feedProps={feedProps} />
154
+ </SiteLayout>
159
155
  </BaseLayout>,
160
156
  );
161
157
  });
@@ -1,4 +1,3 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Custom Page Route
4
3
  *
@@ -6,41 +5,27 @@ import { getSiteName } from "../../lib/config.js";
6
5
  */
7
6
 
8
7
  import { Hono } from "hono";
9
- import { useLingui } from "@lingui/react/macro";
10
8
  import type { Bindings, Post } from "../../types.js";
11
9
  import type { AppVariables } from "../../app.js";
12
- import { BaseLayout } from "../../theme/layouts/index.js";
10
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
11
+ import { getNavigationData } from "../../lib/navigation.js";
13
12
 
14
13
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
14
 
16
15
  export const pageRoutes = new Hono<Env>();
17
16
 
18
17
  function PageContent({ page }: { page: Post }) {
19
- const { t } = useLingui();
20
-
21
18
  return (
22
- <div class="container py-8 max-w-2xl">
23
- <article class="h-entry">
24
- {page.title && (
25
- <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
26
- )}
27
-
28
- <div
29
- class="e-content prose"
30
- dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
31
- />
32
- </article>
33
-
34
- <nav class="mt-8 pt-6 border-t">
35
- <a href="/" class="text-sm hover:underline">
36
- ←{" "}
37
- {t({
38
- message: "Back to home",
39
- comment: "@context: Navigation link back to home page",
40
- })}
41
- </a>
42
- </nav>
43
- </div>
19
+ <article class="h-entry">
20
+ {page.title && (
21
+ <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
22
+ )}
23
+
24
+ <div
25
+ class="e-content prose"
26
+ dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
27
+ />
28
+ </article>
44
29
  );
45
30
  }
46
31
 
@@ -61,15 +46,17 @@ pageRoutes.get("/:path", async (c) => {
61
46
  return c.notFound();
62
47
  }
63
48
 
64
- const siteName = await getSiteName(c);
49
+ const navData = await getNavigationData(c);
65
50
 
66
51
  return c.html(
67
52
  <BaseLayout
68
- title={`${page.title} - ${siteName}`}
53
+ title={`${page.title} - ${navData.siteName}`}
69
54
  description={page.content?.slice(0, 160)}
70
55
  c={c}
71
56
  >
72
- <PageContent page={page} />
57
+ <SiteLayout {...navData}>
58
+ <PageContent page={page} />
59
+ </SiteLayout>
73
60
  </BaseLayout>,
74
61
  );
75
62
  });
@@ -1,4 +1,3 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Single Post Page Route
4
3
  */
@@ -7,11 +6,16 @@ import { Hono } from "hono";
7
6
  import { useLingui } from "@lingui/react/macro";
8
7
  import type { Bindings, Post, MediaAttachment } from "../../types.js";
9
8
  import type { AppVariables } from "../../app.js";
10
- import { BaseLayout } from "../../theme/layouts/index.js";
9
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
11
10
  import { MediaGallery } from "../../theme/components/index.js";
12
11
  import * as sqid from "../../lib/sqid.js";
13
12
  import * as time from "../../lib/time.js";
14
- import { getMediaUrl, getImageUrl } from "../../lib/image.js";
13
+ import {
14
+ getMediaUrl,
15
+ getImageUrl,
16
+ getPublicUrlForProvider,
17
+ } from "../../lib/image.js";
18
+ import { getNavigationData } from "../../lib/navigation.js";
15
19
 
16
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
17
21
 
@@ -27,46 +31,35 @@ function PostContent({
27
31
  const { t } = useLingui();
28
32
 
29
33
  return (
30
- <div class="container py-8">
31
- <article class="h-entry">
32
- {post.title && (
33
- <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
34
- )}
35
-
36
- <div
37
- class="e-content prose"
38
- dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
39
- />
40
-
41
- {mediaAttachments.length > 0 && (
42
- <MediaGallery attachments={mediaAttachments} />
43
- )}
44
-
45
- <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
46
- <time
47
- class="dt-published"
48
- datetime={time.toISOString(post.publishedAt)}
49
- >
50
- {time.formatDate(post.publishedAt)}
51
- </time>
52
- <a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
53
- {t({
54
- message: "Permalink",
55
- comment: "@context: Link to permanent URL of post",
56
- })}
57
- </a>
58
- </footer>
59
- </article>
60
-
61
- <nav class="mt-8">
62
- <a href="/" class="text-sm hover:underline">
34
+ <article class="h-entry">
35
+ {post.title && (
36
+ <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
37
+ )}
38
+
39
+ <div
40
+ class="e-content prose"
41
+ dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
42
+ />
43
+
44
+ {mediaAttachments.length > 0 && (
45
+ <MediaGallery attachments={mediaAttachments} />
46
+ )}
47
+
48
+ <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
49
+ <time
50
+ class="dt-published"
51
+ datetime={time.toISOString(post.publishedAt)}
52
+ >
53
+ {time.formatDate(post.publishedAt)}
54
+ </time>
55
+ <a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
63
56
  {t({
64
- message: "← Back to home",
65
- comment: "@context: Navigation link",
57
+ message: "Permalink",
58
+ comment: "@context: Link to permanent URL of post",
66
59
  })}
67
60
  </a>
68
- </nav>
69
- </div>
61
+ </footer>
62
+ </article>
70
63
  );
71
64
  }
72
65
 
@@ -98,29 +91,39 @@ postRoutes.get("/:id", async (c) => {
98
91
  const rawMedia = await c.var.services.media.getByPostId(post.id);
99
92
  const r2PublicUrl = c.env.R2_PUBLIC_URL;
100
93
  const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
101
-
102
- const mediaAttachments: MediaAttachment[] = rawMedia.map((m) => ({
103
- id: m.id,
104
- url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
105
- previewUrl: getImageUrl(
106
- getMediaUrl(m.id, m.r2Key, r2PublicUrl),
107
- imageTransformUrl,
108
- { width: 400, quality: 80, format: "auto", fit: "cover" },
109
- ),
110
- alt: m.alt,
111
- blurhash: m.blurhash,
112
- width: m.width,
113
- height: m.height,
114
- position: m.position,
115
- mimeType: m.mimeType,
116
- }));
117
-
118
- const siteName = await getSiteName(c);
119
- const title = post.title || siteName;
94
+ const s3PublicUrl = c.env.S3_PUBLIC_URL;
95
+
96
+ const mediaAttachments: MediaAttachment[] = rawMedia.map((m) => {
97
+ const publicUrl = getPublicUrlForProvider(
98
+ m.provider,
99
+ r2PublicUrl,
100
+ s3PublicUrl,
101
+ );
102
+ return {
103
+ id: m.id,
104
+ url: getMediaUrl(m.id, m.storageKey, publicUrl),
105
+ previewUrl: getImageUrl(
106
+ getMediaUrl(m.id, m.storageKey, publicUrl),
107
+ imageTransformUrl,
108
+ { width: 400, quality: 80, format: "auto", fit: "cover" },
109
+ ),
110
+ alt: m.alt,
111
+ blurhash: m.blurhash,
112
+ width: m.width,
113
+ height: m.height,
114
+ position: m.position,
115
+ mimeType: m.mimeType,
116
+ };
117
+ });
118
+
119
+ const navData = await getNavigationData(c);
120
+ const title = post.title || navData.siteName;
120
121
 
121
122
  return c.html(
122
123
  <BaseLayout title={title} description={post.content?.slice(0, 160)} c={c}>
123
- <PostContent post={post} mediaAttachments={mediaAttachments} />
124
+ <SiteLayout {...navData}>
125
+ <PostContent post={post} mediaAttachments={mediaAttachments} />
126
+ </SiteLayout>
124
127
  </BaseLayout>,
125
128
  );
126
129
  });
@@ -1,4 +1,3 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Search Page Route
4
3
  */
@@ -8,10 +7,11 @@ import { useLingui } from "@lingui/react/macro";
8
7
  import type { Bindings } from "../../types.js";
9
8
  import type { AppVariables } from "../../app.js";
10
9
  import type { SearchResult } from "../../services/search.js";
11
- import { BaseLayout } from "../../theme/layouts/index.js";
10
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
12
11
  import { PagePagination } from "../../theme/components/index.js";
13
12
  import * as sqid from "../../lib/sqid.js";
14
13
  import * as time from "../../lib/time.js";
14
+ import { getNavigationData } from "../../lib/navigation.js";
15
15
 
16
16
  type Env = { Bindings: Bindings; Variables: AppVariables };
17
17
 
@@ -39,7 +39,7 @@ function SearchContent({
39
39
  });
40
40
 
41
41
  return (
42
- <div class="container py-8 max-w-2xl">
42
+ <div>
43
43
  <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
44
44
 
45
45
  {/* Search form */}
@@ -137,16 +137,6 @@ function SearchContent({
137
137
  )}
138
138
  </div>
139
139
  )}
140
-
141
- <nav class="mt-8 pt-6 border-t">
142
- <a href="/" class="text-sm hover:underline">
143
- ←{" "}
144
- {t({
145
- message: "Back to home",
146
- comment: "@context: Navigation link back to home page",
147
- })}
148
- </a>
149
- </nav>
150
140
  </div>
151
141
  );
152
142
  }
@@ -156,7 +146,7 @@ searchRoutes.get("/", async (c) => {
156
146
  const pageParam = c.req.query("page");
157
147
  const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
158
148
 
159
- const siteName = await getSiteName(c);
149
+ const navData = await getNavigationData(c);
160
150
 
161
151
  // Only search if there's a query
162
152
  let results: Awaited<ReturnType<typeof c.var.services.search.search>> = [];
@@ -185,16 +175,22 @@ searchRoutes.get("/", async (c) => {
185
175
 
186
176
  return c.html(
187
177
  <BaseLayout
188
- title={query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`}
178
+ title={
179
+ query
180
+ ? `Search: ${query} - ${navData.siteName}`
181
+ : `Search - ${navData.siteName}`
182
+ }
189
183
  c={c}
190
184
  >
191
- <SearchContent
192
- query={query}
193
- results={results}
194
- error={error}
195
- hasMore={hasMore}
196
- page={page}
197
- />
185
+ <SiteLayout {...navData}>
186
+ <SearchContent
187
+ query={query}
188
+ results={results}
189
+ error={error}
190
+ hasMore={hasMore}
191
+ page={page}
192
+ />
193
+ </SiteLayout>
198
194
  </BaseLayout>,
199
195
  );
200
196
  });