@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
@@ -13,19 +13,35 @@ import { z } from "zod";
13
13
  import type { Bindings } from "../../types.js";
14
14
  import type { AppVariables } from "../../types/app-context.js";
15
15
  import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
16
- import { getI18n, isLocale, resolveCatalogLocale } from "../../i18n/index.js";
16
+ import { getI18n, isLocale } from "../../i18n/index.js";
17
17
  import { renderPublicPage } from "../../lib/render.js";
18
18
  import { getNavigationData } from "../../lib/navigation.js";
19
19
  import { buildPageTitle } from "../../lib/page-title.js";
20
20
  import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
21
21
  import { getTimeZoneOptions } from "../../lib/timezones.js";
22
- import { ValidationError } from "../../lib/errors.js";
22
+ import { getOrBuildEntry } from "../../i18n/supported-locales.js";
23
+ import {
24
+ DomainError,
25
+ LanguageInUseError,
26
+ UnauthorizedError,
27
+ ValidationError,
28
+ } from "../../lib/errors.js";
23
29
  import { SETTINGS_KEYS } from "../../lib/constants.js";
30
+ import { announceInBackground } from "../discover-announce.js";
31
+ import {
32
+ DISCOVER_FIRST_READ_MAX_HOURS,
33
+ DISCOVER_MIN_PUBLIC_POSTS,
34
+ getDiscoverPageUrls,
35
+ getDiscoverSubmitUrl,
36
+ measureDiscoverMaturity,
37
+ parseDiscoverSetting,
38
+ } from "../../lib/discover.js";
24
39
  import { getAvailableThemes } from "../../lib/theme.js";
25
40
  import { THEME_MODES, type ThemeMode } from "../../types/config.js";
26
41
  import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
27
42
  import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
28
43
  import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
44
+ import { LanguageContent } from "../../ui/dash/settings/LanguageContent.js";
29
45
  import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
30
46
  import { AccountMenuContent } from "../../ui/dash/settings/AccountMenuContent.js";
31
47
  import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
@@ -52,16 +68,24 @@ import {
52
68
  } from "../../lib/telegram-settings-status.js";
53
69
  import { TelegramContent } from "../../ui/dash/settings/TelegramContent.js";
54
70
  import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
55
- import { parseValidated, UpdateSiteSettingsSchema } from "../../lib/schemas.js";
71
+ import {
72
+ ContentLanguageSchema,
73
+ parseValidated,
74
+ UpdateSiteSettingsSchema,
75
+ } from "../../lib/schemas.js";
56
76
  import {
57
77
  getHostedControlPlaneAccountPasswordUrl,
58
78
  getHostedControlPlaneAccountUrl,
59
79
  getHostedControlPlaneProviderLabel,
60
80
  getHostedControlPlaneSiteDeleteUrl,
61
81
  getHostedControlPlaneSiteSettingsUrl,
82
+ isHostedControlPlaneEnabled,
62
83
  } from "../../lib/hosted-signin.js";
63
84
  import { syncHostedControlPlaneSiteAvatar } from "../../lib/hosted-control-plane-sync.js";
64
85
  import {
86
+ getDiscoverDefault,
87
+ getDiscoverDirectoryBaseUrl,
88
+ getDiscoverPingUrl,
65
89
  getGitHubAppConfig,
66
90
  getHostedControlPlaneSsoSecret,
67
91
  getTelegramBotPool,
@@ -95,15 +119,26 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
95
119
 
96
120
  export const settingsRoutes = new Hono<Env>();
97
121
 
98
- const UpdateLocaleSettingsSchema = z.object({
99
- siteLanguage: z.string(),
100
- // Optional for back-compat: absent = leave the dashboard language untouched,
101
- // "" = clear it (follow content language).
102
- dashboardLanguage: z.string().optional(),
103
- cjkSerifFont: z.string(),
122
+ const UpdateTimeSettingsSchema = z.object({
104
123
  timeZone: z.string(),
105
124
  });
106
125
 
126
+ // Both fields are optional: the language page saves one control at a time, and
127
+ // an absent field means "leave it alone" rather than "clear it".
128
+ const UpdateLanguageSettingsSchema = z.object({
129
+ contentLanguage: ContentLanguageSchema.optional(),
130
+ dashboardLanguage: z.string().optional(),
131
+ });
132
+
133
+ const LanguageTagSchema = z.object({
134
+ language: ContentLanguageSchema,
135
+ });
136
+
137
+ const EnableMultilingualSchema = z.object({
138
+ primary: ContentLanguageSchema,
139
+ additional: z.array(ContentLanguageSchema),
140
+ });
141
+
107
142
  const UpdateFeedSettingsSchema = z.object({
108
143
  mainRssFeed: z.enum(["featured", "latest"]),
109
144
  });
@@ -116,6 +151,13 @@ const UpdateSearchSettingsSchema = z.object({
116
151
  allowIndexing: z.boolean(),
117
152
  });
118
153
 
154
+ // Only the two values the control writes. `featured` is still a stored value
155
+ // older releases wrote and the feed still declares, but nothing writes it any
156
+ // more: which of a blog's posts reach which list is the directory's rule.
157
+ const UpdateDiscoverSettingsSchema = z.object({
158
+ discover: z.enum(["latest", "off"]),
159
+ });
160
+
119
161
  function publicPath(c: Context<Env>, path: string): string {
120
162
  return toPublicPath(path, c.var.appConfig.sitePathPrefix);
121
163
  }
@@ -124,6 +166,22 @@ function aboutEditPath(c: Context<Env>): string {
124
166
  return `${publicPath(c, "/about")}?edit=1`;
125
167
  }
126
168
 
169
+ /**
170
+ * The session id behind the current request.
171
+ *
172
+ * Every `/settings` route sits behind `requireAuth`, so a missing session here
173
+ * is a wiring mistake rather than a signed-out visitor — hence a throw instead
174
+ * of a redirect.
175
+ */
176
+ function requireSessionId(c: Context<Env>): string {
177
+ const sessionId = c.var.session?.session?.id;
178
+ if (!sessionId) {
179
+ throw new UnauthorizedError();
180
+ }
181
+
182
+ return sessionId;
183
+ }
184
+
127
185
  /**
128
186
  * Breadcrumb labels for admin settings pages.
129
187
  *
@@ -138,6 +196,7 @@ function breadcrumbLabel(
138
196
  key:
139
197
  | "settings"
140
198
  | "general"
199
+ | "language"
141
200
  | "avatar"
142
201
  | "navigation"
143
202
  | "colorTheme"
@@ -163,6 +222,10 @@ function breadcrumbLabel(
163
222
  return i18n._(
164
223
  msg({ message: "General", comment: "@context: Breadcrumb label" }),
165
224
  );
225
+ case "language":
226
+ return i18n._(
227
+ msg({ message: "Language", comment: "@context: Breadcrumb label" }),
228
+ );
166
229
  case "avatar":
167
230
  return i18n._(
168
231
  msg({ message: "Avatar", comment: "@context: Breadcrumb label" }),
@@ -318,11 +381,44 @@ settingsRoutes.get("/general", async (c) => {
318
381
 
319
382
  const dbSiteName = allSettings["SITE_NAME"] ?? "";
320
383
  const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
384
+ // The stored choice, not the effective mode: the control has to be able to
385
+ // show "never chosen", which is what lets `noindex` and then the deployment
386
+ // default decide instead.
387
+ const discoverSetting = parseDiscoverSetting(allSettings["DISCOVER"]);
388
+ // The deployment's own answer, and nothing else folded into it: `latest` on
389
+ // a deployment that lists its fleet, absent on an ordinary self-hosted site.
390
+ // The gates that sit above it — `noindex` most of all — are applied in the
391
+ // browser instead, because they are controls on this same page and a value
392
+ // resolved here would freeze at page load. The component runs the same
393
+ // `resolveDiscoverMode` this route would have.
394
+ const discoverDefault = parseDiscoverSetting(getDiscoverDefault(c.env)) ?? "";
321
395
 
322
396
  const saved = c.req.query("saved") !== undefined;
323
- const [navData, aboutPage] = await Promise.all([
397
+ // What the site can answer about its own standing in the directory, without
398
+ // asking one. The directory deliberately does not take status queries, so
399
+ // everything shown here is local evidence.
400
+ const publicPostFilters = {
401
+ status: "published" as const,
402
+ excludeReplies: true,
403
+ excludeLatestHidden: true,
404
+ excludePrivate: true,
405
+ };
406
+ const [
407
+ navData,
408
+ aboutPage,
409
+ announceState,
410
+ publicPostCount,
411
+ featuredPostCount,
412
+ ] = await Promise.all([
324
413
  getNavigationData(c),
325
414
  c.var.services.aboutPage.getStatus(),
415
+ c.var.services.settings.getDiscoverAnnounceState(),
416
+ c.var.services.posts.count(publicPostFilters),
417
+ c.var.services.posts.countFeaturedThreadRoots({
418
+ status: "published",
419
+ excludePrivate: true,
420
+ excludeLatestHidden: true,
421
+ }),
326
422
  ]);
327
423
  const siteUrlForDisplay =
328
424
  appConfig.siteUrl || new URL(publicPath(c, "/"), c.req.url).toString();
@@ -341,13 +437,6 @@ settingsRoutes.get("/general", async (c) => {
341
437
  <GeneralContent
342
438
  siteName={dbSiteName || ""}
343
439
  siteDescription={dbSiteDescription || ""}
344
- siteLanguage={appConfig.siteLanguage}
345
- dashboardLanguage={
346
- isLocale(appConfig.dashboardLanguage)
347
- ? appConfig.dashboardLanguage
348
- : resolveCatalogLocale(appConfig.siteLanguage)
349
- }
350
- cjkSerifFont={appConfig.cjkSerifFont}
351
440
  siteNameFallback={appConfig.fallbacks.siteName}
352
441
  siteDescriptionFallback={appConfig.fallbacks.siteDescription}
353
442
  mainRssFeed={appConfig.mainRssFeed}
@@ -375,6 +464,32 @@ settingsRoutes.get("/general", async (c) => {
375
464
  siteFooter={appConfig.siteFooter}
376
465
  showJantBrandingOnHome={appConfig.showJantBrandingOnHome}
377
466
  noindex={appConfig.noindex}
467
+ discover={discoverSetting ?? ""}
468
+ discoverDefault={discoverDefault}
469
+ discoverPages={getDiscoverPageUrls(
470
+ getDiscoverDirectoryBaseUrl(c.env),
471
+ )}
472
+ discoverStatus={{
473
+ announced: announceState?.ok ?? null,
474
+ announceError: announceState?.error ?? null,
475
+ announceAt: announceState?.at ?? null,
476
+ // No directory configured means nothing to announce to, and the
477
+ // whole announcement block is beside the point.
478
+ hasDirectory: getDiscoverPingUrl(c.env) !== undefined,
479
+ // A hosted fleet is enrolled by its control plane, so there is no
480
+ // announcement for the owner to make, chase, or retry.
481
+ managedByHost: isHostedControlPlaneEnabled(c.env),
482
+ submitUrl: getDiscoverSubmitUrl(getDiscoverDirectoryBaseUrl(c.env)),
483
+ // The mode the feeds actually declare, already derived once on
484
+ // `appConfig` — re-deriving it here is how this block used to
485
+ // miss the deployment default.
486
+ declaredMode: appConfig.discover,
487
+ ...measureDiscoverMaturity({ publicPostCount }),
488
+ featuredPostCount,
489
+ minPublicPosts: DISCOVER_MIN_PUBLIC_POSTS,
490
+ firstReadMaxHours: DISCOVER_FIRST_READ_MAX_HOURS,
491
+ }}
492
+ rssFeedsEnabled={appConfig.rssFeedsEnabled}
378
493
  demoMode={appConfig.demoMode}
379
494
  timezones={getTimeZoneOptions(appConfig.timeZone)}
380
495
  aboutPage={aboutPage}
@@ -451,42 +566,286 @@ settingsRoutes.post("/general", async (c) => {
451
566
  }
452
567
  });
453
568
 
454
- settingsRoutes.post("/general/language-time", async (c) => {
569
+ // Time zone only. Language moved to its own page, so this no longer sends —
570
+ // and must not clear — the language fields.
571
+ settingsRoutes.post("/general/time", async (c) => {
455
572
  const i18n = getI18n(c);
456
- const body = parseValidated(UpdateLocaleSettingsSchema, await c.req.json());
573
+ const body = parseValidated(UpdateTimeSettingsSchema, await c.req.json());
457
574
  const toast = i18n._(
458
575
  msg({
459
- message: "Language and time updated.",
460
- comment: "@context: Toast after saving language and time settings",
576
+ message: "Time zone updated.",
577
+ comment: "@context: Toast after saving the time zone",
461
578
  }),
462
579
  );
463
- const { languageChanged } =
464
- await c.var.services.settings.updateLocaleSettings(body, {
465
- oldLanguage: c.var.appConfig.siteLanguage,
466
- oldCjkSerifFont: c.var.appConfig.cjkSerifFont,
467
- oldDashboardLanguage: c.var.appConfig.dashboardLanguage,
468
- });
580
+ await c.var.services.settings.updateLocaleSettings(body, {
581
+ oldLanguage: c.var.appConfig.siteLanguage,
582
+ });
469
583
 
470
584
  const wantsJson = c.req.header("accept")?.includes("application/json");
471
585
  if (wantsJson) {
472
- if (languageChanged) {
473
- return c.json({
474
- status: "redirect" as const,
475
- url: publicPath(c, "/settings/general?saved"),
476
- });
477
- }
586
+ return c.json({ status: "ok" as const, toast });
587
+ }
588
+
589
+ return dsToast(toast);
590
+ });
591
+
592
+ // ===========================================================================
593
+ // Language
594
+ //
595
+ // Every language setting lives on one page, and every write goes through the
596
+ // language service — the multilingual keys are DB-only precisely so this is the
597
+ // only door.
598
+ // ===========================================================================
599
+
600
+ settingsRoutes.get("/language", async (c) => {
601
+ const { appConfig } = c.var;
602
+ const navData = await getNavigationData(c);
603
+ const [state, preview] = await Promise.all([
604
+ c.var.services.language.getState(),
605
+ c.var.services.language.getEnablePreview(),
606
+ ]);
607
+
608
+ return renderPublicPage(c, {
609
+ title: buildPageTitle("Language", navData.siteName),
610
+ navData,
611
+ content: (
612
+ <>
613
+ <AdminBreadcrumb
614
+ parent={breadcrumbLabel(c, "settings")}
615
+ parentHref={publicPath(c, "/settings")}
616
+ current={breadcrumbLabel(c, "language")}
617
+ />
618
+ <LanguageContent
619
+ contentLanguage={state.primary}
620
+ // Unset stays unset rather than being shown as the locale it
621
+ // currently resolves to: "Follow content language" is a real choice
622
+ // on this page, and it is the one most sites are on.
623
+ dashboardLanguage={
624
+ isLocale(appConfig.dashboardLanguage)
625
+ ? appConfig.dashboardLanguage
626
+ : ""
627
+ }
628
+ multilingualEnabled={state.enabled}
629
+ additionalLanguages={state.additional}
630
+ unmarkedPostCount={preview.pendingCount}
631
+ sitePathPrefix={appConfig.sitePathPrefix}
632
+ />
633
+ </>
634
+ ),
635
+ });
636
+ });
478
637
 
638
+ /**
639
+ * Run a language mutation and report it as JSON.
640
+ *
641
+ * Every language endpoint has the same shape — do the thing, or explain in one
642
+ * sentence why it could not be done — so they share one wrapper rather than
643
+ * repeating the try/catch. The toast may be built from what the action
644
+ * returned, for the one operation whose outcome is not known until it runs.
645
+ */
646
+ async function respondToLanguageAction<T>(
647
+ c: Context<Env>,
648
+ toast: string | ((result: T) => string),
649
+ action: () => Promise<T>,
650
+ ): Promise<Response> {
651
+ try {
652
+ const result = await action();
479
653
  return c.json({
480
654
  status: "ok" as const,
481
- toast,
655
+ toast: typeof toast === "function" ? toast(result) : toast,
482
656
  });
657
+ } catch (error) {
658
+ // The one refusal an author will actually meet — removing or dropping a
659
+ // language that posts still use — gets a localized sentence naming the
660
+ // language; the settings page pairs it with a link to those posts.
661
+ if (error instanceof LanguageInUseError) {
662
+ const i18n = getI18n(c);
663
+ return c.json(
664
+ {
665
+ error: i18n._(
666
+ msg({
667
+ message:
668
+ "{count, plural, one {# post is} other {# posts are}} still written in {language}. Change their language, or keep the language.",
669
+ comment:
670
+ "@context: Error when a content language that posts still use would be dropped",
671
+ }),
672
+ {
673
+ count: error.postCount,
674
+ language: getOrBuildEntry(error.language).native,
675
+ },
676
+ ),
677
+ code: error.code,
678
+ language: error.language,
679
+ },
680
+ 400,
681
+ );
682
+ }
683
+ if (error instanceof DomainError) {
684
+ return c.json({ error: error.message, code: error.code }, 400);
685
+ }
686
+ throw error;
483
687
  }
688
+ }
689
+
690
+ settingsRoutes.post("/language", async (c) => {
691
+ const i18n = getI18n(c);
692
+ const body = parseValidated(UpdateLanguageSettingsSchema, await c.req.json());
693
+
694
+ return respondToLanguageAction(
695
+ c,
696
+ i18n._(
697
+ msg({
698
+ message: "Language updated.",
699
+ comment: "@context: Toast after saving a language setting",
700
+ }),
701
+ ),
702
+ async () => {
703
+ await c.var.services.settings.updateLocaleSettings(
704
+ {
705
+ siteLanguage: body.contentLanguage,
706
+ dashboardLanguage: body.dashboardLanguage,
707
+ },
708
+ {
709
+ oldLanguage: c.var.appConfig.siteLanguage,
710
+ oldDashboardLanguage: c.var.appConfig.dashboardLanguage,
711
+ },
712
+ );
713
+ },
714
+ );
715
+ });
716
+
717
+ settingsRoutes.post("/language/enable", async (c) => {
718
+ const i18n = getI18n(c);
719
+ const body = parseValidated(EnableMultilingualSchema, await c.req.json());
484
720
 
485
- if (languageChanged) {
486
- return dsRedirect(publicPath(c, "/settings/general?saved"));
721
+ try {
722
+ const { markedCount } = await c.var.services.language.enable(body);
723
+ return c.json({
724
+ status: "ok" as const,
725
+ toast:
726
+ markedCount > 0
727
+ ? i18n._(
728
+ msg({
729
+ message:
730
+ "Multilingual content is on. {count, plural, one {# post was marked} other {# posts were marked}} as {language}.",
731
+ comment:
732
+ "@context: Toast after turning multilingual content on with existing posts",
733
+ }),
734
+ // The language's own name, not its tag — the author picked
735
+ // "繁體中文" in the dialog, not "zh-Hant".
736
+ {
737
+ count: markedCount,
738
+ language: getOrBuildEntry(body.primary).native,
739
+ },
740
+ )
741
+ : i18n._(
742
+ msg({
743
+ message: "Multilingual content is on.",
744
+ comment:
745
+ "@context: Toast after turning multilingual content on with no existing posts",
746
+ }),
747
+ ),
748
+ });
749
+ } catch (error) {
750
+ if (error instanceof LanguageInUseError) {
751
+ // Worded for the dialog it lands in: the fix is putting the language
752
+ // back on the list right there, and the response carries the tag so
753
+ // the dialog can offer that as one click.
754
+ return c.json(
755
+ {
756
+ error: i18n._(
757
+ msg({
758
+ message:
759
+ "{count, plural, one {# post is} other {# posts are}} written in {language}, which is not on this list. Add it back, or change their language first.",
760
+ comment:
761
+ "@context: Refusal in the multilingual dialog when the list drops a language that posts still use",
762
+ }),
763
+ {
764
+ count: error.postCount,
765
+ language: getOrBuildEntry(error.language).native,
766
+ },
767
+ ),
768
+ code: error.code,
769
+ language: error.language,
770
+ },
771
+ 400,
772
+ );
773
+ }
774
+ if (error instanceof DomainError) {
775
+ return c.json({ error: error.message, code: error.code }, 400);
776
+ }
777
+ throw error;
487
778
  }
779
+ });
488
780
 
489
- return dsToast(toast);
781
+ settingsRoutes.post("/language/disable", async (c) => {
782
+ const i18n = getI18n(c);
783
+ return respondToLanguageAction(
784
+ c,
785
+ i18n._(
786
+ msg({
787
+ message: "Multilingual content is off. Your languages are still saved.",
788
+ comment: "@context: Toast after turning multilingual content off",
789
+ }),
790
+ ),
791
+ () => c.var.services.language.disable(),
792
+ );
793
+ });
794
+
795
+ settingsRoutes.post("/language/primary", async (c) => {
796
+ const i18n = getI18n(c);
797
+ const body = parseValidated(LanguageTagSchema, await c.req.json());
798
+ return respondToLanguageAction(
799
+ c,
800
+ i18n._(
801
+ msg({
802
+ message: "Primary language changed.",
803
+ comment: "@context: Toast after changing the primary language",
804
+ }),
805
+ ),
806
+ () => c.var.services.language.setPrimary(body.language),
807
+ );
808
+ });
809
+
810
+ settingsRoutes.post("/language/add", async (c) => {
811
+ const i18n = getI18n(c);
812
+ const body = parseValidated(LanguageTagSchema, await c.req.json());
813
+ return respondToLanguageAction(
814
+ c,
815
+ i18n._(
816
+ msg({
817
+ message: "Language added.",
818
+ comment: "@context: Toast after adding a content language",
819
+ }),
820
+ ),
821
+ () => c.var.services.language.addLanguage(body.language),
822
+ );
823
+ });
824
+
825
+ settingsRoutes.post("/language/remove", async (c) => {
826
+ const i18n = getI18n(c);
827
+ const body = parseValidated(LanguageTagSchema, await c.req.json());
828
+ return respondToLanguageAction(
829
+ c,
830
+ // The last language takes the feature with it, and that is the larger of
831
+ // the two facts — the author leaves this page single-language.
832
+ ({ multilingualDisabled }) =>
833
+ multilingualDisabled
834
+ ? i18n._(
835
+ msg({
836
+ message: "Language removed. Multilingual content is off.",
837
+ comment:
838
+ "@context: Toast after removing the last additional content language",
839
+ }),
840
+ )
841
+ : i18n._(
842
+ msg({
843
+ message: "Language removed.",
844
+ comment: "@context: Toast after removing a content language",
845
+ }),
846
+ ),
847
+ () => c.var.services.language.removeLanguage(body.language),
848
+ );
490
849
  });
491
850
 
492
851
  settingsRoutes.post("/general/feeds", async (c) => {
@@ -527,6 +886,88 @@ settingsRoutes.post("/general/home", async (c) => {
527
886
  return dsToast(toast);
528
887
  });
529
888
 
889
+ /**
890
+ * Announce, because the owner asked.
891
+ *
892
+ * The route the settings page offers whenever the directory has not heard from
893
+ * this site: an announcement that failed, and one that was never made at all.
894
+ * Sending it is safe at any time — the receiving end treats a repeat as a
895
+ * no-op for a site it already knows, and it carries nothing but the feed
896
+ * address.
897
+ */
898
+ settingsRoutes.post("/general/discover/announce", async (c) => {
899
+ const i18n = getI18n(c);
900
+ const { appConfig } = c.var;
901
+
902
+ const started =
903
+ !appConfig.demoMode &&
904
+ announceInBackground(c, c.var.allSettings["DISCOVER"]);
905
+
906
+ const toast = started
907
+ ? i18n._(
908
+ msg({
909
+ message: "Announcing your site. Reload to see the result.",
910
+ comment:
911
+ "@context: Toast after sending the Discover announcement. It runs in the background, so the page does not yet know how it went.",
912
+ }),
913
+ )
914
+ : i18n._(
915
+ msg({
916
+ message: "This site has no directory to announce to.",
917
+ comment:
918
+ "@context: Toast when the Discover announcement cannot be sent because no directory is configured, or the site cannot be listed at all",
919
+ }),
920
+ );
921
+
922
+ // The settings bridge is the only caller, and it always asks for JSON — a
923
+ // Datastar toast reaches it as a body it cannot parse, which it reports as a
924
+ // failed save whatever the server actually did. Every other settings route
925
+ // answers both shapes; this one has to as well.
926
+ const wantsJson = c.req.header("accept")?.includes("application/json");
927
+ if (wantsJson) {
928
+ return c.json({ status: "ok" as const, toast });
929
+ }
930
+
931
+ return dsToast(toast);
932
+ });
933
+
934
+ settingsRoutes.post("/general/discover", async (c) => {
935
+ const i18n = getI18n(c);
936
+ const body = parseValidated(UpdateDiscoverSettingsSchema, await c.req.json());
937
+ const { appConfig } = c.var;
938
+
939
+ const { shouldAnnounce } =
940
+ await c.var.services.settings.updateDiscoverSetting(body.discover, {
941
+ demoMode: appConfig.demoMode,
942
+ });
943
+
944
+ if (shouldAnnounce) {
945
+ announceInBackground(c, body.discover);
946
+ }
947
+
948
+ const wantsJson = c.req.header("accept")?.includes("application/json");
949
+ if (wantsJson) {
950
+ return c.json({
951
+ status: "ok" as const,
952
+ toast: i18n._(
953
+ msg({
954
+ message: "Site visibility updated.",
955
+ comment: "@context: Toast after saving the site visibility settings",
956
+ }),
957
+ ),
958
+ });
959
+ }
960
+
961
+ return dsToast(
962
+ i18n._(
963
+ msg({
964
+ message: "Site visibility updated.",
965
+ comment: "@context: Toast after saving the site visibility settings",
966
+ }),
967
+ ),
968
+ );
969
+ });
970
+
530
971
  settingsRoutes.post("/general/search", async (c) => {
531
972
  const i18n = getI18n(c);
532
973
  const body = parseValidated(UpdateSearchSettingsSchema, await c.req.json());
@@ -795,7 +1236,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
795
1236
  settingsRoutes.get("/navigation", async (c) => {
796
1237
  const [navItems, directoryData, suggestedLinks] = await Promise.all([
797
1238
  c.var.services.navItems.list(),
798
- c.var.services.collections.listDirectoryData(),
1239
+ c.var.services.collections.listDirectoryData({ isAuthenticated: true }),
799
1240
  c.var.services.navItems.listSuggestedLinks({
800
1241
  siteOrigin: c.var.appConfig.siteOrigin,
801
1242
  sitePathPrefix: c.var.appConfig.sitePathPrefix,
@@ -1332,7 +1773,9 @@ settingsRoutes.get("/account/delete-account", async (c) => {
1332
1773
  }
1333
1774
 
1334
1775
  const navData = await getNavigationData(c);
1335
- const csrfToken = await c.var.services.auth.generateDeleteCsrfToken();
1776
+ const csrfToken = await c.var.services.auth.generateDeleteCsrfToken(
1777
+ requireSessionId(c),
1778
+ );
1336
1779
 
1337
1780
  return renderPublicPage(c, {
1338
1781
  title: buildPageTitle("Delete Account", navData.siteName),
@@ -1390,7 +1833,10 @@ settingsRoutes.post("/account/delete-account", async (c) => {
1390
1833
  );
1391
1834
  }
1392
1835
 
1393
- const isValid = await c.var.services.auth.validateDeleteCsrfToken(csrfToken);
1836
+ const isValid = await c.var.services.auth.validateDeleteCsrfToken(
1837
+ csrfToken,
1838
+ requireSessionId(c),
1839
+ );
1394
1840
  if (!isValid) {
1395
1841
  return dsToast(
1396
1842
  i18n._(
@@ -1404,11 +1850,26 @@ settingsRoutes.post("/account/delete-account", async (c) => {
1404
1850
  );
1405
1851
  }
1406
1852
 
1853
+ // Sign the browser out first. Every row behind this session is about to
1854
+ // disappear, and better-auth's cookie cache would keep answering "signed in"
1855
+ // from the browser's own copy for minutes afterwards — a ticket to a site
1856
+ // that no longer exists, handed to whoever completes the next setup.
1857
+ let signOutHeaders: Headers | undefined;
1858
+ try {
1859
+ const signedOut = await c.var.auth.api.signOut({
1860
+ headers: c.req.raw.headers,
1861
+ asResponse: true,
1862
+ });
1863
+ signOutHeaders = signedOut.headers;
1864
+ } catch {
1865
+ // The wipe below removes the session either way.
1866
+ }
1867
+
1407
1868
  await c.var.services.auth.deleteAllData({
1408
1869
  storage: c.var.storage,
1409
1870
  });
1410
1871
 
1411
- return dsRedirect(publicPath(c, "/setup"));
1872
+ return dsRedirect(publicPath(c, "/setup"), { headers: signOutHeaders });
1412
1873
  });
1413
1874
 
1414
1875
  // ===========================================================================