@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. 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
+ }
@@ -433,7 +433,9 @@ const mcpTools: McpToolDefinition[] = [
433
433
  };
434
434
  }
435
435
 
436
- return context.services.collections.listDirectoryData();
436
+ return context.services.collections.listDirectoryData({
437
+ isAuthenticated: true,
438
+ });
437
439
  },
438
440
  },
439
441
  {
@@ -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,
@@ -723,11 +727,15 @@ export function createMediaService(
723
727
  const result = new Map<string, Media[]>();
724
728
  if (postIds.length === 0) return result;
725
729
 
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));
730
+ // A feed asks for every post it renders at once, which can be hundreds.
731
+ // Each post's attachments land in one chunk, so their order holds.
732
+ const rows = await batchQueryRows([...new Set(postIds)], (chunk) =>
733
+ db
734
+ .select()
735
+ .from(media)
736
+ .where(and(eq(media.siteId, siteId), inArray(media.postId, chunk)))
737
+ .orderBy(asc(media.position)),
738
+ );
731
739
 
732
740
  for (const row of rows) {
733
741
  const m = toMedia(row);