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