@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -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/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/src/app.tsx CHANGED
@@ -3,16 +3,17 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import type { FC } from "hono/jsx";
7
6
  import { createDatabase } from "./db/index.js";
8
7
  import { createServices, type Services } from "./services/index.js";
9
8
  import { createAuth, type Auth } from "./auth.js";
10
9
  import { i18nMiddleware } from "./i18n/index.js";
11
- import { useLingui } from "@lingui/react/macro";
12
10
  import type { Bindings, JantConfig } from "./types.js";
13
11
  import { SETTINGS_KEYS } from "./lib/constants.js";
14
- import { theme as threadsTheme } from "./themes/threads/index.js";
15
- import { hashPassword } from "better-auth/crypto";
12
+
13
+ // Routes - Auth
14
+ import { setupRoutes } from "./routes/auth/setup.js";
15
+ import { signinRoutes } from "./routes/auth/signin.js";
16
+ import { resetRoutes } from "./routes/auth/reset.js";
16
17
 
17
18
  // Routes - Pages
18
19
  import { homeRoutes } from "./routes/pages/home.js";
@@ -21,6 +22,9 @@ import { pageRoutes } from "./routes/pages/page.js";
21
22
  import { collectionRoutes } from "./routes/pages/collection.js";
22
23
  import { archiveRoutes } from "./routes/pages/archive.js";
23
24
  import { searchRoutes } from "./routes/pages/search.js";
25
+ import { featuredRoutes } from "./routes/pages/featured.js";
26
+ import { latestRoutes } from "./routes/pages/latest.js";
27
+ import { collectionsPageRoutes } from "./routes/pages/collections.js";
24
28
 
25
29
  // Routes - Dashboard
26
30
  import { dashIndexRoutes } from "./routes/dash/index.js";
@@ -30,12 +34,18 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
30
34
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
31
35
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
32
36
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
33
- import { navigationRoutes as dashNavigationRoutes } from "./routes/dash/navigation.js";
34
37
 
35
38
  // Routes - API
36
39
  import { postsApiRoutes } from "./routes/api/posts.js";
40
+ import { pagesApiRoutes } from "./routes/api/pages.js";
41
+ import { navItemsApiRoutes } from "./routes/api/nav-items.js";
42
+ import { collectionsApiRoutes } from "./routes/api/collections.js";
43
+ import { settingsApiRoutes } from "./routes/api/settings.js";
37
44
  import { uploadApiRoutes } from "./routes/api/upload.js";
38
45
  import { searchApiRoutes } from "./routes/api/search.js";
46
+ // Routes - Compose
47
+ import { composeRoutes } from "./routes/compose.js";
48
+
39
49
  // Routes - Feed
40
50
  import { rssRoutes } from "./routes/feed/rss.js";
41
51
  import { sitemapRoutes } from "./routes/feed/sitemap.js";
@@ -44,11 +54,11 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
44
54
  import { requireAuth } from "./middleware/auth.js";
45
55
  import { requireOnboarding } from "./middleware/onboarding.js";
46
56
 
47
- // Layouts for auth pages
48
- import { BaseLayout } from "./theme/layouts/index.js";
49
- import { dsRedirect, dsToast } from "./lib/sse.js";
50
57
  import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
51
58
  import { createStorageDriver, type StorageDriver } from "./lib/storage.js";
59
+ import { BUILTIN_FONT_THEMES } from "./ui/font-themes.js";
60
+ import { getMediaUrl, getPublicUrlForProvider } from "./lib/image.js";
61
+ import { base64ToUint8Array } from "./lib/favicon.js";
52
62
 
53
63
  // Extend Hono's context variables
54
64
  export interface AppVariables {
@@ -56,7 +66,11 @@ export interface AppVariables {
56
66
  auth: Auth;
57
67
  config: JantConfig;
58
68
  themeStyle: string;
69
+ customCSS: string;
70
+ isAuthenticated: boolean;
59
71
  storage: StorageDriver | null;
72
+ faviconUrl?: string;
73
+ noindex?: boolean;
60
74
  }
61
75
 
62
76
  export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
@@ -76,38 +90,18 @@ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
76
90
  * import { createApp } from "@jant/core";
77
91
  *
78
92
  * export default createApp({
79
- * theme: { components: { PostPage: MyPostPage } },
93
+ * cssVariables: { "--card-radius": "0" },
80
94
  * });
81
95
  * ```
82
96
  */
83
97
  export function createApp(config: JantConfig = {}): App {
84
- // Merge with default threads theme
85
- const defaultTheme = threadsTheme();
86
- const resolvedConfig: JantConfig = {
87
- ...config,
88
- theme: {
89
- name: config.theme?.name ?? defaultTheme.name,
90
- components: {
91
- ...defaultTheme.components,
92
- ...config.theme?.components,
93
- },
94
- timelineMore: config.theme?.timelineMore ?? defaultTheme.timelineMore,
95
- cssVariables: {
96
- ...defaultTheme.cssVariables,
97
- ...config.theme?.cssVariables,
98
- },
99
- colorThemes: config.theme?.colorThemes ?? defaultTheme.colorThemes,
100
- feed: config.theme?.feed,
101
- },
102
- };
98
+ const resolvedConfig: JantConfig = { ...config };
103
99
 
104
100
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
105
101
 
106
102
  // Initialize services, auth, and config middleware
107
103
  app.use("*", async (c, next) => {
108
104
  // Use withSession() to enable D1 Read Replication
109
- // Automatically routes read queries to the nearest replica for lower latency
110
- // See: https://developers.cloudflare.com/d1/best-practices/read-replication/
111
105
  const session = c.env.DB.withSession();
112
106
 
113
107
  // Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
@@ -118,6 +112,13 @@ export function createApp(config: JantConfig = {}): App {
118
112
  c.set("config", resolvedConfig);
119
113
  c.set("storage", createStorageDriver(c.env));
120
114
 
115
+ if (!c.env.AUTH_SECRET) {
116
+ // eslint-disable-next-line no-console -- Startup warning is intentional
117
+ console.warn(
118
+ "[Jant] AUTH_SECRET is not set. Authentication is disabled. Set AUTH_SECRET in .dev.vars or wrangler.toml to enable auth.",
119
+ );
120
+ }
121
+
121
122
  if (c.env.AUTH_SECRET) {
122
123
  const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
123
124
  const auth = createAuth(session as unknown as D1Database, {
@@ -133,18 +134,64 @@ export function createApp(config: JantConfig = {}): App {
133
134
  // Onboarding gate — redirect to /setup if not yet initialized
134
135
  app.use("*", requireOnboarding());
135
136
 
136
- // Theme middleware - resolve active color theme and build CSS
137
+ // Theme middleware - resolve active color theme, font theme, custom CSS, and auth state
137
138
  app.use("*", async (c, next) => {
138
- const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
139
+ const [themeId, fontThemeId, customCSS, noindexValue, avatarKey] =
140
+ await Promise.all([
141
+ c.var.services.settings.get(SETTINGS_KEYS.THEME),
142
+ c.var.services.settings.get("FONT_THEME"),
143
+ c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
144
+ c.var.services.settings.get("NOINDEX"),
145
+ c.var.services.settings.get("SITE_AVATAR"),
146
+ ]);
139
147
  const themes = getAvailableThemes(resolvedConfig);
140
148
  const activeTheme = themeId
141
149
  ? themes.find((t) => t.id === themeId)
142
150
  : undefined;
143
- const themeStyle = buildThemeStyle(
144
- activeTheme,
145
- resolvedConfig.theme?.cssVariables,
146
- );
151
+
152
+ // Build font override CSS variables
153
+ const fontTheme = fontThemeId
154
+ ? BUILTIN_FONT_THEMES.find((f) => f.id === fontThemeId)
155
+ : undefined;
156
+ const fontOverrides: Record<string, string> = {};
157
+ if (fontTheme) {
158
+ fontOverrides["--font-body"] = fontTheme.fontFamily;
159
+ }
160
+
161
+ const themeStyle = buildThemeStyle(activeTheme, {
162
+ ...resolvedConfig.cssVariables,
163
+ ...fontOverrides,
164
+ });
147
165
  c.set("themeStyle", themeStyle);
166
+ c.set("customCSS", customCSS ?? "");
167
+
168
+ // Noindex
169
+ c.set("noindex", noindexValue === "true");
170
+
171
+ // Resolve favicon from avatar storage key
172
+ if (avatarKey) {
173
+ const publicUrl = getPublicUrlForProvider(
174
+ c.env.STORAGE_DRIVER || "r2",
175
+ c.env.R2_PUBLIC_URL,
176
+ c.env.S3_PUBLIC_URL,
177
+ );
178
+ c.set("faviconUrl", getMediaUrl(avatarKey, publicUrl));
179
+ }
180
+
181
+ // Check auth state for data-authenticated attribute on <body>
182
+ let isAuthenticated = false;
183
+ if (c.var.auth) {
184
+ try {
185
+ const session = await c.var.auth.api.getSession({
186
+ headers: c.req.raw.headers,
187
+ });
188
+ isAuthenticated = !!session;
189
+ } catch {
190
+ // Not authenticated
191
+ }
192
+ }
193
+ c.set("isAuthenticated", isAuthenticated);
194
+
148
195
  await next();
149
196
  });
150
197
 
@@ -186,528 +233,50 @@ export function createApp(config: JantConfig = {}): App {
186
233
  }),
187
234
  );
188
235
 
189
- // better-auth handler
190
- app.all("/api/auth/*", async (c) => {
191
- if (!c.var.auth) {
192
- return c.json({ error: "Auth not configured. Set AUTH_SECRET." }, 500);
193
- }
194
- return c.var.auth.handler(c.req.raw);
195
- });
196
-
197
- // API Routes
198
- app.route("/api/posts", postsApiRoutes);
199
-
200
- // Setup page component
201
- const SetupContent: FC = () => {
202
- const { t } = useLingui();
203
-
204
- return (
205
- <div class="min-h-screen flex items-center justify-center">
206
- <div class="card max-w-md w-full">
207
- <header>
208
- <h2>
209
- {t({
210
- message: "Welcome to Jant",
211
- comment: "@context: Setup page welcome heading",
212
- })}
213
- </h2>
214
- <p>
215
- {t({
216
- message: "Create your admin account.",
217
- comment: "@context: Setup page description",
218
- })}
219
- </p>
220
- </header>
221
- <section>
222
- <form
223
- data-signals="{name: '', email: '', password: ''}"
224
- data-on:submit__prevent="@post('/setup')"
225
- data-indicator="_loading"
226
- class="flex flex-col gap-4"
227
- >
228
- <div class="field">
229
- <label class="label">
230
- {t({
231
- message: "Your Name",
232
- comment: "@context: Setup form field - user name",
233
- })}
234
- </label>
235
- <input
236
- type="text"
237
- data-bind="name"
238
- class="input"
239
- required
240
- placeholder="John Doe"
241
- />
242
- </div>
243
- <div class="field">
244
- <label class="label">
245
- {t({
246
- message: "Email",
247
- comment: "@context: Setup/signin form field - email",
248
- })}
249
- </label>
250
- <input
251
- type="email"
252
- data-bind="email"
253
- class="input"
254
- required
255
- placeholder="you@example.com"
256
- />
257
- </div>
258
- <div class="field">
259
- <label class="label">
260
- {t({
261
- message: "Password",
262
- comment: "@context: Setup/signin form field - password",
263
- })}
264
- </label>
265
- <input
266
- type="password"
267
- data-bind="password"
268
- class="input"
269
- required
270
- minLength={8}
271
- />
272
- </div>
273
- <button type="submit" class="btn" data-attr-disabled="$_loading">
274
- <span data-show="!$_loading">
275
- {t({
276
- message: "Complete Setup",
277
- comment: "@context: Setup form submit button",
278
- })}
279
- </span>
280
- <span data-show="$_loading">
281
- {t({
282
- message: "Processing...",
283
- comment:
284
- "@context: Loading text shown on submit button while request is in progress",
285
- })}
286
- </span>
287
- </button>
288
- </form>
289
- </section>
290
- </div>
291
- </div>
292
- );
293
- };
294
-
295
- // Setup page
296
- app.get("/setup", async (c) => {
297
- const isComplete = await c.var.services.settings.isOnboardingComplete();
298
- if (isComplete) return c.redirect("/");
299
-
300
- return c.html(
301
- <BaseLayout title="Setup - Jant" c={c}>
302
- <SetupContent />
303
- </BaseLayout>,
304
- );
305
- });
306
-
307
- app.post("/setup", async (c) => {
308
- const isComplete = await c.var.services.settings.isOnboardingComplete();
309
- if (isComplete) return c.redirect("/");
310
-
311
- const body = await c.req.json<{
312
- name: string;
313
- email: string;
314
- password: string;
315
- }>();
316
- const { name, email, password } = body;
317
-
318
- if (!name || !email || !password) {
319
- return dsToast("All fields are required", "error");
320
- }
236
+ // Favicon routes - serve from DB settings (small files, avoids R2 round-trip)
237
+ app.get("/favicon.ico", async (c) => {
238
+ const data = await c.var.services.settings.get("SITE_FAVICON_ICO");
239
+ if (!data) return c.notFound();
321
240
 
322
- if (password.length < 8) {
323
- return dsToast("Password must be at least 8 characters", "error");
324
- }
325
-
326
- if (!c.var.auth) {
327
- return dsToast("AUTH_SECRET not configured", "error");
328
- }
329
-
330
- try {
331
- const signUpResponse = await c.var.auth.api.signUpEmail({
332
- body: { name, email, password },
333
- });
334
-
335
- if (!signUpResponse || "error" in signUpResponse) {
336
- return dsToast("Failed to create account", "error");
337
- }
338
-
339
- await c.var.services.settings.completeOnboarding();
340
-
341
- return dsRedirect("/signin?setup");
342
- } catch (err) {
343
- // eslint-disable-next-line no-console -- Error logging is intentional
344
- console.error("Setup error:", err);
345
- return dsToast("Failed to create account", "error");
346
- }
241
+ return new Response(base64ToUint8Array(data), {
242
+ headers: {
243
+ "Content-Type": "image/x-icon",
244
+ "Cache-Control": "public, max-age=86400",
245
+ },
246
+ });
347
247
  });
348
248
 
349
- // Signin page component
350
- const SigninContent: FC<{
351
- demoEmail?: string;
352
- demoPassword?: string;
353
- }> = ({ demoEmail, demoPassword }) => {
354
- const { t } = useLingui();
355
- const signals = JSON.stringify({
356
- email: demoEmail || "",
357
- password: demoPassword || "",
358
- }).replace(/</g, "\\u003c");
359
-
360
- return (
361
- <div class="min-h-screen flex items-center justify-center">
362
- <div class="card max-w-md w-full">
363
- <header>
364
- <h2>
365
- {t({
366
- message: "Sign In",
367
- comment: "@context: Sign in page heading",
368
- })}
369
- </h2>
370
- </header>
371
- <section>
372
- {demoEmail && demoPassword && (
373
- <p class="text-muted-foreground text-sm mb-4">
374
- {t({
375
- message: "Demo account pre-filled. Just click Sign In.",
376
- comment:
377
- "@context: Hint shown on signin page when demo credentials are pre-filled",
378
- })}
379
- </p>
380
- )}
381
- <form
382
- data-signals={signals}
383
- data-on:submit__prevent="@post('/signin')"
384
- data-indicator="_loading"
385
- class="flex flex-col gap-4"
386
- >
387
- <div class="field">
388
- <label class="label">
389
- {t({
390
- message: "Email",
391
- comment: "@context: Setup/signin form field - email",
392
- })}
393
- </label>
394
- <input type="email" data-bind="email" class="input" required />
395
- </div>
396
- <div class="field">
397
- <label class="label">
398
- {t({
399
- message: "Password",
400
- comment: "@context: Setup/signin form field - password",
401
- })}
402
- </label>
403
- <input
404
- type="password"
405
- data-bind="password"
406
- class="input"
407
- required
408
- />
409
- </div>
410
- <button type="submit" class="btn" data-attr-disabled="$_loading">
411
- <span data-show="!$_loading">
412
- {t({
413
- message: "Sign In",
414
- comment: "@context: Sign in form submit button",
415
- })}
416
- </span>
417
- <span data-show="$_loading">
418
- {t({
419
- message: "Processing...",
420
- comment:
421
- "@context: Loading text shown on submit button while request is in progress",
422
- })}
423
- </span>
424
- </button>
425
- </form>
426
- </section>
427
- </div>
428
- </div>
429
- );
430
- };
431
-
432
- // Signin page
433
- app.get("/signin", async (c) => {
434
- const isSetup = c.req.query("setup") !== undefined;
435
- const isReset = c.req.query("reset") !== undefined;
436
- let toast: { message: string } | undefined;
437
- if (isSetup) {
438
- toast = { message: "Account created successfully. Please sign in." };
439
- } else if (isReset) {
440
- toast = { message: "Password reset successfully. Please sign in." };
441
- }
249
+ app.get("/apple-touch-icon.png", async (c) => {
250
+ const data = await c.var.services.settings.get("SITE_FAVICON_APPLE_TOUCH");
251
+ if (!data) return c.notFound();
442
252
 
443
- return c.html(
444
- <BaseLayout title="Sign In - Jant" c={c} toast={toast}>
445
- <SigninContent
446
- demoEmail={c.env.DEMO_EMAIL}
447
- demoPassword={c.env.DEMO_PASSWORD}
448
- />
449
- </BaseLayout>,
450
- );
253
+ return new Response(base64ToUint8Array(data), {
254
+ headers: {
255
+ "Content-Type": "image/png",
256
+ "Cache-Control": "public, max-age=86400",
257
+ },
258
+ });
451
259
  });
452
260
 
453
- app.post("/signin", async (c) => {
261
+ // better-auth handler
262
+ app.all("/api/auth/*", async (c) => {
454
263
  if (!c.var.auth) {
455
- return dsToast("Auth not configured", "error");
456
- }
457
-
458
- const body = await c.req.json<{ email: string; password: string }>();
459
- const { email, password } = body;
460
-
461
- try {
462
- const { headers } = await c.var.auth.api.signInEmail({
463
- returnHeaders: true,
464
- body: { email, password },
465
- headers: c.req.raw.headers,
466
- });
467
-
468
- return dsRedirect("/dash", { headers });
469
- } catch {
470
- return dsToast("Invalid email or password", "error");
471
- }
472
- });
473
-
474
- app.get("/signout", async (c) => {
475
- if (c.var.auth) {
476
- try {
477
- await c.var.auth.api.signOut({ headers: c.req.raw.headers });
478
- } catch {
479
- // Ignore signout errors
480
- }
481
- }
482
- return c.redirect("/");
483
- });
484
-
485
- // Password reset via one-time token
486
- const ResetContent: FC<{ token: string }> = ({ token }) => {
487
- const { t } = useLingui();
488
- const signals = JSON.stringify({
489
- password: "",
490
- confirmPassword: "",
491
- token,
492
- }).replace(/</g, "\\u003c");
493
-
494
- return (
495
- <div class="min-h-screen flex items-center justify-center">
496
- <div class="card max-w-md w-full">
497
- <header>
498
- <h2>
499
- {t({
500
- message: "Reset Password",
501
- comment: "@context: Password reset page heading",
502
- })}
503
- </h2>
504
- <p>
505
- {t({
506
- message: "Enter your new password.",
507
- comment: "@context: Password reset page description",
508
- })}
509
- </p>
510
- </header>
511
- <section>
512
- <form
513
- data-signals={signals}
514
- data-on:submit__prevent="@post('/reset')"
515
- data-indicator="_loading"
516
- class="flex flex-col gap-4"
517
- >
518
- <div class="field">
519
- <label class="label">
520
- {t({
521
- message: "New Password",
522
- comment: "@context: Password reset form field",
523
- })}
524
- </label>
525
- <input
526
- type="password"
527
- data-bind="password"
528
- class="input"
529
- required
530
- minLength={8}
531
- autocomplete="new-password"
532
- />
533
- </div>
534
- <div class="field">
535
- <label class="label">
536
- {t({
537
- message: "Confirm Password",
538
- comment: "@context: Password reset form field",
539
- })}
540
- </label>
541
- <input
542
- type="password"
543
- data-bind="confirmPassword"
544
- class="input"
545
- required
546
- minLength={8}
547
- autocomplete="new-password"
548
- />
549
- </div>
550
- <button type="submit" class="btn" data-attr-disabled="$_loading">
551
- <span data-show="!$_loading">
552
- {t({
553
- message: "Reset Password",
554
- comment: "@context: Password reset form submit button",
555
- })}
556
- </span>
557
- <span data-show="$_loading">
558
- {t({
559
- message: "Processing...",
560
- comment:
561
- "@context: Loading text shown on submit button while request is in progress",
562
- })}
563
- </span>
564
- </button>
565
- </form>
566
- </section>
567
- </div>
568
- </div>
569
- );
570
- };
571
-
572
- const ResetErrorContent: FC = () => {
573
- const { t } = useLingui();
574
-
575
- return (
576
- <div class="min-h-screen flex items-center justify-center">
577
- <div class="card max-w-md w-full">
578
- <header>
579
- <h2>
580
- {t({
581
- message: "Invalid or Expired Link",
582
- comment: "@context: Password reset error heading",
583
- })}
584
- </h2>
585
- </header>
586
- <section>
587
- <p class="text-muted-foreground">
588
- {t({
589
- message:
590
- "This password reset link is invalid or has expired. Please generate a new one.",
591
- comment: "@context: Password reset error description",
592
- })}
593
- </p>
594
- </section>
595
- </div>
596
- </div>
597
- );
598
- };
599
-
600
- app.get("/reset", async (c) => {
601
- const token = c.req.query("token");
602
- if (!token) {
603
- return c.html(
604
- <BaseLayout title="Reset Password - Jant" c={c}>
605
- <ResetErrorContent />
606
- </BaseLayout>,
607
- );
608
- }
609
-
610
- const stored = await c.var.services.settings.get(
611
- SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
612
- );
613
- if (!stored) {
614
- return c.html(
615
- <BaseLayout title="Reset Password - Jant" c={c}>
616
- <ResetErrorContent />
617
- </BaseLayout>,
618
- );
619
- }
620
-
621
- const separatorIndex = stored.lastIndexOf(":");
622
- const storedToken = stored.substring(0, separatorIndex);
623
- const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
624
- const now = Math.floor(Date.now() / 1000);
625
-
626
- if (token !== storedToken || now > expiry) {
627
- return c.html(
628
- <BaseLayout title="Reset Password - Jant" c={c}>
629
- <ResetErrorContent />
630
- </BaseLayout>,
631
- );
264
+ return c.json({ error: "Auth not configured. Set AUTH_SECRET." }, 500);
632
265
  }
633
-
634
- return c.html(
635
- <BaseLayout title="Reset Password - Jant" c={c}>
636
- <ResetContent token={token} />
637
- </BaseLayout>,
638
- );
266
+ return c.var.auth.handler(c.req.raw);
639
267
  });
640
268
 
641
- app.post("/reset", async (c) => {
642
- const body = await c.req.json<{
643
- password: string;
644
- confirmPassword: string;
645
- token: string;
646
- }>();
647
- const { password, confirmPassword, token } = body;
648
-
649
- // Validate token
650
- const stored = await c.var.services.settings.get(
651
- SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
652
- );
653
- if (!stored) {
654
- return dsToast("Invalid or expired reset link.", "error");
655
- }
656
-
657
- const separatorIndex = stored.lastIndexOf(":");
658
- const storedToken = stored.substring(0, separatorIndex);
659
- const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
660
- const now = Math.floor(Date.now() / 1000);
661
-
662
- if (token !== storedToken || now > expiry) {
663
- return dsToast("Invalid or expired reset link.", "error");
664
- }
665
-
666
- // Validate passwords
667
- if (!password || password.length < 8) {
668
- return dsToast("Password must be at least 8 characters.", "error");
669
- }
670
-
671
- if (password !== confirmPassword) {
672
- return dsToast("Passwords do not match.", "error");
673
- }
674
-
675
- try {
676
- const hashedPassword = await hashPassword(password);
677
- const db = c.env.DB.withSession() as unknown as D1Database;
678
-
679
- // Get admin user
680
- const userResult = await db
681
- .prepare("SELECT id FROM user LIMIT 1")
682
- .first<{ id: string }>();
683
- if (!userResult) {
684
- return dsToast("No user account found.", "error");
685
- }
269
+ // API Routes
270
+ app.route("/api/posts", postsApiRoutes);
271
+ app.route("/api/pages", pagesApiRoutes);
272
+ app.route("/api/nav-items", navItemsApiRoutes);
273
+ app.route("/api/collections", collectionsApiRoutes);
274
+ app.route("/api/settings", settingsApiRoutes);
686
275
 
687
- // Update password
688
- await db
689
- .prepare(
690
- "UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
691
- )
692
- .bind(hashedPassword, userResult.id)
693
- .run();
694
-
695
- // Delete all sessions
696
- await db
697
- .prepare("DELETE FROM session WHERE user_id = ?")
698
- .bind(userResult.id)
699
- .run();
700
-
701
- // Delete the reset token
702
- await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
703
-
704
- return dsRedirect("/signin?reset");
705
- } catch (err) {
706
- // eslint-disable-next-line no-console -- Error logging is intentional
707
- console.error("Password reset error:", err);
708
- return dsToast("Failed to reset password.", "error");
709
- }
710
- });
276
+ // Auth routes
277
+ app.route("/", setupRoutes);
278
+ app.route("/", signinRoutes);
279
+ app.route("/", resetRoutes);
711
280
 
712
281
  // Dashboard routes (protected)
713
282
  app.use("/dash/*", requireAuth());
@@ -718,39 +287,37 @@ export function createApp(config: JantConfig = {}): App {
718
287
  app.route("/dash/settings", dashSettingsRoutes);
719
288
  app.route("/dash/redirects", dashRedirectsRoutes);
720
289
  app.route("/dash/collections", dashCollectionsRoutes);
721
- app.route("/dash/navigation", dashNavigationRoutes);
722
290
  // API routes
723
291
  app.route("/api/upload", uploadApiRoutes);
724
292
  app.route("/api/search", searchApiRoutes);
725
293
 
726
- // Media files from storage (UUIDv7-based URLs with extension)
727
- app.get("/media/:idWithExt", async (c) => {
294
+ // Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
295
+ app.get("/media/*", async (c) => {
728
296
  const storage = c.var.storage;
729
297
  if (!storage) {
730
298
  return c.notFound();
731
299
  }
732
300
 
733
- // Extract ID from "uuid.ext" format
734
- const idWithExt = c.req.param("idWithExt");
735
- const mediaId = idWithExt.replace(/\.[^.]+$/, "");
736
-
737
- const media = await c.var.services.media.getById(mediaId);
738
- if (!media) {
739
- return c.notFound();
740
- }
741
-
742
- const object = await storage.get(media.storageKey);
301
+ // The storage key is the full path without the leading "/"
302
+ const storageKey = c.req.path.slice(1);
303
+ const object = await storage.get(storageKey);
743
304
  if (!object) {
744
305
  return c.notFound();
745
306
  }
746
307
 
747
308
  const headers = new Headers();
748
- headers.set("Content-Type", object.contentType || media.mimeType);
309
+ headers.set(
310
+ "Content-Type",
311
+ object.contentType || "application/octet-stream",
312
+ );
749
313
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
750
314
 
751
315
  return new Response(object.body, { headers });
752
316
  });
753
317
 
318
+ // Compose route (auth enforced in route middleware)
319
+ app.route("/compose", composeRoutes);
320
+
754
321
  // Feed routes
755
322
  app.route("/feed", rssRoutes);
756
323
  app.route("/", sitemapRoutes);
@@ -758,6 +325,9 @@ export function createApp(config: JantConfig = {}): App {
758
325
  // Frontend routes
759
326
  app.route("/search", searchRoutes);
760
327
  app.route("/archive", archiveRoutes);
328
+ app.route("/featured", featuredRoutes);
329
+ app.route("/latest", latestRoutes);
330
+ app.route("/collections", collectionsPageRoutes);
761
331
  app.route("/c", collectionRoutes);
762
332
  app.route("/p", postRoutes);
763
333
  app.route("/", homeRoutes);