@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Language Service
3
+ *
4
+ * Owns the multilingual content lifecycle: which languages a site serves, which
5
+ * one the unprefixed root URLs belong to, and the ordering rules that keep
6
+ * those two settings consistent with the `post.language` column.
7
+ *
8
+ * These operations are multi-step by nature — enabling has to check URL-prefix
9
+ * conflicts, write two settings, and stamp every unmarked post, in that order —
10
+ * so they live here rather than in routes, and this service is the only writer
11
+ * of `MULTILINGUAL_ENABLED` and `ADDITIONAL_LANGUAGES`.
12
+ */
13
+
14
+ import { and, eq, inArray } from "drizzle-orm";
15
+ import type { Database } from "../db/index.js";
16
+ import {
17
+ sqliteSchemaBundle,
18
+ type DatabaseSchema,
19
+ } from "../db/schema-bundle.js";
20
+ import {
21
+ formatLanguageList,
22
+ isValidContentLanguage,
23
+ normalizeContentLanguage,
24
+ parseLanguageList,
25
+ toLanguagePrefix,
26
+ } from "../i18n/locales.js";
27
+ import { RESERVED_PATHS } from "../lib/constants.js";
28
+ import {
29
+ ConflictError,
30
+ LanguageInUseError,
31
+ ValidationError,
32
+ } from "../lib/errors.js";
33
+ import type { PathService } from "./path.js";
34
+ import type { PostService } from "./post.js";
35
+ import type { SettingsService } from "./settings.js";
36
+
37
+ /** The site's language configuration, as the rest of the app reads it. */
38
+ export interface LanguageState {
39
+ /** Whether per-language browsing views are served. */
40
+ enabled: boolean;
41
+ /** Canonical tag served from the unprefixed root URLs. */
42
+ primary: string;
43
+ /** Canonical tags served under a URL prefix, in switcher order. */
44
+ additional: string[];
45
+ /** Every configured language, primary first. */
46
+ all: string[];
47
+ }
48
+
49
+ /** What the enable dialog needs to describe what it is about to do. */
50
+ export interface LanguageEnablePreview {
51
+ /** Posts that would be stamped with the primary language. */
52
+ pendingCount: number;
53
+ /** Current primary language, prefilled into the dialog. */
54
+ primary: string;
55
+ }
56
+
57
+ export interface EnableMultilingualInput {
58
+ /** Primary language; may differ from the stored one when the author edits it in the dialog. */
59
+ primary: string;
60
+ /** At least one language to serve under a prefix. */
61
+ additional: string[];
62
+ }
63
+
64
+ export interface LanguageService {
65
+ /** Read the site's language configuration. */
66
+ getState(): Promise<LanguageState>;
67
+ /** Numbers and defaults for the enable confirmation dialog. */
68
+ getEnablePreview(): Promise<LanguageEnablePreview>;
69
+ /**
70
+ * Turn on multilingual content.
71
+ *
72
+ * Validates the language set, rejects prefixes that would shadow an existing
73
+ * URL, writes both settings, then stamps every post that has no language yet
74
+ * with the primary language — so the root view keeps showing exactly what it
75
+ * showed a moment earlier.
76
+ *
77
+ * @returns How many posts were stamped
78
+ * @throws {ValidationError} When the language set is empty or malformed
79
+ * @throws {ConflictError} When a language prefix collides with a stored path
80
+ */
81
+ enable(input: EnableMultilingualInput): Promise<{ markedCount: number }>;
82
+ /**
83
+ * Turn off multilingual content, keeping the configuration and every stored
84
+ * `post.language` value so re-enabling restores the same setup and the old
85
+ * prefixes can still redirect.
86
+ */
87
+ disable(): Promise<void>;
88
+ /**
89
+ * Move the root URLs to another configured language.
90
+ *
91
+ * Swaps the two lists in one step: the new primary leaves the additional
92
+ * list and the old primary joins it. Doing only half would leave the old
93
+ * primary's posts with no view at all — filtered out of the root, and with no
94
+ * prefix of their own.
95
+ */
96
+ setPrimary(language: string): Promise<void>;
97
+ /** Add a language to serve under a URL prefix. */
98
+ addLanguage(language: string): Promise<void>;
99
+ /**
100
+ * Stop serving a language.
101
+ *
102
+ * Only allowed while no post is written in it. With posts present there is no
103
+ * good answer — hiding, migrating, or redirecting them all guess at intent —
104
+ * so this reports the count and leaves the choice to the author.
105
+ *
106
+ * Removing the last additional language turns multilingual content off with
107
+ * it: per-language views need a second language to mean anything, and a site
108
+ * left with the flag on and nothing to serve under it would report the
109
+ * feature as on everywhere the setting is read while behaving as
110
+ * single-language everywhere it is used.
111
+ *
112
+ * @returns Whether multilingual content was turned off along with it
113
+ * @throws {ConflictError} When posts still use the language
114
+ */
115
+ removeLanguage(language: string): Promise<{ multilingualDisabled: boolean }>;
116
+ }
117
+
118
+ export interface LanguageServiceDeps {
119
+ settings: SettingsService;
120
+ posts: PostService;
121
+ paths: PathService;
122
+ }
123
+
124
+ /** Language tags whose URL prefix can never work, whatever the site contains. */
125
+ const RESERVED_PREFIXES = new Set<string>(RESERVED_PATHS);
126
+
127
+ /**
128
+ * Read the site's language configuration directly from its settings rows.
129
+ *
130
+ * A standalone reader rather than a method on the service because slug and
131
+ * custom-URL validation need it from inside other services, where threading a
132
+ * whole `LanguageService` through would be more wiring than the two values are
133
+ * worth. The service remains the only *writer*.
134
+ *
135
+ * @param db - Site database handle
136
+ * @param siteId - Site the settings belong to
137
+ * @param databaseSchema - Dialect-specific schema bundle
138
+ * @returns The primary language tag; every language the site publishes, primary
139
+ * first (empty while multilingual content is off); and the URL prefixes
140
+ * language views currently serve
141
+ * @example
142
+ * await readLanguageSettings(db, siteId);
143
+ * // => { primary: "zh-Hans", languages: ["zh-Hans", "en"], reservedPrefixes: ["en"] }
144
+ */
145
+ export async function readLanguageSettings(
146
+ db: Database,
147
+ siteId: string,
148
+ databaseSchema: DatabaseSchema = sqliteSchemaBundle,
149
+ ): Promise<{
150
+ primary: string;
151
+ languages: string[];
152
+ reservedPrefixes: string[];
153
+ }> {
154
+ const { settings } = databaseSchema;
155
+ const rows = await db
156
+ .select({ key: settings.key, value: settings.value })
157
+ .from(settings)
158
+ .where(
159
+ and(
160
+ eq(settings.siteId, siteId),
161
+ inArray(settings.key, [
162
+ "SITE_LANGUAGE",
163
+ "MULTILINGUAL_ENABLED",
164
+ "ADDITIONAL_LANGUAGES",
165
+ ]),
166
+ ),
167
+ );
168
+ const values = new Map(rows.map((row) => [row.key, row.value]));
169
+ const primary = normalizeContentLanguage(values.get("SITE_LANGUAGE") ?? "");
170
+ const additional =
171
+ values.get("MULTILINGUAL_ENABLED") === "true"
172
+ ? parseLanguageList(values.get("ADDITIONAL_LANGUAGES")).filter(
173
+ (tag) => tag !== primary,
174
+ )
175
+ : [];
176
+ // Empty — not `[primary]` — when the site publishes one language, so callers
177
+ // can treat "no languages" as "this site is not multilingual".
178
+ const languages = additional.length > 0 ? [primary, ...additional] : [];
179
+ return {
180
+ primary,
181
+ languages,
182
+ reservedPrefixes: additional.map(toLanguagePrefix),
183
+ };
184
+ }
185
+
186
+ function requireValidLanguage(value: string): string {
187
+ const trimmed = value.trim();
188
+ if (!trimmed || !isValidContentLanguage(trimmed)) {
189
+ throw new ValidationError(
190
+ "Enter a valid BCP 47 language tag (e.g. en, zh-Hans, ja).",
191
+ );
192
+ }
193
+ return normalizeContentLanguage(trimmed);
194
+ }
195
+
196
+ export function createLanguageService(
197
+ deps: LanguageServiceDeps,
198
+ ): LanguageService {
199
+ const { settings, posts, paths } = deps;
200
+
201
+ async function readState(): Promise<LanguageState> {
202
+ const [enabledValue, primaryValue, additionalValue] = await Promise.all([
203
+ settings.get("MULTILINGUAL_ENABLED"),
204
+ settings.get("SITE_LANGUAGE"),
205
+ settings.get("ADDITIONAL_LANGUAGES"),
206
+ ]);
207
+
208
+ const primary = primaryValue?.trim()
209
+ ? normalizeContentLanguage(primaryValue)
210
+ : "en";
211
+ // Defensive: a hand-edited settings row could list the primary language
212
+ // twice. The prefix for it would never route, so drop it here rather than
213
+ // letting a dead entry reach the switcher.
214
+ const additional = parseLanguageList(additionalValue).filter(
215
+ (tag) => tag !== primary,
216
+ );
217
+
218
+ return {
219
+ enabled: enabledValue === "true" && additional.length > 0,
220
+ primary,
221
+ additional,
222
+ all: [primary, ...additional],
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Reject a language whose URL prefix would shadow something already served.
228
+ *
229
+ * Language prefixes are checked here, when a language is added, rather than
230
+ * being reserved globally: the two-letter ISO codes are ordinary English
231
+ * words (`it`, `is`, `no`, `go`, `my`), and permanently confiscating those
232
+ * slugs from every site for an off-by-default feature is out of proportion.
233
+ */
234
+ async function assertPrefixAvailable(language: string): Promise<void> {
235
+ const prefix = toLanguagePrefix(language);
236
+
237
+ if (RESERVED_PREFIXES.has(prefix)) {
238
+ throw new ConflictError(
239
+ `Jant already uses /${prefix} for its own pages, so it cannot serve a language there.`,
240
+ );
241
+ }
242
+
243
+ const clashes = await paths.findPathsUnderSegment(prefix, 3);
244
+ const first = clashes[0];
245
+ if (first) {
246
+ throw new ConflictError(
247
+ `/${first.path} is already taken. Change that URL, then add this language.`,
248
+ );
249
+ }
250
+ }
251
+
252
+ function normalizeAdditional(
253
+ tags: readonly string[],
254
+ primary: string,
255
+ ): string[] {
256
+ const seen = new Set<string>();
257
+ const result: string[] = [];
258
+ for (const raw of tags) {
259
+ const tag = requireValidLanguage(raw);
260
+ if (tag === primary || seen.has(tag)) continue;
261
+ seen.add(tag);
262
+ result.push(tag);
263
+ }
264
+ return result;
265
+ }
266
+
267
+ async function writeLanguages(
268
+ primary: string,
269
+ additional: readonly string[],
270
+ ): Promise<void> {
271
+ await settings.set("SITE_LANGUAGE", primary);
272
+ if (additional.length > 0) {
273
+ await settings.set(
274
+ "ADDITIONAL_LANGUAGES",
275
+ formatLanguageList(additional),
276
+ );
277
+ } else {
278
+ await settings.remove("ADDITIONAL_LANGUAGES");
279
+ }
280
+ }
281
+
282
+ return {
283
+ getState: readState,
284
+
285
+ async getEnablePreview() {
286
+ const [state, pendingCount] = await Promise.all([
287
+ readState(),
288
+ posts.countMissingLanguage(),
289
+ ]);
290
+ return { pendingCount, primary: state.primary };
291
+ },
292
+
293
+ async enable(input) {
294
+ const primary = requireValidLanguage(input.primary);
295
+ const additional = normalizeAdditional(input.additional, primary);
296
+ if (additional.length === 0) {
297
+ throw new ValidationError(
298
+ "Add at least one more language before turning this on.",
299
+ );
300
+ }
301
+
302
+ // The list must keep a place for every language posts are already
303
+ // written in. Turning multilingual off keeps both the languages and
304
+ // the stamps, so a re-enable that dropped one from the dialog would
305
+ // otherwise strand its posts outside every view — the exact orphaning
306
+ // the remove guard exists to prevent, through a side door.
307
+ const allowed = new Set([primary, ...additional]);
308
+ const stranded = (await posts.listLanguagesInUse()).find(
309
+ (entry) => !allowed.has(entry.language),
310
+ );
311
+ if (stranded) {
312
+ throw new LanguageInUseError(
313
+ stranded.count === 1
314
+ ? "One post is still written in a language missing from this list. Keep the language, or change the post's language first."
315
+ : `${stranded.count} posts are still written in a language missing from this list. Keep the language, or change their language first.`,
316
+ stranded.language,
317
+ stranded.count,
318
+ );
319
+ }
320
+
321
+ // Every prefix is checked before anything is written, so a conflict
322
+ // leaves the site exactly as it was.
323
+ for (const language of additional) {
324
+ await assertPrefixAvailable(language);
325
+ }
326
+
327
+ await writeLanguages(primary, additional);
328
+ await settings.set("MULTILINGUAL_ENABLED", "true");
329
+
330
+ const markedCount = await posts.materializeMissingLanguage(primary);
331
+ return { markedCount };
332
+ },
333
+
334
+ async disable() {
335
+ await settings.remove("MULTILINGUAL_ENABLED");
336
+ },
337
+
338
+ async setPrimary(language) {
339
+ const next = requireValidLanguage(language);
340
+ const state = await readState();
341
+ if (next === state.primary) return;
342
+
343
+ if (!state.additional.includes(next)) {
344
+ throw new ValidationError(
345
+ "Add that language to the site before making it the primary language.",
346
+ );
347
+ }
348
+
349
+ // The old primary needs a prefix from now on, and the new one no longer
350
+ // does. Both halves in one write: doing either alone would leave a
351
+ // language's posts with nowhere to appear.
352
+ const additional = [
353
+ ...state.additional.filter((tag) => tag !== next),
354
+ state.primary,
355
+ ];
356
+ await writeLanguages(next, additional);
357
+ },
358
+
359
+ async addLanguage(language) {
360
+ const tag = requireValidLanguage(language);
361
+ const state = await readState();
362
+ if (tag === state.primary) {
363
+ throw new ConflictError(
364
+ "That is already this site's primary language.",
365
+ );
366
+ }
367
+ if (state.additional.includes(tag)) return;
368
+
369
+ await assertPrefixAvailable(tag);
370
+ await writeLanguages(state.primary, [...state.additional, tag]);
371
+ },
372
+
373
+ async removeLanguage(language) {
374
+ const tag = requireValidLanguage(language);
375
+ const state = await readState();
376
+ if (tag === state.primary) {
377
+ throw new ConflictError(
378
+ "This is the primary language. Make another language primary first.",
379
+ );
380
+ }
381
+ if (!state.additional.includes(tag)) {
382
+ return { multilingualDisabled: false };
383
+ }
384
+
385
+ // Counted whether or not multilingual is currently on: the settings page
386
+ // stays reachable after it is switched off, and the posts are still there.
387
+ const postCount = await posts.countByLanguage(tag);
388
+ if (postCount > 0) {
389
+ throw new LanguageInUseError(
390
+ postCount === 1
391
+ ? "One post is still written in this language. Change its language, or keep the language."
392
+ : `${postCount} posts are still written in this language. Change their language, or keep the language.`,
393
+ tag,
394
+ postCount,
395
+ );
396
+ }
397
+
398
+ const remaining = state.additional.filter((entry) => entry !== tag);
399
+ await writeLanguages(state.primary, remaining);
400
+
401
+ // Nothing left to serve under a prefix, so the feature goes with it. The
402
+ // flag is only cleared when it was actually on — a removal made while
403
+ // multilingual is off has nothing to turn off, and saying so would be a
404
+ // lie the settings page then has to explain.
405
+ const multilingualDisabled = remaining.length === 0 && state.enabled;
406
+ if (multilingualDisabled) {
407
+ await settings.remove("MULTILINGUAL_ENABLED");
408
+ }
409
+ return { multilingualDisabled };
410
+ },
411
+ };
412
+ }
@@ -102,6 +102,7 @@ const SearchPostsToolSchema = z.object({
102
102
 
103
103
  const ListMediaToolSchema = z.object({
104
104
  limit: z.coerce.number().int().min(1).max(200).optional().default(50),
105
+ cursor: z.string().optional(),
105
106
  mimePrefix: z.string().trim().min(1).optional(),
106
107
  });
107
108
 
@@ -244,6 +245,16 @@ const mcpTools: McpToolDefinition[] = [
244
245
  replyToId: { type: "string" },
245
246
  quietReply: { type: "boolean" },
246
247
  publishedAt: { type: "integer" },
248
+ createdAt: {
249
+ type: "integer",
250
+ description:
251
+ "Unix seconds. When moving a post from another site, the time it was written.",
252
+ },
253
+ updatedAt: {
254
+ type: "integer",
255
+ description:
256
+ "Unix seconds. When moving a post from another site, the time it was last edited.",
257
+ },
247
258
  attachments: {
248
259
  type: "array",
249
260
  items: { type: "object" },
@@ -276,6 +287,8 @@ const mcpTools: McpToolDefinition[] = [
276
287
  replyToId: input.replyToId,
277
288
  quietReply: input.quietReply,
278
289
  publishedAt: input.publishedAt,
290
+ createdAt: input.createdAt,
291
+ updatedAt: input.updatedAt,
279
292
  },
280
293
  input.attachments,
281
294
  {
@@ -433,7 +446,9 @@ const mcpTools: McpToolDefinition[] = [
433
446
  };
434
447
  }
435
448
 
436
- return context.services.collections.listDirectoryData();
449
+ return context.services.collections.listDirectoryData({
450
+ isAuthenticated: true,
451
+ });
437
452
  },
438
453
  },
439
454
  {
@@ -540,12 +555,14 @@ const mcpTools: McpToolDefinition[] = [
540
555
  },
541
556
  {
542
557
  name: "jant_media_list",
543
- description: "List uploaded media, optionally filtered by MIME prefix.",
558
+ description:
559
+ "List uploaded media, newest first, optionally filtered by MIME prefix. Pass nextCursor back as cursor for the next page.",
544
560
  inputSchema: {
545
561
  type: "object",
546
562
  properties: {
547
563
  limit: { type: "integer", minimum: 1, maximum: 200, default: 50 },
548
564
  mimePrefix: { type: "string" },
565
+ cursor: { type: "string" },
549
566
  },
550
567
  additionalProperties: false,
551
568
  },
@@ -554,10 +571,13 @@ const mcpTools: McpToolDefinition[] = [
554
571
  const media = await context.services.media.list({
555
572
  limit: input.limit,
556
573
  mimePrefix: input.mimePrefix,
574
+ cursor: input.cursor,
557
575
  });
558
576
 
559
577
  return {
560
578
  media: media.map((item) => serializeMedia(item, context.appConfig)),
579
+ nextCursor:
580
+ media.length === input.limit ? (media.at(-1)?.id ?? null) : null,
561
581
  };
562
582
  },
563
583
  },
@@ -17,7 +17,11 @@ import {
17
17
  lte,
18
18
  } from "drizzle-orm";
19
19
  import { generateKeyBetween } from "fractional-indexing";
20
- import { type Database, supportsDrizzleTransaction } from "../db/index.js";
20
+ import {
21
+ batchQueryRows,
22
+ type Database,
23
+ supportsDrizzleTransaction,
24
+ } from "../db/index.js";
21
25
  import type { DatabaseDialect } from "../db/dialect.js";
22
26
  import {
23
27
  sqliteSchemaBundle,
@@ -201,6 +205,8 @@ export interface MediaFilters {
201
205
  limit?: number;
202
206
  /** Filter by MIME type prefix, e.g. "image/" */
203
207
  mimePrefix?: string;
208
+ /** Exclusive media ID cursor: the last item of the previous page. */
209
+ cursor?: string;
204
210
  }
205
211
 
206
212
  export interface CreateTextAttachmentData {
@@ -723,11 +729,15 @@ export function createMediaService(
723
729
  const result = new Map<string, Media[]>();
724
730
  if (postIds.length === 0) return result;
725
731
 
726
- const rows = await db
727
- .select()
728
- .from(media)
729
- .where(and(eq(media.siteId, siteId), inArray(media.postId, postIds)))
730
- .orderBy(asc(media.position));
732
+ // A feed asks for every post it renders at once, which can be hundreds.
733
+ // Each post's attachments land in one chunk, so their order holds.
734
+ const rows = await batchQueryRows([...new Set(postIds)], (chunk) =>
735
+ db
736
+ .select()
737
+ .from(media)
738
+ .where(and(eq(media.siteId, siteId), inArray(media.postId, chunk)))
739
+ .orderBy(asc(media.position)),
740
+ );
731
741
 
732
742
  for (const row of rows) {
733
743
  const m = toMedia(row);
@@ -766,11 +776,16 @@ export function createMediaService(
766
776
  sql`${media.mimeType} LIKE ${filters.mimePrefix + "%"}`,
767
777
  );
768
778
  }
779
+ if (filters?.cursor) {
780
+ conditions.push(lt(media.id, filters.cursor));
781
+ }
782
+ // Newest first. A TypeID sorts by the time it was minted, so the ID
783
+ // orders like `created_at` and never ties, which a cursor needs.
769
784
  const rows = await db
770
785
  .select()
771
786
  .from(media)
772
787
  .where(conditions.length > 0 ? and(...conditions) : undefined)
773
- .orderBy(desc(media.createdAt))
788
+ .orderBy(desc(media.id))
774
789
  .limit(limit);
775
790
  return rows.map(toMedia);
776
791
  },