@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
@@ -27,6 +27,13 @@ export interface SearchOptions {
27
27
  status?: Status[];
28
28
  /** Filter by format */
29
29
  format?: Format;
30
+ /**
31
+ * Restrict to one BCP 47 content language (canonical form, matched exactly).
32
+ *
33
+ * The index itself is language-agnostic — one shared index serves every
34
+ * language — so this is a plain residual predicate on the matched rows.
35
+ */
36
+ lang?: string;
30
37
  }
31
38
 
32
39
  export interface SearchService {
@@ -54,6 +61,8 @@ interface RawSearchRow {
54
61
  rating: number | null;
55
62
  reply_to_id: string | null;
56
63
  thread_id: string;
64
+ language: string | null;
65
+ translation_group_id: string | null;
57
66
  /** SQLite returns 0/1, Postgres a boolean. */
58
67
  quiet_reply: number | boolean | null;
59
68
  published_at: number | null;
@@ -95,6 +104,8 @@ function mapRow(row: RawSearchRow): SearchResult {
95
104
  previewProvider: null,
96
105
  replyToId: row.reply_to_id,
97
106
  threadId: row.thread_id,
107
+ language: row.language,
108
+ translationGroupId: row.translation_group_id,
98
109
  quietReply: Boolean(row.quiet_reply),
99
110
  publishedAt: row.published_at,
100
111
  lastActivityAt:
@@ -181,6 +192,8 @@ export function createSearchService(
181
192
  const statusPlaceholders = status.map(() => "?").join(", ");
182
193
  const formatFilter = options.format ? "AND post.format = ?" : "";
183
194
  const formatParams = options.format ? [options.format] : [];
195
+ const langFilter = options.lang ? "AND post.language = ?" : "";
196
+ const langParams = options.lang ? [options.lang] : [];
184
197
 
185
198
  if (databaseDialect === "sqlite") {
186
199
  const ftsQuery = buildSqliteFtsQuery(query);
@@ -204,12 +217,21 @@ export function createSearchService(
204
217
  AND post.site_id = ?
205
218
  AND post.status IN (${statusPlaceholders})
206
219
  ${formatFilter}
220
+ ${langFilter}
207
221
  ORDER BY post_fts.rank
208
222
  LIMIT ? OFFSET ?
209
223
  `);
210
224
 
211
225
  const { results } = await stmt
212
- .bind(ftsQuery, siteId, ...status, ...formatParams, limit, offset)
226
+ .bind(
227
+ ftsQuery,
228
+ siteId,
229
+ ...status,
230
+ ...formatParams,
231
+ ...langParams,
232
+ limit,
233
+ offset,
234
+ )
213
235
  .all<RawSearchRow>();
214
236
 
215
237
  return withSnippetFallback((results || []).map(mapRow), query);
@@ -278,12 +300,21 @@ export function createSearchService(
278
300
  AND post.site_id = ?
279
301
  AND post.status IN (${statusPlaceholders})
280
302
  ${formatFilter}
303
+ ${langFilter}
281
304
  ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
282
305
  LIMIT ? OFFSET ?
283
306
  `);
284
307
 
285
308
  const { results } = await stmt
286
- .bind(tsQuery, siteId, ...status, ...formatParams, limit, offset)
309
+ .bind(
310
+ tsQuery,
311
+ siteId,
312
+ ...status,
313
+ ...formatParams,
314
+ ...langParams,
315
+ limit,
316
+ offset,
317
+ )
287
318
  .all<RawSearchRow>();
288
319
 
289
320
  return withSnippetFallback((results || []).map(mapRow), query);
@@ -300,6 +331,8 @@ export function createSearchService(
300
331
  const statusPlaceholders = status.map(() => "?").join(", ");
301
332
  const formatFilter = options.format ? "AND post.format = ?" : "";
302
333
  const formatParams = options.format ? [options.format] : [];
334
+ const langFilter = options.lang ? "AND post.language = ?" : "";
335
+ const langParams = options.lang ? [options.lang] : [];
303
336
 
304
337
  if (databaseDialect === "pg") {
305
338
  const stmt = rawQuery.prepare(`
@@ -324,6 +357,7 @@ export function createSearchService(
324
357
  AND post.site_id = ?
325
358
  AND post.status IN (${statusPlaceholders})
326
359
  ${formatFilter}
360
+ ${langFilter}
327
361
  ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
328
362
  LIMIT ? OFFSET ?
329
363
  `);
@@ -338,6 +372,7 @@ export function createSearchService(
338
372
  siteId,
339
373
  ...status,
340
374
  ...formatParams,
375
+ ...langParams,
341
376
  limit,
342
377
  offset,
343
378
  )
@@ -371,6 +406,7 @@ export function createSearchService(
371
406
  AND post.site_id = ?
372
407
  AND post.status IN (${statusPlaceholders})
373
408
  ${formatFilter}
409
+ ${langFilter}
374
410
  ${likeOrderBy}
375
411
  LIMIT ? OFFSET ?
376
412
  `);
@@ -384,6 +420,7 @@ export function createSearchService(
384
420
  siteId,
385
421
  ...status,
386
422
  ...formatParams,
423
+ ...langParams,
387
424
  limit,
388
425
  offset,
389
426
  )
@@ -15,6 +15,7 @@ import { now } from "../lib/time.js";
15
15
  import {
16
16
  SETTINGS_KEYS,
17
17
  ONBOARDING_STATUS,
18
+ type OnboardingStatus,
18
19
  type SettingsKey,
19
20
  } from "../lib/constants.js";
20
21
  import {
@@ -22,8 +23,8 @@ import {
22
23
  isLocale,
23
24
  isValidContentLanguage,
24
25
  normalizeContentLanguage,
26
+ resolveFirstRunDashboardLocale,
25
27
  } from "../i18n/locales.js";
26
- import { isCjkSerifFont } from "../i18n/detect.js";
27
28
  import type { StorageDriver } from "../lib/storage.js";
28
29
  import type { MediaService } from "./media.js";
29
30
  import {
@@ -34,9 +35,49 @@ import {
34
35
  import { arrayBufferToBase64 } from "../lib/favicon.js";
35
36
  import { ValidationError } from "../lib/errors.js";
36
37
  import { normalizeEditableSettingValue } from "../lib/schemas.js";
38
+ import { parseDiscoverSetting, type DiscoverSetting } from "../lib/discover.js";
39
+ import {
40
+ sendDiscoverPing,
41
+ type DiscoverAnnounceOutcome,
42
+ } from "../lib/discover-ping.js";
37
43
  import { isSupportedTimeZone, normalizeTimeZone } from "../lib/timezones.js";
38
44
  import type { FeedKind } from "../types/constants.js";
39
45
 
46
+ /**
47
+ * Read a stored announcement outcome back.
48
+ *
49
+ * Tolerant on purpose: this is a settings row a person can edit or an older
50
+ * version can have written differently, and the worst honest answer is "no
51
+ * attempt recorded" — never a crashed settings page.
52
+ *
53
+ * @param raw - The stored JSON, if any
54
+ * @returns The outcome, or null when absent or unreadable
55
+ */
56
+ function parseAnnounceState(
57
+ raw: string | null,
58
+ ): DiscoverAnnounceOutcome | null {
59
+ if (!raw) return null;
60
+ try {
61
+ const parsed: unknown = JSON.parse(raw);
62
+ if (typeof parsed !== "object" || parsed === null) return null;
63
+ const value = parsed as Record<string, unknown>;
64
+ if (typeof value["at"] !== "number") return null;
65
+ if (typeof value["ok"] !== "boolean") return null;
66
+ if (typeof value["feedUrl"] !== "string") return null;
67
+ return {
68
+ at: value["at"],
69
+ ok: value["ok"],
70
+ feedUrl: value["feedUrl"],
71
+ ...(typeof value["status"] === "number"
72
+ ? { status: value["status"] }
73
+ : {}),
74
+ ...(typeof value["error"] === "string" ? { error: value["error"] } : {}),
75
+ };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
40
81
  export interface GeneralSettingsData {
41
82
  siteName: string;
42
83
  siteDescription: string;
@@ -44,7 +85,6 @@ export interface GeneralSettingsData {
44
85
  siteLanguage: string;
45
86
  /** Admin UI locale; empty string follows the content language. */
46
87
  dashboardLanguage?: string;
47
- cjkSerifFont: string;
48
88
  showJantBrandingOnHome: boolean;
49
89
  mainRssFeed?: FeedKind;
50
90
  timeZone: string;
@@ -61,15 +101,23 @@ export interface SiteSettingsResult {
61
101
  siteNameChanged: boolean;
62
102
  }
63
103
 
104
+ /**
105
+ * Locale fields a settings page owns.
106
+ *
107
+ * Every field is "undefined = leave untouched" so the two pages that split
108
+ * these settings — Language and General — can each send only what they own,
109
+ * without either erasing the other's field.
110
+ */
64
111
  export interface LocaleSettingsData {
65
- siteLanguage: string;
112
+ /** Site content language; also the primary language when multilingual is on. */
113
+ siteLanguage?: string;
66
114
  /**
67
115
  * Admin dashboard UI locale. Empty string clears the explicit setting so the
68
116
  * dashboard follows the content language. When set, must be a catalog locale.
69
117
  */
70
118
  dashboardLanguage?: string;
71
- cjkSerifFont: string;
72
- timeZone: string;
119
+ /** IANA time zone; empty string resets to UTC. */
120
+ timeZone?: string;
73
121
  }
74
122
 
75
123
  export interface GeneralSettingsResult {
@@ -96,8 +144,27 @@ export interface SettingsService {
96
144
  set(key: SettingsKey, value: string): Promise<void>;
97
145
  setMany(entries: Partial<Record<SettingsKey, string>>): Promise<void>;
98
146
  remove(key: SettingsKey): Promise<void>;
147
+ /** How far first-run setup has got: pending → provisioned → completed. */
148
+ getOnboardingStatus(): Promise<OnboardingStatus>;
99
149
  isOnboardingComplete(): Promise<boolean>;
150
+ /**
151
+ * Mark a control-plane-created site as real but not yet confirmed by its
152
+ * owner. Public pages serve normally from here; setup still owes one answer.
153
+ */
154
+ markSiteProvisioned(): Promise<void>;
100
155
  completeOnboarding(): Promise<void>;
156
+ /**
157
+ * Close first-run setup on a site whose shell already exists, by recording
158
+ * the language its author says they write in.
159
+ *
160
+ * @param data - The confirmed content language, plus the browser's own
161
+ * language so the dashboard can be pinned to it
162
+ * @param opts - The language in effect before this answer
163
+ */
164
+ confirmFirstRunLanguage(
165
+ data: { siteLanguage: string; browserLanguage?: string | null },
166
+ opts: { oldLanguage: string },
167
+ ): Promise<void>;
101
168
  updateSiteSettings(
102
169
  data: SiteSettingsData,
103
170
  opts: { fallbackSiteName: string; oldSiteName: string },
@@ -106,7 +173,6 @@ export interface SettingsService {
106
173
  data: LocaleSettingsData,
107
174
  opts: {
108
175
  oldLanguage: string;
109
- oldCjkSerifFont?: string;
110
176
  oldDashboardLanguage?: string;
111
177
  },
112
178
  ): Promise<{ languageChanged: boolean }>;
@@ -116,6 +182,46 @@ export interface SettingsService {
116
182
  allowIndexing: boolean,
117
183
  opts: { demoMode: boolean },
118
184
  ): Promise<void>;
185
+ /**
186
+ * Record the site's Jant Discover choice.
187
+ *
188
+ * Always stores an explicit value, including `off`. Once someone has used
189
+ * this control their answer is their own, and the rule that reads `noindex`
190
+ * as a refusal stops applying to them.
191
+ *
192
+ * @param mode - The choice the owner just made
193
+ * @param opts - Demo sites never announce themselves
194
+ * @returns Whether this save is the moment the site opted in, and so should
195
+ * announce itself to the configured directory
196
+ */
197
+ updateDiscoverSetting(
198
+ mode: DiscoverSetting,
199
+ opts: { demoMode: boolean },
200
+ ): Promise<{ shouldAnnounce: boolean }>;
201
+ /**
202
+ * Announce this site's feed to the configured directory, and remember how it
203
+ * went.
204
+ *
205
+ * The remembering is the point. An announcement that fails silently leaves
206
+ * the owner with a setting that reads "on" and a directory that has never
207
+ * heard of them, and no way to tell the two apart.
208
+ *
209
+ * Never throws: the caller runs this as background work behind a settings
210
+ * save, which must not fail because a directory is down.
211
+ *
212
+ * @param input - The directory endpoint and the feed address to announce
213
+ * @returns What the attempt came to
214
+ */
215
+ announceToDiscover(input: {
216
+ endpoint: string;
217
+ feedUrl: string;
218
+ }): Promise<DiscoverAnnounceOutcome>;
219
+ /**
220
+ * The last announcement attempt, or `null` if the site has never made one.
221
+ *
222
+ * @returns The stored outcome, or null when absent or unreadable
223
+ */
224
+ getDiscoverAnnounceState(): Promise<DiscoverAnnounceOutcome | null>;
119
225
  /**
120
226
  * Update general site settings with trim/set/remove logic.
121
227
  * Empty strings are removed. Default values are removed to keep the DB clean.
@@ -128,7 +234,6 @@ export interface SettingsService {
128
234
  data: GeneralSettingsData,
129
235
  opts: {
130
236
  oldLanguage: string;
131
- oldCjkSerifFont?: string;
132
237
  fallbackSiteName: string;
133
238
  },
134
239
  ): Promise<GeneralSettingsResult>;
@@ -252,11 +357,29 @@ export function createSettingsService(
252
357
  });
253
358
  },
254
359
 
360
+ async getOnboardingStatus() {
361
+ const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
362
+ if (status === ONBOARDING_STATUS.COMPLETED) {
363
+ return ONBOARDING_STATUS.COMPLETED;
364
+ }
365
+ if (status === ONBOARDING_STATUS.PROVISIONED) {
366
+ return ONBOARDING_STATUS.PROVISIONED;
367
+ }
368
+ return ONBOARDING_STATUS.PENDING;
369
+ },
370
+
255
371
  async isOnboardingComplete() {
256
372
  const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
257
373
  return status === ONBOARDING_STATUS.COMPLETED;
258
374
  },
259
375
 
376
+ async markSiteProvisioned() {
377
+ await this.set(
378
+ SETTINGS_KEYS.ONBOARDING_STATUS,
379
+ ONBOARDING_STATUS.PROVISIONED,
380
+ );
381
+ },
382
+
260
383
  async completeOnboarding() {
261
384
  await this.set(
262
385
  SETTINGS_KEYS.ONBOARDING_STATUS,
@@ -264,6 +387,23 @@ export function createSettingsService(
264
387
  );
265
388
  },
266
389
 
390
+ async confirmFirstRunLanguage(data, opts) {
391
+ await this.updateLocaleSettings(
392
+ {
393
+ siteLanguage: data.siteLanguage,
394
+ // Empty means "follow the content language", which is right unless
395
+ // the browser named a catalog following would not reach.
396
+ dashboardLanguage:
397
+ resolveFirstRunDashboardLocale(
398
+ data.siteLanguage,
399
+ data.browserLanguage,
400
+ ) ?? "",
401
+ },
402
+ { oldLanguage: opts.oldLanguage },
403
+ );
404
+ await this.completeOnboarding();
405
+ },
406
+
267
407
  async updateSiteSettings(data, opts) {
268
408
  const trimmedSiteName = data.siteName.trim();
269
409
  const trimmedDescription = data.siteDescription.trim();
@@ -294,16 +434,18 @@ export function createSettingsService(
294
434
  },
295
435
 
296
436
  async updateLocaleSettings(data, opts) {
297
- const trimmedLanguage = data.siteLanguage.trim() || baseLocale;
298
- if (!isValidContentLanguage(trimmedLanguage)) {
299
- throw new ValidationError(
300
- "Enter a valid BCP 47 language tag (e.g. en, zh-Hans, fi, ja, fr-CA).",
301
- );
437
+ let languageChanged = false;
438
+ if (data.siteLanguage !== undefined) {
439
+ const trimmedLanguage = data.siteLanguage.trim() || baseLocale;
440
+ if (!isValidContentLanguage(trimmedLanguage)) {
441
+ throw new ValidationError(
442
+ "Enter a valid BCP 47 language tag (e.g. en, zh-Hans, fi, ja, fr-CA).",
443
+ );
444
+ }
445
+ const normalized = normalizeContentLanguage(trimmedLanguage);
446
+ await this.set("SITE_LANGUAGE", normalized);
447
+ languageChanged = opts.oldLanguage !== normalized;
302
448
  }
303
- await this.set(
304
- "SITE_LANGUAGE",
305
- normalizeContentLanguage(trimmedLanguage),
306
- );
307
449
 
308
450
  // Dashboard UI locale. undefined = leave untouched; "" = clear so the
309
451
  // dashboard follows the content language; otherwise it must be one of the
@@ -325,37 +467,24 @@ export function createSettingsService(
325
467
  (opts.oldDashboardLanguage ?? "") !== dashboardLanguage;
326
468
  }
327
469
 
328
- // Optional CJK fallback for content languages without a font profile.
329
- const cjkFont = data.cjkSerifFont?.trim() ?? "";
330
- if (cjkFont && isCjkSerifFont(cjkFont) && cjkFont !== "off") {
331
- await this.set("CJK_SERIF_FONT", cjkFont);
332
- } else {
333
- await this.remove("CJK_SERIF_FONT");
334
- }
335
-
336
- if (data.timeZone) {
337
- if (!isSupportedTimeZone(data.timeZone)) {
338
- throw new ValidationError("Choose a valid time zone.");
339
- }
340
-
341
- const normalizedTimeZone = normalizeTimeZone(data.timeZone);
342
- if (normalizedTimeZone !== "UTC") {
343
- await this.set("TIME_ZONE", normalizedTimeZone);
470
+ if (data.timeZone !== undefined) {
471
+ if (data.timeZone) {
472
+ if (!isSupportedTimeZone(data.timeZone)) {
473
+ throw new ValidationError("Choose a valid time zone.");
474
+ }
475
+
476
+ const normalizedTimeZone = normalizeTimeZone(data.timeZone);
477
+ if (normalizedTimeZone !== "UTC") {
478
+ await this.set("TIME_ZONE", normalizedTimeZone);
479
+ } else {
480
+ await this.remove("TIME_ZONE");
481
+ }
344
482
  } else {
345
483
  await this.remove("TIME_ZONE");
346
484
  }
347
- } else {
348
- await this.remove("TIME_ZONE");
349
485
  }
350
486
 
351
- const effectiveCjkFont =
352
- cjkFont && isCjkSerifFont(cjkFont) ? cjkFont : "off";
353
- return {
354
- languageChanged:
355
- opts.oldLanguage !== trimmedLanguage ||
356
- (opts.oldCjkSerifFont ?? "off") !== effectiveCjkFont ||
357
- dashboardChanged,
358
- };
487
+ return { languageChanged: languageChanged || dashboardChanged };
359
488
  },
360
489
 
361
490
  async updateFeedSettings(data) {
@@ -384,6 +513,73 @@ export function createSettingsService(
384
513
  }
385
514
  },
386
515
 
516
+ async updateDiscoverSetting(mode, opts) {
517
+ const previous = parseDiscoverSetting(await this.get("DISCOVER"));
518
+ await this.set("DISCOVER", mode);
519
+
520
+ // A ping says "read me now", so both edges of this setting are worth
521
+ // sending one. Opting in is the obvious half — and coming from unset
522
+ // counts, because a site that has never touched this control has never
523
+ // told anyone it exists. Opting out is the half that used to be missed:
524
+ // a directory that is not told keeps the blog until its next scheduled
525
+ // read, and the owner watches a blog they just removed sit there for
526
+ // another hour. Switching between `latest` and `featured` while already
527
+ // listed changes nothing a directory needs to be told twice.
528
+ const wasListed = previous !== null && previous !== "off";
529
+ const isListed = mode !== "off";
530
+ const changed = wasListed !== isListed;
531
+ return { shouldAnnounce: changed && !opts.demoMode };
532
+ },
533
+
534
+ async announceToDiscover(input) {
535
+ const outcome = await sendDiscoverPing({
536
+ endpoint: input.endpoint,
537
+ feedUrl: input.feedUrl,
538
+ now,
539
+ });
540
+ // Recording is best-effort, and the try/catch is what makes the "never
541
+ // throws" above true. The caller runs this as background work with
542
+ // nothing awaiting it, so a rejection here has nobody to reject to — on
543
+ // a Node runtime that ends the process. A settings row that could not be
544
+ // written is worth a log and a status block the owner cannot read; it is
545
+ // not worth the site.
546
+ try {
547
+ await this.set("DISCOVER_ANNOUNCE_STATE", JSON.stringify(outcome));
548
+ } catch (error) {
549
+ // eslint-disable-next-line no-console -- The owner's status block is now stale; say why.
550
+ console.error(
551
+ `[Jant] Discover announcement could not be recorded: ${
552
+ error instanceof Error ? error.message : String(error)
553
+ }`,
554
+ );
555
+ }
556
+
557
+ // The stored outcome is for the owner, and it does not always reach
558
+ // them: the announcement runs behind the settings save so the response
559
+ // cannot carry it, and a hosted site's settings page hides the status
560
+ // block altogether. Log it as well, so an announcement that never got
561
+ // through is answerable by whoever runs the deployment — a directory
562
+ // pointed at the wrong host answers 404 and looks, from the dashboard,
563
+ // exactly like one that worked.
564
+ if (outcome.ok) {
565
+ // eslint-disable-next-line no-console -- One-line audit trail for a rare write.
566
+ console.log(
567
+ `[Jant] Discover announced: feed=${outcome.feedUrl} directory=${input.endpoint}`,
568
+ );
569
+ } else {
570
+ // eslint-disable-next-line no-console -- A lost announcement must be visible.
571
+ console.error(
572
+ `[Jant] Discover announcement failed: ${outcome.error ?? "Unknown error."} feed=${outcome.feedUrl} directory=${input.endpoint}`,
573
+ );
574
+ }
575
+
576
+ return outcome;
577
+ },
578
+
579
+ async getDiscoverAnnounceState() {
580
+ return parseAnnounceState(await this.get("DISCOVER_ANNOUNCE_STATE"));
581
+ },
582
+
387
583
  async updateGeneral(data, opts) {
388
584
  const { displayName } = await this.updateSiteSettings(data, {
389
585
  fallbackSiteName: opts.fallbackSiteName,
@@ -392,7 +588,6 @@ export function createSettingsService(
392
588
  await this.updateHomeBranding(data.showJantBrandingOnHome);
393
589
  const { languageChanged } = await this.updateLocaleSettings(data, {
394
590
  oldLanguage: opts.oldLanguage,
395
- oldCjkSerifFont: opts.oldCjkSerifFont,
396
591
  });
397
592
 
398
593
  await this.updateFeedSettings({ mainRssFeed: data.mainRssFeed });
@@ -218,6 +218,7 @@ export function createSiteAdminService(
218
218
  media,
219
219
  navItems,
220
220
  pathRegistry,
221
+ smartCollections,
221
222
  threadCollections,
222
223
  posts,
223
224
  settings,
@@ -313,17 +314,20 @@ export function createSiteAdminService(
313
314
  databaseSchema,
314
315
  databaseDialect,
315
316
  );
316
- if (await settingsService.isOnboardingComplete()) {
317
+ if ((await settingsService.getOnboardingStatus()) !== "pending") {
317
318
  return;
318
319
  }
319
320
 
320
321
  await settingsService.set(SETTINGS_KEYS.SITE_NAME, input.siteName);
322
+ // The control plane can only pass a guess — today it forwards the locale
323
+ // the owner was browsing it in, which is their reading language, not
324
+ // necessarily the one they write in. It stands in until setup asks, so the
325
+ // site is never languageless, and setup offers it preselected.
321
326
  await settingsService.updateLocaleSettings(
322
327
  {
323
328
  siteLanguage: input.siteLanguage?.trim()
324
329
  ? detectLocaleFromHeader(input.siteLanguage)
325
330
  : baseLocale,
326
- cjkSerifFont: "off",
327
331
  timeZone: input.timeZone,
328
332
  },
329
333
  {
@@ -332,7 +336,9 @@ export function createSiteAdminService(
332
336
  );
333
337
  const navItems = createNavItemService(targetDb, siteId, databaseSchema);
334
338
  await navItems.materializeDefaultNavigation();
335
- await settingsService.completeOnboarding();
339
+ // Not `completeOnboarding()`: the site is real and readable from here, but
340
+ // its owner still owes setup the one answer nothing can infer.
341
+ await settingsService.markSiteProvisioned();
336
342
  }
337
343
 
338
344
  async function createWithDatabase(
@@ -509,6 +515,14 @@ export function createSiteAdminService(
509
515
  );
510
516
  await targetDb.delete(posts).where(eq(posts.siteId, siteId));
511
517
 
518
+ // Both kinds of collection, and smart ones first — a smart collection holds
519
+ // a `collection_id`. There is no constraint forcing that order any more
520
+ // (see the note on the column in `db/schema.ts`), but this function names
521
+ // every table it clears, and leaving one to an implicit cascade from `site`
522
+ // is how the next table gets forgotten.
523
+ await targetDb
524
+ .delete(smartCollections)
525
+ .where(eq(smartCollections.siteId, siteId));
512
526
  await targetDb.delete(collections).where(eq(collections.siteId, siteId));
513
527
  await targetDb.delete(apiTokens).where(eq(apiTokens.siteId, siteId));
514
528
  await targetDb.delete(settings).where(eq(settings.siteId, siteId));
@@ -782,11 +796,11 @@ export function createSiteAdminService(
782
796
  (theme) => theme.id === appConfig.fontThemeId,
783
797
  )
784
798
  : undefined;
799
+ // The static export is one site-wide stylesheet, so it uses the site
800
+ // language for the CJK stack. Per-page language overrides are a runtime
801
+ // concern that a flat Hugo export has no equivalent for.
785
802
  const fontOverrides = {
786
- ...getCjkFontCssVariables(
787
- appConfig.siteLanguage,
788
- appConfig.cjkSerifFont,
789
- ),
803
+ ...getCjkFontCssVariables(appConfig.siteLanguage),
790
804
  ...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
791
805
  };
792
806
  const themeCss = buildThemeStyle(
@@ -809,10 +823,13 @@ export function createSiteAdminService(
809
823
  siteUrl: appConfig.siteUrl,
810
824
  siteDescription: appConfig.siteDescription,
811
825
  siteLanguage: appConfig.siteLanguage,
826
+ multilingualEnabled: appConfig.multilingualEnabled,
827
+ additionalLanguages: appConfig.additionalLanguages,
812
828
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
813
829
  publicApiEnabled: appConfig.publicApiEnabled,
814
830
  rssFeedsEnabled: appConfig.rssFeedsEnabled,
815
831
  mainRssFeed: appConfig.mainRssFeed,
832
+ archiveDefaultLayout: appConfig.archiveDefaultLayout,
816
833
  siteFooter: appConfig.siteFooter,
817
834
  showHeaderAvatar: appConfig.showHeaderAvatar,
818
835
  siteAvatarUrl: appConfig.siteAvatarUrl,