@jant/core 0.3.27 → 0.3.28

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 (313) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -1,64 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Home Page Route
4
- *
5
- * Timeline feed with per-type card components and thread previews.
6
- * Uses page-based pagination.
7
- *
8
- * When HOME_DEFAULT_VIEW is "featured", the homepage shows featured posts
9
- * instead of latest. The /latest route always shows latest posts explicitly.
10
- */ import { Hono } from "hono";
11
- import { getNavigationData } from "../../lib/navigation.js";
12
- import { renderPublicPage } from "../../lib/render.js";
13
- import { assembleTimeline } from "../../lib/timeline.js";
14
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
15
- import { HomePage } from "../../ui/pages/HomePage.js";
16
- import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
17
- export const homeRoutes = new Hono();
18
- homeRoutes.get("/", async (c)=>{
19
- const navData = await getNavigationData(c);
20
- if (navData.homeDefaultView === "featured") {
21
- // Show featured posts on homepage
22
- const posts = await c.var.services.posts.list({
23
- featured: true,
24
- status: "published",
25
- excludeReplies: true
26
- });
27
- const mediaCtx = createMediaContext(c);
28
- const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
- const items = postViews.map((post)=>({
30
- post
31
- }));
32
- return renderPublicPage(c, {
33
- title: navData.siteName,
34
- navData,
35
- content: /*#__PURE__*/ _jsx(FeaturedPage, {
36
- items: items
37
- })
38
- });
39
- }
40
- // Default: show latest posts
41
- const pageParam = c.req.query("page");
42
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
43
- const { items, currentPage, totalPages } = await assembleTimeline(c, {
44
- page
45
- });
46
- // Fetch pinned posts
47
- const pinnedPosts = await c.var.services.posts.list({
48
- pinned: true,
49
- status: "published",
50
- excludeReplies: true
51
- });
52
- const mediaCtx = createMediaContext(c);
53
- const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
54
- return renderPublicPage(c, {
55
- title: navData.siteName,
56
- navData,
57
- content: /*#__PURE__*/ _jsx(HomePage, {
58
- items: items,
59
- pinnedItems: pinnedItems,
60
- currentPage: currentPage,
61
- totalPages: totalPages
62
- })
63
- });
64
- });
@@ -1,45 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Latest Page Route
4
- *
5
- * Explicit /latest URL that always shows the latest posts timeline.
6
- * When HOME_DEFAULT_VIEW is "latest" (default), this redirects to /
7
- * to avoid duplicate content. When it's "featured", this serves as
8
- * the explicit latest view.
9
- */ import { Hono } from "hono";
10
- import { getNavigationData } from "../../lib/navigation.js";
11
- import { renderPublicPage } from "../../lib/render.js";
12
- import { assembleTimeline } from "../../lib/timeline.js";
13
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
14
- import { HomePage } from "../../ui/pages/HomePage.js";
15
- export const latestRoutes = new Hono();
16
- latestRoutes.get("/", async (c)=>{
17
- const navData = await getNavigationData(c);
18
- // When homepage already shows latest, redirect to avoid duplicate content
19
- if (navData.homeDefaultView !== "featured") {
20
- return c.redirect("/", 302);
21
- }
22
- const pageParam = c.req.query("page");
23
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
24
- const { items, currentPage, totalPages } = await assembleTimeline(c, {
25
- page
26
- });
27
- // Fetch pinned posts
28
- const pinnedPosts = await c.var.services.posts.list({
29
- pinned: true,
30
- status: "published",
31
- excludeReplies: true
32
- });
33
- const mediaCtx = createMediaContext(c);
34
- const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
35
- return renderPublicPage(c, {
36
- title: `Latest - ${navData.siteName}`,
37
- navData,
38
- content: /*#__PURE__*/ _jsx(HomePage, {
39
- items: items,
40
- pinnedItems: pinnedItems,
41
- currentPage: currentPage,
42
- totalPages: totalPages
43
- })
44
- });
45
- });
@@ -1,68 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Custom Page Route
4
- *
5
- * Serves pages from the pages table and posts with custom paths.
6
- * This is a catch-all route mounted at "/" - must be registered last.
7
- * Supports multi-level paths (e.g. /2024/my-post) for posts.
8
- */ import { Hono } from "hono";
9
- import { SinglePage } from "../../ui/pages/SinglePage.js";
10
- import { PostPage } from "../../ui/pages/PostPage.js";
11
- import { getNavigationData } from "../../lib/navigation.js";
12
- import { renderPublicPage } from "../../lib/render.js";
13
- import { buildMediaMap } from "../../lib/media-helpers.js";
14
- import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
15
- export const pageRoutes = new Hono();
16
- // Catch-all for custom page slugs and post paths (including multi-level)
17
- pageRoutes.get("/*", async (c)=>{
18
- const fullPath = c.req.path.slice(1); // Remove leading /
19
- if (!fullPath) return c.notFound();
20
- const isMultiSegment = fullPath.includes("/");
21
- // Pages only have single-level slugs; skip page lookup for multi-segment paths
22
- if (!isMultiSegment) {
23
- const page = await c.var.services.pages.getBySlug(fullPath);
24
- if (page) {
25
- if (page.status === "draft") {
26
- return c.notFound();
27
- }
28
- const navData = await getNavigationData(c);
29
- const pageView = toPageView(page);
30
- return renderPublicPage(c, {
31
- title: `${page.title || fullPath} - ${navData.siteName}`,
32
- description: page.body?.slice(0, 160),
33
- navData,
34
- content: /*#__PURE__*/ _jsx(SinglePage, {
35
- page: pageView
36
- })
37
- });
38
- }
39
- }
40
- // Posts support multi-level paths
41
- const post = await c.var.services.posts.getByPath(fullPath);
42
- if (post) {
43
- if (post.status === "draft") {
44
- return c.notFound();
45
- }
46
- // Load media attachments
47
- const rawMediaMap = await c.var.services.media.getByPostIds([
48
- post.id
49
- ]);
50
- const mediaCtx = createMediaContext(c);
51
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
52
- const postView = toPostView({
53
- ...post,
54
- mediaAttachments: mediaMap.get(post.id) ?? []
55
- }, mediaCtx);
56
- const navData = await getNavigationData(c);
57
- const title = post.title || navData.siteName;
58
- return renderPublicPage(c, {
59
- title,
60
- description: post.body?.slice(0, 160),
61
- navData,
62
- content: /*#__PURE__*/ _jsx(PostPage, {
63
- post: postView
64
- })
65
- });
66
- }
67
- return c.notFound();
68
- });
@@ -1,44 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Single Post Page Route
4
- */ import { Hono } from "hono";
5
- import { PostPage } from "../../ui/pages/PostPage.js";
6
- import * as sqid from "../../lib/sqid.js";
7
- import { getNavigationData } from "../../lib/navigation.js";
8
- import { renderPublicPage } from "../../lib/render.js";
9
- import { buildMediaMap } from "../../lib/media-helpers.js";
10
- import { createMediaContext, toPostView } from "../../lib/view.js";
11
- export const postRoutes = new Hono();
12
- postRoutes.get("/:id", async (c)=>{
13
- const paramId = c.req.param("id");
14
- // Decode sqid to numeric ID
15
- const id = sqid.decode(paramId);
16
- if (!id) return c.notFound();
17
- const post = await c.var.services.posts.getById(id);
18
- if (!post) return c.notFound();
19
- // Don't show drafts on public site
20
- if (post.status === "draft") {
21
- return c.notFound();
22
- }
23
- // Batch load media attachments
24
- const rawMediaMap = await c.var.services.media.getByPostIds([
25
- post.id
26
- ]);
27
- const mediaCtx = createMediaContext(c);
28
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
29
- // Transform to View Model
30
- const postView = toPostView({
31
- ...post,
32
- mediaAttachments: mediaMap.get(post.id) ?? []
33
- }, mediaCtx);
34
- const navData = await getNavigationData(c);
35
- const title = post.title || navData.siteName;
36
- return renderPublicPage(c, {
37
- title,
38
- description: post.body?.slice(0, 160),
39
- navData,
40
- content: /*#__PURE__*/ _jsx(PostPage, {
41
- post: postView
42
- })
43
- });
44
- });
@@ -1,54 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Search Page Route
4
- */ import { Hono } from "hono";
5
- import { SearchPage } from "../../ui/pages/SearchPage.js";
6
- import { getNavigationData } from "../../lib/navigation.js";
7
- import { renderPublicPage } from "../../lib/render.js";
8
- import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
9
- const PAGE_SIZE = 10;
10
- export const searchRoutes = new Hono();
11
- searchRoutes.get("/", async (c)=>{
12
- const query = c.req.query("q") || "";
13
- const pageParam = c.req.query("page");
14
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
15
- const navData = await getNavigationData(c);
16
- // Only search if there's a query
17
- let results = [];
18
- let error;
19
- let hasMore = false;
20
- if (query.trim()) {
21
- try {
22
- // Fetch one extra to check for more
23
- results = await c.var.services.search.search(query, {
24
- limit: PAGE_SIZE + 1,
25
- offset: (page - 1) * PAGE_SIZE,
26
- status: [
27
- "published"
28
- ]
29
- });
30
- hasMore = results.length > PAGE_SIZE;
31
- if (hasMore) {
32
- results = results.slice(0, PAGE_SIZE);
33
- }
34
- } catch (err) {
35
- // eslint-disable-next-line no-console -- Error logging is intentional
36
- console.error("Search error:", err);
37
- error = "Search failed. Please try again.";
38
- }
39
- }
40
- // Transform to View Models
41
- const mediaCtx = createMediaContext(c);
42
- const resultViews = toSearchResultViews(results, mediaCtx);
43
- return renderPublicPage(c, {
44
- title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
45
- navData,
46
- content: /*#__PURE__*/ _jsx(SearchPage, {
47
- query: query,
48
- results: resultViews,
49
- error: error,
50
- hasMore: hasMore,
51
- page: page
52
- })
53
- });
54
- });
@@ -1,109 +0,0 @@
1
- /**
2
- * Collection Service (v2)
3
- *
4
- * Manages collections. Posts belong to collections via posts.collection_id (1:M).
5
- */ import { eq, asc, sql, desc } from "drizzle-orm";
6
- import { collections, posts } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- export function createCollectionService(db) {
9
- function toCollection(row) {
10
- return {
11
- id: row.id,
12
- slug: row.slug,
13
- title: row.title,
14
- description: row.description,
15
- icon: row.icon,
16
- sortOrder: row.sortOrder,
17
- position: row.position,
18
- showDivider: row.showDivider,
19
- createdAt: row.createdAt,
20
- updatedAt: row.updatedAt
21
- };
22
- }
23
- return {
24
- async getById (id) {
25
- const result = await db.select().from(collections).where(eq(collections.id, id)).limit(1);
26
- return result[0] ? toCollection(result[0]) : null;
27
- },
28
- async getBySlug (slug) {
29
- const result = await db.select().from(collections).where(eq(collections.slug, slug)).limit(1);
30
- return result[0] ? toCollection(result[0]) : null;
31
- },
32
- async list () {
33
- const rows = await db.select().from(collections).orderBy(asc(collections.position), desc(collections.createdAt));
34
- return rows.map(toCollection);
35
- },
36
- async create (data) {
37
- const timestamp = now();
38
- let position = data.position;
39
- if (position === undefined) {
40
- const maxResult = await db.select({
41
- maxPos: sql`COALESCE(MAX(position), -1)`
42
- }).from(collections);
43
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
44
- position = maxResult[0].maxPos + 1;
45
- }
46
- const result = await db.insert(collections).values({
47
- slug: data.slug,
48
- title: data.title,
49
- description: data.description ?? null,
50
- icon: data.icon ?? null,
51
- sortOrder: data.sortOrder ?? "newest",
52
- position,
53
- showDivider: data.showDivider ? 1 : 0,
54
- createdAt: timestamp,
55
- updatedAt: timestamp
56
- }).returning();
57
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
58
- return toCollection(result[0]);
59
- },
60
- async update (id, data) {
61
- const existing = await this.getById(id);
62
- if (!existing) return null;
63
- const timestamp = now();
64
- const updates = {
65
- updatedAt: timestamp
66
- };
67
- if (data.title !== undefined) updates.title = data.title;
68
- if (data.slug !== undefined) updates.slug = data.slug;
69
- if (data.description !== undefined) updates.description = data.description;
70
- if (data.icon !== undefined) updates.icon = data.icon;
71
- if (data.sortOrder !== undefined) updates.sortOrder = data.sortOrder;
72
- if (data.position !== undefined) updates.position = data.position;
73
- if (data.showDivider !== undefined) updates.showDivider = data.showDivider ? 1 : 0;
74
- const result = await db.update(collections).set(updates).where(eq(collections.id, id)).returning();
75
- return result[0] ? toCollection(result[0]) : null;
76
- },
77
- async delete (id) {
78
- // Clear collection_id on posts that belong to this collection
79
- await db.update(posts).set({
80
- collectionId: null
81
- }).where(eq(posts.collectionId, id));
82
- const result = await db.delete(collections).where(eq(collections.id, id)).returning();
83
- return result.length > 0;
84
- },
85
- async reorder (ids) {
86
- const timestamp = now();
87
- for(let i = 0; i < ids.length; i++){
88
- await db.update(collections).set({
89
- position: i,
90
- updatedAt: timestamp
91
- })// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
92
- .where(eq(collections.id, ids[i]));
93
- }
94
- },
95
- async getPostCounts () {
96
- const rows = await db.select({
97
- collectionId: posts.collectionId,
98
- count: sql`count(*)`.as("count")
99
- }).from(posts).where(sql`${posts.collectionId} IS NOT NULL AND ${posts.deletedAt} IS NULL`).groupBy(posts.collectionId);
100
- const counts = new Map();
101
- for (const row of rows){
102
- if (row.collectionId !== null) {
103
- counts.set(row.collectionId, row.count);
104
- }
105
- }
106
- return counts;
107
- }
108
- };
109
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Services (v2)
3
- *
4
- * Business logic layer
5
- */ import { createSettingsService } from "./settings.js";
6
- import { createPostService } from "./post.js";
7
- import { createPageService } from "./page.js";
8
- import { createRedirectService } from "./redirect.js";
9
- import { createMediaService } from "./media.js";
10
- import { createCollectionService } from "./collection.js";
11
- import { createSearchService } from "./search.js";
12
- import { createNavItemService } from "./navigation.js";
13
- export function createServices(db, d1) {
14
- return {
15
- settings: createSettingsService(db),
16
- posts: createPostService(db),
17
- pages: createPageService(db),
18
- redirects: createRedirectService(db),
19
- media: createMediaService(db),
20
- collections: createCollectionService(db),
21
- search: createSearchService(d1),
22
- navItems: createNavItemService(db)
23
- };
24
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * Media Service
3
- *
4
- * Handles media upload and management with pluggable storage backends.
5
- */ import { eq, desc, inArray, asc } from "drizzle-orm";
6
- import { uuidv7 } from "uuidv7";
7
- import { media } from "../db/schema.js";
8
- import { now } from "../lib/time.js";
9
- export function createMediaService(db) {
10
- function toMedia(row) {
11
- return {
12
- id: row.id,
13
- postId: row.postId,
14
- filename: row.filename,
15
- originalName: row.originalName,
16
- mimeType: row.mimeType,
17
- size: row.size,
18
- storageKey: row.storageKey,
19
- provider: row.provider,
20
- width: row.width,
21
- height: row.height,
22
- alt: row.alt,
23
- position: row.position,
24
- blurhash: row.blurhash,
25
- createdAt: row.createdAt
26
- };
27
- }
28
- return {
29
- async getById (id) {
30
- const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
31
- return result[0] ? toMedia(result[0]) : null;
32
- },
33
- async getByIds (ids) {
34
- if (ids.length === 0) return [];
35
- const rows = await db.select().from(media).where(inArray(media.id, ids));
36
- return rows.map(toMedia);
37
- },
38
- async getByPostId (postId) {
39
- const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
40
- return rows.map(toMedia);
41
- },
42
- async getByPostIds (postIds) {
43
- const result = new Map();
44
- if (postIds.length === 0) return result;
45
- const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
46
- for (const row of rows){
47
- const m = toMedia(row);
48
- if (m.postId === null) continue;
49
- const list = result.get(m.postId);
50
- if (list) {
51
- list.push(m);
52
- } else {
53
- result.set(m.postId, [
54
- m
55
- ]);
56
- }
57
- }
58
- return result;
59
- },
60
- async getByStorageKey (storageKey) {
61
- const result = await db.select().from(media).where(eq(media.storageKey, storageKey)).limit(1);
62
- return result[0] ? toMedia(result[0]) : null;
63
- },
64
- async list (limit = 100) {
65
- const rows = await db.select().from(media).orderBy(desc(media.createdAt)).limit(limit);
66
- return rows.map(toMedia);
67
- },
68
- async create (data) {
69
- const id = data.id ?? uuidv7();
70
- const timestamp = now();
71
- const result = await db.insert(media).values({
72
- id,
73
- postId: data.postId ?? null,
74
- filename: data.filename,
75
- originalName: data.originalName,
76
- mimeType: data.mimeType,
77
- size: data.size,
78
- storageKey: data.storageKey,
79
- provider: data.provider ?? "r2",
80
- width: data.width ?? null,
81
- height: data.height ?? null,
82
- alt: data.alt ?? null,
83
- position: data.position ?? 0,
84
- blurhash: data.blurhash ?? null,
85
- createdAt: timestamp
86
- }).returning();
87
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
88
- return toMedia(result[0]);
89
- },
90
- async attachToPost (postId, mediaIds) {
91
- // Clear existing attachments
92
- await db.update(media).set({
93
- postId: null,
94
- position: 0
95
- }).where(eq(media.postId, postId));
96
- // Set new attachments with position = array index
97
- for(let i = 0; i < mediaIds.length; i++){
98
- const mediaId = mediaIds[i];
99
- if (!mediaId) continue;
100
- await db.update(media).set({
101
- postId,
102
- position: i
103
- }).where(eq(media.id, mediaId));
104
- }
105
- },
106
- async detachFromPost (postId) {
107
- await db.update(media).set({
108
- postId: null,
109
- position: 0
110
- }).where(eq(media.postId, postId));
111
- },
112
- async delete (id) {
113
- const result = await db.delete(media).where(eq(media.id, id)).returning();
114
- return result.length > 0;
115
- }
116
- };
117
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Nav Item Service (v2)
3
- *
4
- * Manages navigation items (page links and external links)
5
- */ import { eq, asc, sql } from "drizzle-orm";
6
- import { navItems } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- export function createNavItemService(db) {
9
- function toNavItem(row) {
10
- return {
11
- id: row.id,
12
- type: row.type,
13
- label: row.label,
14
- url: row.url,
15
- pageId: row.pageId,
16
- position: row.position,
17
- createdAt: row.createdAt,
18
- updatedAt: row.updatedAt
19
- };
20
- }
21
- return {
22
- async list () {
23
- const rows = await db.select().from(navItems).orderBy(asc(navItems.position));
24
- return rows.map(toNavItem);
25
- },
26
- async getById (id) {
27
- const result = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
28
- return result[0] ? toNavItem(result[0]) : null;
29
- },
30
- async create (data) {
31
- const timestamp = now();
32
- let position = data.position;
33
- if (position === undefined) {
34
- const maxResult = await db.select({
35
- maxPos: sql`COALESCE(MAX(position), -1)`
36
- }).from(navItems);
37
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
38
- position = maxResult[0].maxPos + 1;
39
- }
40
- const result = await db.insert(navItems).values({
41
- type: data.type,
42
- label: data.label,
43
- url: data.url,
44
- pageId: data.pageId ?? null,
45
- position,
46
- createdAt: timestamp,
47
- updatedAt: timestamp
48
- }).returning();
49
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
50
- return toNavItem(result[0]);
51
- },
52
- async update (id, data) {
53
- const existing = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
54
- if (!existing[0]) return null;
55
- const timestamp = now();
56
- const result = await db.update(navItems).set({
57
- ...data.type !== undefined && {
58
- type: data.type
59
- },
60
- ...data.label !== undefined && {
61
- label: data.label
62
- },
63
- ...data.url !== undefined && {
64
- url: data.url
65
- },
66
- ...data.pageId !== undefined && {
67
- pageId: data.pageId
68
- },
69
- ...data.position !== undefined && {
70
- position: data.position
71
- },
72
- updatedAt: timestamp
73
- }).where(eq(navItems.id, id)).returning();
74
- return result[0] ? toNavItem(result[0]) : null;
75
- },
76
- async delete (id) {
77
- const result = await db.delete(navItems).where(eq(navItems.id, id)).returning();
78
- return result.length > 0;
79
- },
80
- async reorder (ids) {
81
- const timestamp = now();
82
- for(let i = 0; i < ids.length; i++){
83
- await db.update(navItems).set({
84
- position: i,
85
- updatedAt: timestamp
86
- })// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
87
- .where(eq(navItems.id, ids[i]));
88
- }
89
- }
90
- };
91
- }