@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.35",
3
+ "version": "0.3.37",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,19 +23,36 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@aws-sdk/client-s3": "^3.987.0",
26
+ "@emoji-mart/data": "^1.2.1",
26
27
  "@lingui/core": "^5.9.0",
27
28
  "@lingui/react": "^5.9.0",
28
29
  "@tailwindcss/typography": "^0.5.19",
30
+ "@tiptap/core": "^3.20.0",
31
+ "@tiptap/extension-image": "^3.20.0",
32
+ "@tiptap/extension-placeholder": "^3.20.0",
33
+ "@tiptap/extension-table": "^3.20.0",
34
+ "@tiptap/pm": "^3.20.0",
35
+ "@tiptap/starter-kit": "^3.20.0",
36
+ "@tiptap/suggestion": "^3.20.0",
29
37
  "basecoat-css": "^0.3.10",
30
38
  "better-auth": "^1.4.18",
39
+ "blurhash": "^2.0.5",
31
40
  "drizzle-orm": "^0.45.1",
41
+ "emoji-mart": "^5.6.0",
42
+ "fflate": "^0.8.2",
43
+ "fractional-indexing": "^3.2.0",
44
+ "heic-to": "^1.4.2",
45
+ "limax": "^4.2.2",
32
46
  "lit": "^3.3.2",
33
47
  "lucide-static": "^0.574.0",
34
48
  "marked": "^17.0.1",
35
- "pinyin-pro": "^3.28.0",
49
+ "mediabunny": "^1.35.1",
50
+ "nanoid": "^5.1.6",
51
+ "smol-toml": "^1.6.0",
36
52
  "sortablejs": "^1.15.6",
37
- "sqids": "^0.3.0",
53
+ "tiptap-markdown": "^0.9.0",
38
54
  "uuidv7": "^1.1.0",
55
+ "yaml": "^2.8.2",
39
56
  "zod": "^4.3.6"
40
57
  },
41
58
  "peerDependencies": {
@@ -50,13 +67,13 @@
50
67
  "@lingui/format-po": "^5.9.0",
51
68
  "@lingui/swc-plugin": "^5.10.1",
52
69
  "@swc/core": "^1.15.11",
70
+ "@tailwindcss/vite": "^4.1.18",
53
71
  "@types/better-sqlite3": "^7.6.13",
54
72
  "@types/node": "^25.1.0",
55
73
  "better-sqlite3": "^12.6.2",
56
74
  "drizzle-kit": "^0.31.8",
57
75
  "glob": "^13.0.0",
58
76
  "happy-dom": "^20.6.3",
59
- "@tailwindcss/vite": "^4.1.18",
60
77
  "tailwindcss": "^4.1.18",
61
78
  "tsx": "^4.21.0",
62
79
  "typescript": "^5.9.3",
@@ -94,11 +111,11 @@
94
111
  "build:lib": "vite build --config vite.config.worker.ts",
95
112
  "typecheck": "tsc -b --noEmit",
96
113
  "db:generate": "drizzle-kit generate",
97
- "i18n:extract": "lingui extract",
114
+ "i18n:extract": "lingui extract --clean",
98
115
  "i18n:compile": "lingui compile --typescript",
99
116
  "i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
100
117
  "dev": "pnpm db:migrate:local && vite dev",
101
- "dev:debug": "pnpm db:migrate:local && vite dev --port 19019",
118
+ "dev:debug": "pnpm db:migrate:local && vite dev --port 19020",
102
119
  "db:migrate:local": "echo y | wrangler d1 migrations apply DB --local",
103
120
  "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
104
121
  "test": "vitest run",
@@ -9,15 +9,15 @@ import type { Bindings } from "../../types.js";
9
9
  import type { AppVariables } from "../../types/app-context.js";
10
10
  import { createTestDatabase } from "./db.js";
11
11
  import { createPostService } from "../../services/post.js";
12
- import { createPageService } from "../../services/page.js";
12
+ import { createPathService } from "../../services/path.js";
13
13
  import { createSettingsService } from "../../services/settings.js";
14
- import { createRedirectService } from "../../services/redirect.js";
14
+ import { createCustomUrlService } from "../../services/custom-url.js";
15
15
  import { createMediaService } from "../../services/media.js";
16
16
  import { createCollectionService } from "../../services/collection.js";
17
17
  import { createSearchService } from "../../services/search.js";
18
18
  import { createNavItemService } from "../../services/navigation.js";
19
19
  import { createAuthService } from "../../services/auth.js";
20
- import { createPathRegistryService } from "../../services/path-registry.js";
20
+ import { createApiTokenService } from "../../services/api-token.js";
21
21
  import type { Database } from "../../db/index.js";
22
22
  import type BetterSqlite3 from "better-sqlite3";
23
23
  import { errorHandler } from "../../middleware/error-handler.js";
@@ -46,18 +46,18 @@ export function createTestApp(options: TestAppOptions = {}) {
46
46
  const mockD1 = createMockD1(sqlite);
47
47
 
48
48
  const settingsService = createSettingsService(db);
49
- const pathRegistryService = createPathRegistryService(db);
49
+ const pathService = createPathService(db);
50
50
  const services = {
51
- posts: createPostService(db, pathRegistryService),
52
- pages: createPageService(db, pathRegistryService),
51
+ paths: pathService,
52
+ posts: createPostService(db, { slugIdLength: 5 }, pathService),
53
53
  settings: settingsService,
54
- pathRegistry: pathRegistryService,
55
- redirects: createRedirectService(db, pathRegistryService),
54
+ customUrls: createCustomUrlService(db, pathService),
56
55
  media: createMediaService(db),
57
- collections: createCollectionService(db),
56
+ collections: createCollectionService(db, pathService),
58
57
  search: createSearchService(mockD1),
59
58
  navItems: createNavItemService(db),
60
59
  auth: createAuthService(db, settingsService),
60
+ apiTokens: createApiTokenService(db),
61
61
  };
62
62
 
63
63
  const app = new Hono<Env>();
@@ -69,7 +69,7 @@ export function createTestApp(options: TestAppOptions = {}) {
69
69
  app.use("*", async (c, next) => {
70
70
  // Provide mock env bindings so c.env.* works in route handlers
71
71
  c.env = {
72
- SITE_URL: "http://localhost:9019",
72
+ SITE_URL: "http://localhost:9020",
73
73
  } as AppVariables["services"] extends never ? never : Bindings;
74
74
 
75
75
  c.set("services", services as AppVariables["services"]);
@@ -1,140 +1,144 @@
1
1
  /**
2
2
  * Test Database Helper
3
3
  *
4
- * Creates an in-memory SQLite database with all migrations applied (up to v2).
4
+ * Creates an in-memory SQLite database with all migrations applied.
5
5
  * Used for service integration tests.
6
6
  */
7
7
 
8
8
  import Database from "better-sqlite3";
9
9
  import { drizzle } from "drizzle-orm/better-sqlite3";
10
10
  import * as schema from "../../db/schema.js";
11
- import { readFileSync } from "fs";
11
+ import { readFileSync, readdirSync } from "fs";
12
12
  import { resolve } from "path";
13
13
 
14
14
  const MIGRATIONS_DIR = resolve(import.meta.dirname, "../../db/migrations");
15
15
 
16
16
  /**
17
17
  * Applies a migration file, splitting on Drizzle statement breakpoints.
18
+ * When `skipFts` is true, silently skips statements that reference the
19
+ * FTS virtual table (triggers, rebuild) since it may not exist.
18
20
  */
19
- function applyMigration(sqlite: Database.Database, filename: string) {
21
+ function applyMigration(
22
+ sqlite: Database.Database,
23
+ filename: string,
24
+ options?: { skipFts?: boolean },
25
+ ) {
20
26
  const migration = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
21
27
  for (const sql of migration.split("--> statement-breakpoint")) {
22
28
  const trimmed = sql.trim();
23
- if (trimmed) sqlite.exec(trimmed);
29
+ if (!trimmed) continue;
30
+ if (options?.skipFts && trimmed.includes("post_fts")) continue;
31
+ sqlite.exec(trimmed);
24
32
  }
25
33
  }
26
34
 
27
35
  /**
28
- * Creates a fresh in-memory SQLite database with all migrations applied.
29
- * Each call returns an isolated database instance for test isolation.
30
- *
31
- * @param options.fts - Whether to enable FTS5 for search tests (default: false).
32
- * The trigram tokenizer used in production may not be available in all
33
- * better-sqlite3 builds, so FTS is opt-in for tests that need it.
36
+ * Applies the FTS migration with fallback for environments lacking
37
+ * the trigram tokenizer.
34
38
  */
35
- export function createTestDatabase(options?: { fts?: boolean }) {
36
- const sqlite = new Database(":memory:");
37
-
38
- // Enable WAL mode for better performance
39
- sqlite.pragma("journal_mode = WAL");
40
- sqlite.pragma("foreign_keys = ON");
41
-
42
- // Apply v1 base migrations (0000-0004)
43
- applyMigration(sqlite, "0000_square_wallflower.sql");
44
- // Skip 0001 (FTS) — v2 migration will create updated FTS if needed
45
- applyMigration(sqlite, "0002_add_media_attachments.sql");
46
- applyMigration(sqlite, "0003_add_navigation_links.sql");
47
- applyMigration(sqlite, "0004_add_storage_provider.sql");
48
-
49
- // Apply v2 schema migration (0005)
50
- // Split FTS-related statements so we can handle them separately
51
- const v2Migration = readFileSync(
52
- resolve(MIGRATIONS_DIR, "0005_v2_schema_migration.sql"),
53
- "utf-8",
54
- );
55
-
56
- for (const stmt of v2Migration.split("--> statement-breakpoint")) {
39
+ function applyFtsMigration(sqlite: Database.Database, filename: string) {
40
+ const ftsSql = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
41
+ for (const stmt of ftsSql.split("--> statement-breakpoint")) {
57
42
  const trimmed = stmt.trim();
58
43
  if (!trimmed) continue;
59
-
60
- // Skip FTS-related statements if FTS not requested
61
- const isFts = trimmed.includes("posts_fts");
62
- if (!options?.fts && isFts) continue;
63
-
64
44
  try {
65
45
  sqlite.exec(trimmed);
66
46
  } catch {
67
- // Handle trigram tokenizer failure for FTS virtual table
68
- if (options?.fts && trimmed.includes("CREATE VIRTUAL TABLE")) {
47
+ // Trigram tokenizer may not be available — fall back to default tokenizer
48
+ if (trimmed.includes("CREATE VIRTUAL TABLE")) {
69
49
  sqlite.exec(`
70
- CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
50
+ CREATE VIRTUAL TABLE IF NOT EXISTS post_fts USING fts5(
71
51
  title,
72
- body,
52
+ body_text,
73
53
  quote_text,
74
- content='posts',
75
- content_rowid='id'
54
+ url,
55
+ content='post',
56
+ content_rowid='rowid'
76
57
  );
77
58
  `);
78
59
  }
79
- // Ignore DROP TRIGGER/TABLE IF EXISTS failures silently
80
- else if (
81
- !trimmed.startsWith("DROP TRIGGER") &&
82
- !trimmed.startsWith("DROP TABLE")
83
- ) {
84
- throw new Error(`Migration statement failed: ${trimmed.slice(0, 100)}`);
85
- }
86
- }
87
- }
88
-
89
- // Apply 0006: rename slug to path on posts
90
- applyMigration(sqlite, "0006_rename_slug_to_path.sql");
91
-
92
- // Apply 0007: post_collections M:N junction table
93
- const m7 = readFileSync(
94
- resolve(MIGRATIONS_DIR, "0007_post_collections_m2m.sql"),
95
- "utf-8",
96
- );
97
- for (const stmt of m7.split("--> statement-breakpoint")) {
98
- const trimmed = stmt.trim();
99
- if (!trimmed) continue;
100
- // Skip FTS trigger statements if FTS not requested
101
- const isFts = trimmed.includes("posts_fts");
102
- if (!options?.fts && isFts) continue;
103
- try {
104
- sqlite.exec(trimmed);
105
- } catch {
106
- // Ignore DROP TRIGGER failures silently
107
- if (!trimmed.startsWith("DROP TRIGGER")) {
108
- throw new Error(`Migration 0007 failed: ${trimmed.slice(0, 100)}`);
60
+ // Ignore trigger failures if virtual table creation failed
61
+ else if (!trimmed.startsWith("CREATE TRIGGER")) {
62
+ throw new Error(
63
+ `FTS migration statement failed: ${trimmed.slice(0, 100)}`,
64
+ );
109
65
  }
110
66
  }
111
67
  }
112
68
 
113
- // Apply 0008: collection_dividers table
114
- applyMigration(sqlite, "0008_add_collection_dividers.sql");
69
+ // If trigram fallback was used, triggers need to be created without trigram
70
+ // Re-create triggers unconditionally (IF NOT EXISTS handles idempotency)
71
+ sqlite.exec(`
72
+ CREATE TRIGGER IF NOT EXISTS post_ai AFTER INSERT ON post BEGIN
73
+ INSERT INTO post_fts(rowid, title, body_text, quote_text, url)
74
+ VALUES (new.rowid, new.title, new.body_text, new.quote_text, new.url);
75
+ END;
76
+ CREATE TRIGGER IF NOT EXISTS post_ad AFTER DELETE ON post BEGIN
77
+ INSERT INTO post_fts(post_fts, rowid, title, body_text, quote_text, url)
78
+ VALUES ('delete', old.rowid, old.title, old.body_text, old.quote_text, old.url);
79
+ END;
80
+ CREATE TRIGGER IF NOT EXISTS post_au AFTER UPDATE ON post BEGIN
81
+ INSERT INTO post_fts(post_fts, rowid, title, body_text, quote_text, url)
82
+ VALUES ('delete', old.rowid, old.title, old.body_text, old.quote_text, old.url);
83
+ INSERT INTO post_fts(rowid, title, body_text, quote_text, url)
84
+ VALUES (new.rowid, new.title, new.body_text, new.quote_text, new.url);
85
+ END;
86
+ `);
87
+ }
115
88
 
116
- // Apply 0009: drop show_divider column from collections
117
- applyMigration(sqlite, "0009_drop_collection_show_divider.sql");
89
+ /**
90
+ * Creates a fresh in-memory SQLite database with all migrations applied.
91
+ * Each call returns an isolated database instance for test isolation.
92
+ *
93
+ * @param options.fts - Whether to enable FTS5 for search tests (default: false).
94
+ * The trigram tokenizer used in production may not be available in all
95
+ * better-sqlite3 builds, so FTS is opt-in for tests that need it.
96
+ */
97
+ export function createTestDatabase(options?: { fts?: boolean }) {
98
+ const sqlite = new Database(":memory:");
118
99
 
119
- // Apply 0010: performance indexes
120
- applyMigration(sqlite, "0010_add_performance_indexes.sql");
100
+ // Enable WAL mode for better performance
101
+ sqlite.pragma("journal_mode = WAL");
102
+ sqlite.pragma("foreign_keys = ON");
121
103
 
122
- // Apply 0011: path registry
123
- applyMigration(sqlite, "0011_add_path_registry.sql");
104
+ // Apply all migrations in order (sorted by filename prefix: 0000_, 0001_, …)
105
+ // FTS migration (0001_*) is only applied when requested because the trigram
106
+ // tokenizer may not be available in all better-sqlite3 builds.
107
+ const allFiles = readdirSync(MIGRATIONS_DIR)
108
+ .filter((f) => f.endsWith(".sql"))
109
+ .sort();
110
+
111
+ for (const file of allFiles) {
112
+ const isFts = file.startsWith("0001_");
113
+ if (isFts && !options?.fts) continue;
114
+
115
+ if (isFts) {
116
+ applyFtsMigration(sqlite, file);
117
+ } else {
118
+ applyMigration(sqlite, file, { skipFts: !options?.fts });
119
+ }
120
+ }
124
121
 
125
122
  const db = drizzle(sqlite, { schema });
126
123
 
127
124
  // Polyfill D1 batch() for test compatibility.
128
125
  // In production, D1 batch executes statements atomically in a single transaction.
129
- // In tests, better-sqlite3 is synchronous and single-threaded so sequential
130
- // execution is effectively atomic.
126
+ // In tests, wrap sequential execution in an explicit transaction so rollback
127
+ // behavior matches D1's all-or-nothing semantics.
131
128
  Object.defineProperty(db, "batch", {
132
129
  value: async (queries: PromiseLike<unknown>[]) => {
133
- const results = [];
134
- for (const q of queries) {
135
- results.push(await q);
130
+ sqlite.exec("BEGIN");
131
+ try {
132
+ const results = [];
133
+ for (const q of queries) {
134
+ results.push(await q);
135
+ }
136
+ sqlite.exec("COMMIT");
137
+ return results;
138
+ } catch (err) {
139
+ sqlite.exec("ROLLBACK");
140
+ throw err;
136
141
  }
137
- return results;
138
142
  },
139
143
  });
140
144
 
package/src/app.tsx CHANGED
@@ -16,7 +16,6 @@ import { resetRoutes } from "./routes/auth/reset.js";
16
16
 
17
17
  // Routes - Pages
18
18
  import { homeRoutes } from "./routes/pages/home.js";
19
- import { postRoutes } from "./routes/pages/post.js";
20
19
  import { pageRoutes } from "./routes/pages/page.js";
21
20
  import { collectionRoutes } from "./routes/pages/collection.js";
22
21
  import { archiveRoutes } from "./routes/pages/archive.js";
@@ -24,25 +23,22 @@ import { searchRoutes } from "./routes/pages/search.js";
24
23
  import { featuredRoutes } from "./routes/pages/featured.js";
25
24
  import { latestRoutes } from "./routes/pages/latest.js";
26
25
  import { collectionsPageRoutes } from "./routes/pages/collections.js";
26
+ import { newPostRoutes } from "./routes/pages/new.js";
27
27
 
28
- // Routes - Dashboard
29
- import { dashIndexRoutes } from "./routes/dash/index.js";
30
- import { postsRoutes as dashPostsRoutes } from "./routes/dash/posts.js";
31
- import { pagesRoutes as dashPagesRoutes } from "./routes/dash/pages.js";
32
- import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
33
- import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
34
- import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
35
- import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
36
- import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
28
+ // Routes - Settings (admin)
29
+ import { settingsRoutes } from "./routes/dash/settings.js";
30
+ import { customUrlsRoutes } from "./routes/dash/custom-urls.js";
37
31
 
38
32
  // Routes - API
39
33
  import { postsApiRoutes } from "./routes/api/posts.js";
40
- import { pagesApiRoutes } from "./routes/api/pages.js";
41
34
  import { navItemsApiRoutes } from "./routes/api/nav-items.js";
42
35
  import { collectionsApiRoutes } from "./routes/api/collections.js";
43
36
  import { settingsApiRoutes } from "./routes/api/settings.js";
44
37
  import { uploadApiRoutes } from "./routes/api/upload.js";
38
+ import { multipartUploadApiRoutes } from "./routes/api/upload-multipart.js";
45
39
  import { searchApiRoutes } from "./routes/api/search.js";
40
+ import { customUrlsApiRoutes } from "./routes/api/custom-urls.js";
41
+ import { exportApiRoutes } from "./routes/api/export.js";
46
42
  // Routes - Compose
47
43
  import { composeRoutes } from "./routes/compose.js";
48
44
 
@@ -51,10 +47,11 @@ import { rssRoutes } from "./routes/feed/rss.js";
51
47
  import { sitemapRoutes } from "./routes/feed/sitemap.js";
52
48
 
53
49
  // Middleware
54
- import { requireAuth } from "./middleware/auth.js";
50
+ import { requireAuth, isLocalHostname } from "./middleware/auth.js";
55
51
  import { requireOnboarding } from "./middleware/onboarding.js";
56
52
  import { errorHandler } from "./middleware/error-handler.js";
57
53
  import { withConfig } from "./middleware/config.js";
54
+ import { secureHeadersMiddleware } from "./middleware/secure-headers.js";
58
55
 
59
56
  import { createStorageDriver } from "./lib/storage.js";
60
57
  import { base64ToUint8Array } from "./lib/favicon.js";
@@ -82,6 +79,31 @@ export function createApp(): App {
82
79
 
83
80
  // Lightweight init — no DB queries
84
81
  app.use("*", async (c, next) => {
82
+ // Fail fast: DEV_API_TOKEN must never be reachable from a non-local hostname
83
+ if (c.env.DEV_API_TOKEN) {
84
+ const hostname = new URL(c.req.url).hostname;
85
+ if (!isLocalHostname(hostname)) {
86
+ return c.html(
87
+ `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="utf-8">
91
+ <meta name="viewport" content="width=device-width, initial-scale=1">
92
+ <title>Configuration Error</title>
93
+ <style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111}div{max-width:480px;text-align:center}h1{font-size:1.25rem;font-weight:600}p{color:#666;line-height:1.6}code{background:#eee;padding:2px 6px;border-radius:4px;font-size:.9em}</style>
94
+ </head>
95
+ <body>
96
+ <div>
97
+ <h1>DEV_API_TOKEN must not be set in production</h1>
98
+ <p>Remove <code>DEV_API_TOKEN</code> from your environment variables. This token is only for local development.</p>
99
+ </div>
100
+ </body>
101
+ </html>`,
102
+ 500,
103
+ );
104
+ }
105
+ }
106
+
85
107
  if (!c.env.AUTH_SECRET) {
86
108
  return c.html(
87
109
  `<!DOCTYPE html>
@@ -109,7 +131,11 @@ export function createApp(): App {
109
131
  // Note: Drizzle ORM doesn't officially support D1DatabaseSession yet (issue #2226)
110
132
  // but it works at runtime. We use type assertion as a temporary workaround.
111
133
  const db = createDatabase(session as unknown as D1Database);
112
- c.set("services", createServices(db, session as unknown as D1Database));
134
+ const slugIdLength = parseInt(c.env.SLUG_ID_LENGTH ?? "5", 10) || 5;
135
+ c.set(
136
+ "services",
137
+ createServices(db, session as unknown as D1Database, { slugIdLength }),
138
+ );
113
139
  c.set("storage", createStorageDriver(c.env));
114
140
 
115
141
  const baseURL = c.env.SITE_URL || new URL(c.req.url).origin;
@@ -126,20 +152,118 @@ export function createApp(): App {
126
152
  await next();
127
153
  });
128
154
 
155
+ // Security headers (CSP, X-Frame-Options, etc.)
156
+ app.use("*", secureHeadersMiddleware());
157
+
129
158
  // --- Routes that don't need config/theme ---
130
159
 
131
160
  // Health check
132
161
  app.get("/health", (c) => c.json({ status: "ok" }));
133
162
 
163
+ // Fetch text media content by ID (same-origin proxy to avoid CORS with CDN URLs)
164
+ app.get("/api/media/:id/content", async (c) => {
165
+ const media = await c.var.services.media.getById(c.req.param("id"));
166
+ if (!media) return c.notFound();
167
+
168
+ const storage = c.var.storage;
169
+ if (!storage) return c.notFound();
170
+
171
+ const object = await storage.get(media.storageKey);
172
+ if (!object) return c.notFound();
173
+
174
+ const headers = new Headers();
175
+ headers.set(
176
+ "Content-Type",
177
+ object.contentType || "application/octet-stream",
178
+ );
179
+ // Use updatedAt as ETag so browsers can cache but revalidate on change
180
+ const etag = `"${media.updatedAt}"`;
181
+ headers.set("Cache-Control", "public, no-cache");
182
+ headers.set("ETag", etag);
183
+
184
+ if (c.req.header("If-None-Match") === etag) {
185
+ return new Response(null, { status: 304, headers });
186
+ }
187
+
188
+ return new Response(object.body, { headers });
189
+ });
190
+
134
191
  // Media files from storage (path matches storage key: media/YYYY/MM/uuid.ext)
192
+ // Supports HTTP Range requests for seekable audio/video playback.
135
193
  app.get("/media/*", async (c) => {
136
194
  const storage = c.var.storage;
137
195
  if (!storage) {
138
196
  return c.notFound();
139
197
  }
140
198
 
141
- // The storage key is the full path without the leading "/"
142
199
  const storageKey = c.req.path.slice(1);
200
+ if (storageKey.includes("..") || !storageKey.startsWith("media/")) {
201
+ return c.notFound();
202
+ }
203
+
204
+ const rangeHeader = c.req.header("Range");
205
+
206
+ // First fetch without range to get the total size
207
+ if (rangeHeader) {
208
+ // Get total size via a full request first
209
+ const full = await storage.get(storageKey);
210
+ if (!full) return c.notFound();
211
+
212
+ const totalSize = full.size;
213
+ if (!totalSize) {
214
+ // Driver doesn't report size — fall back to full response
215
+ const headers = new Headers();
216
+ headers.set(
217
+ "Content-Type",
218
+ full.contentType || "application/octet-stream",
219
+ );
220
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
221
+ return new Response(full.body, { headers });
222
+ }
223
+
224
+ // Cancel the full stream — we'll re-fetch with the range
225
+ await full.body.cancel();
226
+
227
+ // Parse "bytes=START-END" (END is optional)
228
+ const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
229
+ if (!match) {
230
+ return new Response("Invalid Range", {
231
+ status: 416,
232
+ headers: { "Content-Range": `bytes */${totalSize}` },
233
+ });
234
+ }
235
+
236
+ const start = parseInt(match[1] ?? "0", 10);
237
+ const end = match[2]
238
+ ? Math.min(parseInt(match[2], 10), totalSize - 1)
239
+ : totalSize - 1;
240
+
241
+ if (start > end || start >= totalSize) {
242
+ return new Response("Range Not Satisfiable", {
243
+ status: 416,
244
+ headers: { "Content-Range": `bytes */${totalSize}` },
245
+ });
246
+ }
247
+
248
+ const rangeObj = await storage.get(storageKey, {
249
+ range: { offset: start, length: end - start + 1 },
250
+ });
251
+ if (!rangeObj) return c.notFound();
252
+
253
+ const headers = new Headers();
254
+ headers.set(
255
+ "Content-Type",
256
+ rangeObj.contentType || "application/octet-stream",
257
+ );
258
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
259
+ headers.set("Accept-Ranges", "bytes");
260
+ headers.set("Content-Range", `bytes ${start}-${end}/${totalSize}`);
261
+ headers.set("Content-Length", String(end - start + 1));
262
+
263
+ return new Response(rangeObj.body, { status: 206, headers });
264
+ }
265
+
266
+ // No Range header — serve full file
143
267
  const object = await storage.get(storageKey);
144
268
  if (!object) {
145
269
  return c.notFound();
@@ -151,6 +275,10 @@ export function createApp(): App {
151
275
  object.contentType || "application/octet-stream",
152
276
  );
153
277
  headers.set("Cache-Control", "public, max-age=31536000, immutable");
278
+ headers.set("Accept-Ranges", "bytes");
279
+ if (object.size) {
280
+ headers.set("Content-Length", String(object.size));
281
+ }
154
282
 
155
283
  return new Response(object.body, { headers });
156
284
  });
@@ -206,7 +334,7 @@ export function createApp(): App {
206
334
  await next();
207
335
  });
208
336
 
209
- // Redirect middleware
337
+ // Redirect middleware — only handles redirect-type custom URLs
210
338
  app.use("*", async (c, next) => {
211
339
  const path = new URL(c.req.url).pathname;
212
340
  // Skip redirect check for API routes and static assets
@@ -214,9 +342,9 @@ export function createApp(): App {
214
342
  return next();
215
343
  }
216
344
 
217
- const redirect = await c.var.services.redirects.getByPath(path);
218
- if (redirect) {
219
- return c.redirect(redirect.toPath, redirect.type);
345
+ const customUrl = await c.var.services.customUrls.getByPath(path.slice(1));
346
+ if (customUrl?.targetType === "redirect" && customUrl.toPath) {
347
+ return c.redirect(customUrl.toPath, customUrl.redirectType ?? 301);
220
348
  }
221
349
 
222
350
  await next();
@@ -230,27 +358,25 @@ export function createApp(): App {
230
358
 
231
359
  // API Routes
232
360
  app.route("/api/posts", postsApiRoutes);
233
- app.route("/api/pages", pagesApiRoutes);
234
361
  app.route("/api/nav-items", navItemsApiRoutes);
235
362
  app.route("/api/collections", collectionsApiRoutes);
236
363
  app.route("/api/settings", settingsApiRoutes);
364
+ app.route("/api/custom-urls", customUrlsApiRoutes);
365
+ app.route("/api/export", exportApiRoutes);
237
366
 
238
367
  // Auth routes
239
368
  app.route("/", setupRoutes);
240
369
  app.route("/", signinRoutes);
241
370
  app.route("/", resetRoutes);
242
371
 
243
- // Dashboard routes (protected)
244
- app.use("/dash/*", requireAuth());
245
- app.route("/dash", dashIndexRoutes);
246
- app.route("/dash/posts", dashPostsRoutes);
247
- app.route("/dash/pages", dashPagesRoutes);
248
- app.route("/dash/media", dashMediaRoutes);
249
- app.route("/dash/settings", dashSettingsRoutes);
250
- app.route("/dash/appearance", dashAppearanceRoutes);
251
- app.route("/dash/settings/redirects", dashRedirectsRoutes);
252
- app.route("/dash/collections", dashCollectionsRoutes);
253
- // API routes
372
+ // Settings routes (protected)
373
+ app.use("/settings/*", requireAuth());
374
+ app.use("/settings", requireAuth());
375
+ app.route("/settings/custom-urls", customUrlsRoutes);
376
+ app.route("/settings", settingsRoutes);
377
+
378
+ // Protected API routes (multipart must be registered before base upload)
379
+ app.route("/api/upload/multipart", multipartUploadApiRoutes);
254
380
  app.route("/api/upload", uploadApiRoutes);
255
381
  app.route("/api/search", searchApiRoutes);
256
382
 
@@ -263,12 +389,12 @@ export function createApp(): App {
263
389
 
264
390
  // Frontend routes
265
391
  app.route("/search", searchRoutes);
392
+ app.route("/", newPostRoutes);
266
393
  app.route("/archive", archiveRoutes);
267
394
  app.route("/featured", featuredRoutes);
268
395
  app.route("/latest", latestRoutes);
269
396
  app.route("/c", collectionsPageRoutes);
270
397
  app.route("/c", collectionRoutes);
271
- app.route("/p", postRoutes);
272
398
  app.route("/", homeRoutes);
273
399
 
274
400
  // Custom page catch-all (must be last)