@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
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Search Service (v2)
3
3
  *
4
- * Full-text search using FTS5
4
+ * Full-text search using FTS5 trigram for queries ≥ 3 characters.
5
+ * Falls back to LIKE for shorter queries (common in CJK languages where
6
+ * 2-character words cannot form a trigram).
5
7
  */
6
8
 
7
9
  import type { Post, Status, Format, SearchResult } from "../types.js";
10
+ import { escapeHtml } from "../lib/html.js";
8
11
 
9
12
  export type { SearchResult };
10
13
 
@@ -24,98 +27,182 @@ export interface SearchService {
24
27
  }
25
28
 
26
29
  interface RawSearchRow {
27
- id: number;
30
+ id: string;
28
31
  format: string;
29
32
  status: string;
30
- featured: number;
31
- pinned: number;
32
- path: string | null;
33
+ visibility: string | null;
34
+ effective_visibility: string | null;
35
+ pinned_at: number | null;
36
+ featured_at: number | null;
37
+ slug: string;
33
38
  title: string | null;
34
39
  url: string | null;
35
40
  body: string | null;
36
41
  body_html: string | null;
42
+ body_text: string | null;
37
43
  quote_text: string | null;
44
+ summary: string | null;
38
45
  rating: number | null;
39
- collection_id: number | null;
40
- reply_to_id: number | null;
41
- thread_id: number | null;
46
+ collection_id: string | null;
47
+ reply_to_id: string | null;
48
+ thread_id: string;
42
49
  deleted_at: number | null;
43
- published_at: number;
50
+ published_at: number | null;
51
+ last_activity_at: number | null;
44
52
  created_at: number;
45
53
  updated_at: number;
46
54
  rank: number;
47
- snippet: string;
55
+ snippet: string | null;
56
+ }
57
+
58
+ function mapRow(row: RawSearchRow): SearchResult {
59
+ return {
60
+ post: {
61
+ id: row.id,
62
+ format: row.format as Post["format"],
63
+ status: row.status as Post["status"],
64
+ visibility: (row.effective_visibility ??
65
+ row.visibility) as Post["visibility"],
66
+ pinnedAt: row.pinned_at,
67
+ featuredAt: row.featured_at,
68
+ slug: row.slug,
69
+ title: row.title,
70
+ url: row.url,
71
+ body: row.body,
72
+ bodyHtml: row.body_html,
73
+ bodyText: row.body_text,
74
+ quoteText: row.quote_text,
75
+ summary: row.summary,
76
+ rating: row.rating,
77
+ replyToId: row.reply_to_id,
78
+ threadId: row.thread_id,
79
+ deletedAt: row.deleted_at,
80
+ publishedAt: row.published_at,
81
+ lastActivityAt:
82
+ row.last_activity_at ?? row.published_at ?? row.updated_at,
83
+ createdAt: row.created_at,
84
+ updatedAt: row.updated_at,
85
+ },
86
+ rank: row.rank,
87
+ snippet: row.snippet
88
+ ? escapeHtml(row.snippet)
89
+ .replaceAll(String.fromCharCode(2), "<mark>")
90
+ .replaceAll(String.fromCharCode(3), "</mark>")
91
+ : undefined,
92
+ };
48
93
  }
49
94
 
50
95
  export function createSearchService(d1: D1Database): SearchService {
96
+ async function searchFts(
97
+ query: string,
98
+ options: SearchOptions,
99
+ ): Promise<SearchResult[]> {
100
+ const limit = options.limit ?? 20;
101
+ const offset = options.offset ?? 0;
102
+ const status = options.status ?? ["published"];
103
+
104
+ const ftsQuery = query
105
+ .trim()
106
+ .split(/\s+/)
107
+ .filter((term) => term.length > 0)
108
+ .map((term) => `"${term.replace(/"/g, '""')}"*`)
109
+ .join(" ");
110
+
111
+ if (!ftsQuery) return [];
112
+
113
+ const statusPlaceholders = status.map(() => "?").join(", ");
114
+ const formatFilter = options.format ? "AND post.format = ?" : "";
115
+ const formatParams = options.format ? [options.format] : [];
116
+
117
+ const stmt = d1.prepare(`
118
+ SELECT
119
+ post.*,
120
+ COALESCE(post.visibility, root_post.visibility) AS effective_visibility,
121
+ path_registry.path AS slug,
122
+ post_fts.rank AS rank,
123
+ snippet(post_fts, 1, char(2), char(3), '...', 32) AS snippet
124
+ FROM post_fts
125
+ JOIN post ON post.rowid = post_fts.rowid
126
+ JOIN post AS root_post ON root_post.id = post.thread_id
127
+ JOIN path_registry
128
+ ON path_registry.post_id = post.id
129
+ AND path_registry.kind = 'slug'
130
+ WHERE post_fts MATCH ?
131
+ AND post.deleted_at IS NULL
132
+ AND post.status IN (${statusPlaceholders})
133
+ ${formatFilter}
134
+ ORDER BY post_fts.rank
135
+ LIMIT ? OFFSET ?
136
+ `);
137
+
138
+ const { results } = await stmt
139
+ .bind(ftsQuery, ...status, ...formatParams, limit, offset)
140
+ .all<RawSearchRow>();
141
+
142
+ return (results || []).map(mapRow);
143
+ }
144
+
145
+ async function searchLike(
146
+ query: string,
147
+ options: SearchOptions,
148
+ ): Promise<SearchResult[]> {
149
+ const limit = options.limit ?? 20;
150
+ const offset = options.offset ?? 0;
151
+ const status = options.status ?? ["published"];
152
+ const like = `%${query}%`;
153
+
154
+ const statusPlaceholders = status.map(() => "?").join(", ");
155
+ const formatFilter = options.format ? "AND post.format = ?" : "";
156
+ const formatParams = options.format ? [options.format] : [];
157
+
158
+ const stmt = d1.prepare(`
159
+ SELECT
160
+ post.*,
161
+ COALESCE(post.visibility, root_post.visibility) AS effective_visibility,
162
+ path_registry.path AS slug,
163
+ 0 AS rank,
164
+ NULL AS snippet
165
+ FROM post
166
+ JOIN post AS root_post ON root_post.id = post.thread_id
167
+ JOIN path_registry
168
+ ON path_registry.post_id = post.id
169
+ AND path_registry.kind = 'slug'
170
+ WHERE (
171
+ post.title LIKE ? OR
172
+ post.body_text LIKE ? OR
173
+ post.quote_text LIKE ? OR
174
+ post.url LIKE ?
175
+ )
176
+ AND post.deleted_at IS NULL
177
+ AND post.status IN (${statusPlaceholders})
178
+ ${formatFilter}
179
+ ORDER BY post.published_at DESC
180
+ LIMIT ? OFFSET ?
181
+ `);
182
+
183
+ const { results } = await stmt
184
+ .bind(like, like, like, like, ...status, ...formatParams, limit, offset)
185
+ .all<RawSearchRow>();
186
+
187
+ return (results || []).map(mapRow);
188
+ }
189
+
51
190
  return {
52
191
  async search(query, options = {}) {
53
- const limit = options.limit ?? 20;
54
- const offset = options.offset ?? 0;
55
- const status = options.status ?? ["published"];
56
-
57
- // Escape and prepare the query for FTS5
58
- const ftsQuery = query
59
- .trim()
60
- .split(/\s+/)
61
- .filter((term) => term.length > 0)
62
- .map((term) => `"${term.replace(/"/g, '""')}"*`)
63
- .join(" ");
64
-
65
- if (!ftsQuery) {
66
- return [];
192
+ const trimmed = query.trim();
193
+ if (!trimmed) return [];
194
+
195
+ // Trigram FTS requires at least 3 characters.
196
+ // For shorter queries (common in CJK), fall back to LIKE.
197
+ const charCount = [...trimmed].length;
198
+ if (charCount < 3) {
199
+ return searchLike(trimmed, options);
67
200
  }
68
201
 
69
- // Build status placeholders
70
- const statusPlaceholders = status.map(() => "?").join(", ");
71
-
72
- // Build format filter
73
- const formatFilter = options.format ? "AND posts.format = ?" : "";
74
- const formatParams = options.format ? [options.format] : [];
75
-
76
- const stmt = d1.prepare(`
77
- SELECT
78
- posts.*,
79
- posts_fts.rank AS rank,
80
- snippet(posts_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
81
- FROM posts_fts
82
- JOIN posts ON posts.id = posts_fts.rowid
83
- WHERE posts_fts MATCH ?
84
- AND posts.deleted_at IS NULL
85
- AND posts.status IN (${statusPlaceholders})
86
- ${formatFilter}
87
- ORDER BY posts_fts.rank
88
- LIMIT ? OFFSET ?
89
- `);
90
-
91
- const { results } = await stmt
92
- .bind(ftsQuery, ...status, ...formatParams, limit, offset)
93
- .all<RawSearchRow>();
94
-
95
- return (results || []).map((row) => ({
96
- post: {
97
- id: row.id,
98
- format: row.format as Post["format"],
99
- status: row.status as Post["status"],
100
- featured: row.featured,
101
- pinned: row.pinned,
102
- path: row.path,
103
- title: row.title,
104
- url: row.url,
105
- body: row.body,
106
- bodyHtml: row.body_html,
107
- quoteText: row.quote_text,
108
- rating: row.rating,
109
- replyToId: row.reply_to_id,
110
- threadId: row.thread_id,
111
- deletedAt: row.deleted_at,
112
- publishedAt: row.published_at,
113
- createdAt: row.created_at,
114
- updatedAt: row.updated_at,
115
- },
116
- rank: row.rank,
117
- snippet: row.snippet,
118
- }));
202
+ const ftsResults = await searchFts(trimmed, options);
203
+ if (ftsResults.length > 0) return ftsResults;
204
+
205
+ return searchLike(trimmed, options);
119
206
  },
120
207
  };
121
208
  }
@@ -44,6 +44,7 @@ export interface AvatarUploadDeps {
44
44
  media: MediaService;
45
45
  storage: StorageDriver;
46
46
  storageProvider: string;
47
+ maxFileSizeMB: number;
47
48
  }
48
49
 
49
50
  export interface SettingsService {
@@ -194,7 +195,7 @@ export function createSettingsService(db: Database): SettingsService {
194
195
  // Header nav max visible: only update if provided (may be managed separately)
195
196
  if (data.headerNavMaxVisible !== undefined) {
196
197
  const navMax = parseInt(String(data.headerNavMaxVisible), 10);
197
- if (!isNaN(navMax) && navMax !== 3) {
198
+ if (!isNaN(navMax) && navMax !== 2) {
198
199
  await this.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
199
200
  } else {
200
201
  await this.remove("HEADER_NAV_MAX_VISIBLE");
@@ -215,7 +216,10 @@ export function createSettingsService(db: Database): SettingsService {
215
216
  },
216
217
 
217
218
  async uploadAvatar(data, deps) {
218
- const uploadError = validateUploadFile(data.file as unknown as File);
219
+ const uploadError = validateUploadFile(data.file as unknown as File, {
220
+ imagesOnly: true,
221
+ maxFileSizeMB: deps.maxFileSizeMB,
222
+ });
219
223
  if (uploadError) {
220
224
  throw new ValidationError(uploadError);
221
225
  }