@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Custom URLs API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings } from "../../types.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { CreateCustomUrlSchema, parseValidated } from "../../lib/schemas.js";
10
+ import { parseIdParam, NotFoundError } from "../../lib/errors.js";
11
+ import { DEFAULT_PAGE_SIZE } from "../../lib/constants.js";
12
+
13
+ type Env = { Bindings: Bindings; Variables: AppVariables };
14
+
15
+ export const customUrlsApiRoutes = new Hono<Env>();
16
+
17
+ // List custom URLs (requires auth)
18
+ customUrlsApiRoutes.get("/", requireAuthApi(), async (c) => {
19
+ const pageParam = c.req.query("page");
20
+ const page = Math.max(1, parseInt(pageParam || "1", 10) || 1);
21
+
22
+ const [total, customUrls] = await Promise.all([
23
+ c.var.services.customUrls.count(),
24
+ c.var.services.customUrls.list({
25
+ limit: DEFAULT_PAGE_SIZE,
26
+ offset: (page - 1) * DEFAULT_PAGE_SIZE,
27
+ }),
28
+ ]);
29
+
30
+ const totalPages = Math.max(1, Math.ceil(total / DEFAULT_PAGE_SIZE));
31
+ return c.json({ customUrls, total, page, totalPages });
32
+ });
33
+
34
+ // Create custom URL (requires auth)
35
+ customUrlsApiRoutes.post("/", requireAuthApi(), async (c) => {
36
+ const body = parseValidated(CreateCustomUrlSchema, await c.req.json());
37
+
38
+ const redirectType = body.redirectType
39
+ ? (parseInt(body.redirectType, 10) as 301 | 302)
40
+ : undefined;
41
+
42
+ // Resolve slug → ID for post/collection targets
43
+ let targetId = body.targetId;
44
+ if (body.targetType === "post" && body.targetId) {
45
+ const post = await c.var.services.posts.getBySlug(body.targetId);
46
+ if (!post) {
47
+ throw new NotFoundError(`Post with slug "${body.targetId}" not found`);
48
+ }
49
+ targetId = post.id;
50
+ }
51
+ if (body.targetType === "collection" && body.targetId) {
52
+ const col = await c.var.services.collections.getBySlug(body.targetId);
53
+ if (!col) {
54
+ throw new NotFoundError(
55
+ `Collection with slug "${body.targetId}" not found`,
56
+ );
57
+ }
58
+ targetId = col.id;
59
+ }
60
+
61
+ const customUrl = await c.var.services.customUrls.create({
62
+ path: body.path,
63
+ targetType: body.targetType,
64
+ targetId,
65
+ toPath: body.toPath,
66
+ redirectType,
67
+ });
68
+
69
+ return c.json(customUrl, 201);
70
+ });
71
+
72
+ // Delete custom URL (requires auth)
73
+ customUrlsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
74
+ const id = parseIdParam(c.req.param("id"));
75
+
76
+ const success = await c.var.services.customUrls.delete(id);
77
+ if (!success) throw new NotFoundError("Custom URL");
78
+
79
+ return c.json({ success: true });
80
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Export API Routes
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import type { Bindings } from "../../types.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
+ import { requireAuthApi } from "../../middleware/auth.js";
9
+ import { createExportService } from "../../services/export.js";
10
+
11
+ type Env = { Bindings: Bindings; Variables: AppVariables };
12
+
13
+ export const exportApiRoutes = new Hono<Env>();
14
+
15
+ exportApiRoutes.post("/zola", requireAuthApi(), async (c) => {
16
+ const { services, appConfig } = c.var;
17
+ const exportService = createExportService(services, {
18
+ siteName: appConfig.siteName,
19
+ siteUrl: appConfig.siteUrl,
20
+ siteDescription: appConfig.siteDescription,
21
+ siteLanguage: appConfig.siteLanguage,
22
+ });
23
+ const zip = await exportService.generateZolaSite();
24
+ return new Response(zip, {
25
+ headers: {
26
+ "Content-Type": "application/zip",
27
+ "Content-Disposition": 'attachment; filename="jant-export.zip"',
28
+ "Content-Length": String(zip.byteLength),
29
+ },
30
+ });
31
+ });
@@ -7,20 +7,18 @@ import { z } from "zod";
7
7
  import type { Bindings, NavItemType } from "../../types.js";
8
8
  import type { AppVariables } from "../../types/app-context.js";
9
9
  import { requireAuthApi } from "../../middleware/auth.js";
10
- import {
11
- CreateNavItemSchema,
12
- ReorderSchema,
13
- parseValidated,
14
- } from "../../lib/schemas.js";
15
- import { assertFound, parseIntParam, NotFoundError } from "../../lib/errors.js";
10
+ import { CreateNavItemSchema, parseValidated } from "../../lib/schemas.js";
11
+ import { assertFound, parseIdParam, NotFoundError } from "../../lib/errors.js";
16
12
 
17
13
  type Env = { Bindings: Bindings; Variables: AppVariables };
18
14
 
19
15
  export const navItemsApiRoutes = new Hono<Env>();
20
16
 
21
- // API update schema extends shared schema with nullable pageId for explicit clearing
22
- const UpdateNavItemSchema = CreateNavItemSchema.partial().extend({
23
- pageId: z.number().int().positive().nullable().optional(),
17
+ const UpdateNavItemSchema = CreateNavItemSchema.partial();
18
+
19
+ const MoveSchema = z.object({
20
+ after: z.string().nullable().optional(),
21
+ before: z.string().nullable().optional(),
24
22
  });
25
23
 
26
24
  // List nav items
@@ -29,13 +27,21 @@ navItemsApiRoutes.get("/", async (c) => {
29
27
  return c.json({ navItems: items });
30
28
  });
31
29
 
32
- // Reorder nav items (requires auth) — must be before /:id
33
- navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c) => {
34
- const body = parseValidated(ReorderSchema, await c.req.json());
30
+ // Move nav item (requires auth) — must be before /:id
31
+ navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
32
+ const id = parseIdParam(c.req.param("id"));
33
+ const body = parseValidated(MoveSchema, await c.req.json());
35
34
 
36
- await c.var.services.navItems.reorder(body.ids);
37
- const items = await c.var.services.navItems.list();
38
- return c.json({ navItems: items });
35
+ const item = assertFound(
36
+ await c.var.services.navItems.move(
37
+ id,
38
+ body.after ?? null,
39
+ body.before ?? null,
40
+ ),
41
+ "Nav item",
42
+ );
43
+
44
+ return c.json(item);
39
45
  });
40
46
 
41
47
  // Create nav item (requires auth)
@@ -46,8 +52,6 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
46
52
  type: body.type as NavItemType,
47
53
  label: body.label,
48
54
  url: body.url,
49
- pageId: body.pageId,
50
- position: body.position,
51
55
  });
52
56
 
53
57
  return c.json(item, 201);
@@ -55,7 +59,7 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
55
59
 
56
60
  // Update nav item (requires auth)
57
61
  navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
58
- const id = parseIntParam(c.req.param("id"));
62
+ const id = parseIdParam(c.req.param("id"));
59
63
  const body = parseValidated(UpdateNavItemSchema, await c.req.json());
60
64
 
61
65
  const item = assertFound(
@@ -68,7 +72,7 @@ navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
68
72
 
69
73
  // Delete nav item (requires auth)
70
74
  navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
71
- const id = parseIntParam(c.req.param("id"));
75
+ const id = parseIdParam(c.req.param("id"));
72
76
 
73
77
  const success = await c.var.services.navItems.delete(id);
74
78
  if (!success) throw new NotFoundError("Nav item");
@@ -3,12 +3,14 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import type { Bindings, Format, Status, Media } from "../../types.js";
6
+ import type { Bindings, Media } from "../../types.js";
7
7
  import type { AppVariables } from "../../types/app-context.js";
8
- import * as sqid from "../../lib/sqid.js";
8
+ import { z } from "zod";
9
9
  import {
10
10
  CreatePostSchema,
11
11
  UpdatePostSchema,
12
+ FormatSchema,
13
+ StatusSchema,
12
14
  parseValidated,
13
15
  } from "../../lib/schemas.js";
14
16
  import { requireAuthApi } from "../../middleware/auth.js";
@@ -17,11 +19,7 @@ import {
17
19
  getImageUrl,
18
20
  getPublicUrlForProvider,
19
21
  } from "../../lib/image.js";
20
- import {
21
- assertFound,
22
- NotFoundError,
23
- ValidationError,
24
- } from "../../lib/errors.js";
22
+ import { assertFound, NotFoundError, parseIdParam } from "../../lib/errors.js";
25
23
 
26
24
  type Env = { Bindings: Bindings; Variables: AppVariables };
27
25
 
@@ -49,31 +47,41 @@ function toMediaAttachment(
49
47
  format: "auto",
50
48
  fit: "scale-down",
51
49
  });
50
+ const posterUrl = m.posterKey ? getMediaUrl(m.posterKey, publicUrl) : null;
52
51
 
53
52
  return {
54
53
  id: m.id,
55
54
  url,
56
55
  previewUrl,
56
+ posterUrl,
57
57
  alt: m.alt,
58
58
  blurhash: m.blurhash,
59
59
  width: m.width,
60
60
  height: m.height,
61
61
  position: m.position,
62
62
  mimeType: m.mimeType,
63
+ summary: m.summary,
63
64
  };
64
65
  }
65
66
 
66
- // List posts
67
- postsApiRoutes.get("/", async (c) => {
68
- const format = c.req.query("format") as Format | undefined;
69
- const status = c.req.query("status") as Status | undefined;
70
- const cursor = c.req.query("cursor");
71
- const limit = parseInt(c.req.query("limit") ?? "100", 10);
67
+ const ListPostsQuerySchema = z.object({
68
+ format: FormatSchema.optional(),
69
+ status: StatusSchema.optional(),
70
+ cursor: z.string().optional(),
71
+ limit: z.coerce.number().int().min(1).max(100).optional().default(100),
72
+ });
73
+
74
+ // List posts (requires auth)
75
+ postsApiRoutes.get("/", requireAuthApi(), async (c) => {
76
+ const { format, status, cursor, limit } = parseValidated(
77
+ ListPostsQuerySchema,
78
+ c.req.query(),
79
+ );
72
80
 
73
81
  const posts = await c.var.services.posts.list({
74
82
  format,
75
83
  status: status ?? "published",
76
- cursor: cursor ? (sqid.decode(cursor) ?? undefined) : undefined,
84
+ cursor: cursor ?? undefined,
77
85
  limit,
78
86
  });
79
87
 
@@ -85,32 +93,33 @@ postsApiRoutes.get("/", async (c) => {
85
93
  return c.json({
86
94
  posts: posts.map((p) => ({
87
95
  ...p,
88
- sqid: sqid.encode(p.id),
89
96
  mediaAttachments: (mediaMap.get(p.id) ?? []).map((m) =>
90
97
  toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
91
98
  ),
92
99
  })),
93
100
 
94
101
  nextCursor:
95
- posts.length === limit
96
- ? sqid.encode(posts[posts.length - 1]?.id ?? 0)
97
- : null,
102
+ posts.length === limit ? (posts[posts.length - 1]?.id ?? null) : null,
98
103
  });
99
104
  });
100
105
 
101
- // Get single post
102
- postsApiRoutes.get("/:id", async (c) => {
103
- const id = sqid.decode(c.req.param("id"));
104
- if (!id) throw new ValidationError("Invalid ID");
106
+ // Get single post (requires auth)
107
+ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
108
+ const id = parseIdParam(c.req.param("id"));
105
109
 
106
110
  const post = assertFound(await c.var.services.posts.getById(id), "Post");
107
111
 
108
112
  const mediaList = await c.var.services.media.getByPostId(post.id);
109
113
  const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
110
114
 
115
+ // Get collection IDs for this post
116
+ const postCollections =
117
+ await c.var.services.collections.getCollectionsByPostId(post.id);
118
+ const collectionIds = postCollections.map((col) => col.id);
119
+
111
120
  return c.json({
112
121
  ...post,
113
- sqid: sqid.encode(post.id),
122
+ collectionIds,
114
123
  mediaAttachments: mediaList.map((m) =>
115
124
  toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
116
125
  ),
@@ -126,23 +135,30 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
126
135
  await c.var.services.media.validateIds(body.mediaIds);
127
136
  }
128
137
 
129
- const post = await c.var.services.posts.create({
130
- format: body.format,
131
- title: body.title,
132
- body: body.body,
133
- path: body.path || undefined,
134
- status: body.status,
135
- featured: body.featured,
136
- pinned: body.pinned,
137
- url: body.url || undefined,
138
- quoteText: body.quoteText,
139
- rating: body.rating || undefined,
140
- collectionIds: body.collectionIds?.length ? body.collectionIds : undefined,
141
- replyToId: body.replyToId
142
- ? (sqid.decode(body.replyToId) ?? undefined)
143
- : undefined,
144
- publishedAt: body.publishedAt,
145
- });
138
+ const post = await c.var.services.posts.create(
139
+ {
140
+ format: body.format,
141
+ title: body.title,
142
+ body: body.body,
143
+ bodyMarkdown: body.bodyMarkdown,
144
+ slug: body.slug || undefined,
145
+ path: body.path || undefined,
146
+ status: body.status,
147
+ visibility: body.visibility,
148
+ pinned: body.pinned,
149
+ featured: body.featured,
150
+ url: body.url || undefined,
151
+ quoteText: body.quoteText,
152
+ rating: body.rating || undefined,
153
+ collectionIds: body.collectionIds,
154
+ replyToId: body.replyToId,
155
+ publishedAt: body.publishedAt,
156
+ },
157
+ {
158
+ maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
159
+ maxChars: c.var.appConfig.summaryMaxChars,
160
+ },
161
+ );
146
162
 
147
163
  // Attach media
148
164
  if (body.mediaIds && body.mediaIds.length > 0) {
@@ -155,7 +171,6 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
155
171
  return c.json(
156
172
  {
157
173
  ...post,
158
- sqid: sqid.encode(post.id),
159
174
  mediaAttachments: mediaList.map((m) =>
160
175
  toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
161
176
  ),
@@ -166,8 +181,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
166
181
 
167
182
  // Update post (requires auth)
168
183
  postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
169
- const id = sqid.decode(c.req.param("id"));
170
- if (!id) throw new ValidationError("Invalid ID");
184
+ const id = parseIdParam(c.req.param("id"));
171
185
 
172
186
  const body = parseValidated(UpdatePostSchema, await c.req.json());
173
187
 
@@ -177,22 +191,29 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
177
191
  }
178
192
 
179
193
  const post = assertFound(
180
- await c.var.services.posts.update(id, {
181
- format: body.format,
182
- title: body.title,
183
- body: body.body,
184
- path: body.path,
185
- status: body.status,
186
- featured: body.featured,
187
- pinned: body.pinned,
188
- url: body.url,
189
- quoteText: body.quoteText,
190
- rating: body.rating || undefined,
191
- collectionIds: body.collectionIds?.length
192
- ? body.collectionIds
193
- : undefined,
194
- publishedAt: body.publishedAt,
195
- }),
194
+ await c.var.services.posts.update(
195
+ id,
196
+ {
197
+ format: body.format,
198
+ title: body.title,
199
+ body: body.body,
200
+ bodyMarkdown: body.bodyMarkdown,
201
+ slug: body.slug,
202
+ status: body.status,
203
+ visibility: body.visibility,
204
+ pinned: body.pinned,
205
+ featured: body.featured,
206
+ url: body.url,
207
+ quoteText: body.quoteText,
208
+ rating: body.rating || undefined,
209
+ collectionIds: body.collectionIds,
210
+ publishedAt: body.publishedAt,
211
+ },
212
+ {
213
+ maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
214
+ maxChars: c.var.appConfig.summaryMaxChars,
215
+ },
216
+ ),
196
217
  "Post",
197
218
  );
198
219
 
@@ -206,7 +227,6 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
206
227
 
207
228
  return c.json({
208
229
  ...post,
209
- sqid: sqid.encode(post.id),
210
230
  mediaAttachments: mediaList.map((m) =>
211
231
  toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
212
232
  ),
@@ -215,8 +235,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
215
235
 
216
236
  // Delete post (requires auth)
217
237
  postsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
218
- const id = sqid.decode(c.req.param("id"));
219
- if (!id) throw new ValidationError("Invalid ID");
238
+ const id = parseIdParam(c.req.param("id"));
220
239
 
221
240
  const success = await c.var.services.posts.delete(id, {
222
241
  media: c.var.services.media,
@@ -5,7 +5,6 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../types/app-context.js";
8
- import * as sqid from "../../lib/sqid.js";
9
8
  import { ValidationError, ExternalServiceError } from "../../lib/errors.js";
10
9
 
11
10
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -36,13 +35,13 @@ searchApiRoutes.get("/", async (c) => {
36
35
  return c.json({
37
36
  query,
38
37
  results: results.map((r) => ({
39
- id: sqid.encode(r.post.id),
38
+ id: r.post.id,
40
39
  format: r.post.format,
41
40
  title: r.post.title,
42
- path: r.post.path,
41
+ slug: r.post.slug,
43
42
  snippet: r.snippet,
44
43
  publishedAt: r.post.publishedAt,
45
- url: r.post.path ? `/${r.post.path}` : `/p/${sqid.encode(r.post.id)}`,
44
+ url: `/${r.post.slug}`,
46
45
  })),
47
46
  count: results.length,
48
47
  });