@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -486,6 +486,133 @@ describe("PostService - Timeline features", () => {
486
486
  });
487
487
  });
488
488
 
489
+ // A reply can be older than its root: a post moved into a Thread keeps its
490
+ // own creation time, and an export without `created` dates every post by
491
+ // publication. Thread order still opens on the root.
492
+ describe("a reply created before its root", () => {
493
+ async function createThreadWithOlderReplies() {
494
+ const root = await postService.create({
495
+ format: "note",
496
+ bodyMarkdown: "Root",
497
+ featured: true,
498
+ publishedAt: 3000,
499
+ });
500
+ const first = await postService.create({
501
+ format: "note",
502
+ bodyMarkdown: "First reply",
503
+ replyToId: root.id,
504
+ publishedAt: 1000,
505
+ });
506
+ const second = await postService.create({
507
+ format: "note",
508
+ bodyMarkdown: "Second reply",
509
+ replyToId: first.id,
510
+ publishedAt: 2000,
511
+ });
512
+ // Backdated after the fact, the way a restore lands them, so each test
513
+ // reads the shape without going through the reply guard.
514
+ for (const [id, createdAt] of [
515
+ [root.id, 3000],
516
+ [first.id, 1000],
517
+ [second.id, 2000],
518
+ ] as const) {
519
+ await db.update(posts).set({ createdAt }).where(eq(posts.id, id));
520
+ }
521
+ return { root, first, second };
522
+ }
523
+
524
+ it("opens getThread on the root", async () => {
525
+ const { root, first, second } = await createThreadWithOlderReplies();
526
+
527
+ const thread = await postService.getThread(root.id);
528
+
529
+ expect(thread.map((post) => post.id)).toEqual([
530
+ root.id,
531
+ first.id,
532
+ second.id,
533
+ ]);
534
+ });
535
+
536
+ it("opens getPublishedThreads on the root", async () => {
537
+ const { root, first, second } = await createThreadWithOlderReplies();
538
+
539
+ const threads = await postService.getPublishedThreads([root.id]);
540
+
541
+ expect(threads.get(root.id)?.map((post) => post.id)).toEqual([
542
+ root.id,
543
+ first.id,
544
+ second.id,
545
+ ]);
546
+ });
547
+
548
+ it("puts the root at position 0 of the Featured projection", async () => {
549
+ const { root, second } = await createThreadWithOlderReplies();
550
+
551
+ const result = await postService.getFeaturedThreadTimelineData([root.id]);
552
+
553
+ expect(
554
+ result
555
+ .get(root.id)
556
+ ?.posts.map(({ post, position }) => [post.id, position]),
557
+ ).toEqual([
558
+ [root.id, 0],
559
+ [second.id, 2],
560
+ ]);
561
+ expect(result.get(root.id)?.featuredPostIds).toEqual([root.id]);
562
+ });
563
+
564
+ it("ends the Thread on the newest reply, not the root", async () => {
565
+ const { root, second } = await createThreadWithOlderReplies();
566
+
567
+ const published = await postService.getThreadTailIds([root.id]);
568
+ const withDrafts = await postService.getThreadTailIds([root.id], {
569
+ includeDrafts: true,
570
+ });
571
+
572
+ expect(published.get(root.id)).toBe(second.id);
573
+ expect(withDrafts.get(root.id)).toBe(second.id);
574
+ });
575
+
576
+ it("refuses a reply to the root once a reply exists", async () => {
577
+ const { root, second } = await createThreadWithOlderReplies();
578
+
579
+ await expect(
580
+ postService.create({
581
+ format: "note",
582
+ bodyMarkdown: "Fork",
583
+ replyToId: root.id,
584
+ }),
585
+ ).rejects.toThrow("This post is no longer the end of the thread.");
586
+
587
+ const next = await postService.create({
588
+ format: "note",
589
+ bodyMarkdown: "Third reply",
590
+ replyToId: second.id,
591
+ });
592
+ expect(next.threadId).toBe(root.id);
593
+ });
594
+
595
+ it("folds the replies for the timeline in Thread order", async () => {
596
+ const { root, first, second } = await createThreadWithOlderReplies();
597
+
598
+ const context = (
599
+ await postService.getThreadTimelineContext([root.id])
600
+ ).get(root.id);
601
+ const previews = await postService.getThreadPreviews([root.id]);
602
+
603
+ expect(context?.leadingReplies.map((post) => post.id)).toEqual([
604
+ first.id,
605
+ second.id,
606
+ ]);
607
+ expect(context?.latestReply.id).toBe(second.id);
608
+ expect(context?.totalReplyCount).toBe(2);
609
+ expect(previews.get(root.id)?.map((post) => post.id)).toEqual([
610
+ first.id,
611
+ second.id,
612
+ ]);
613
+ });
614
+ });
615
+
489
616
  describe("timeline assembly", () => {
490
617
  it("fetches published non-reply posts for the timeline", async () => {
491
618
  const root = await postService.create({
@@ -13,6 +13,7 @@ import type { Database } from "../../db/index.js";
13
13
  import { createPathService } from "../path.js";
14
14
  import type { MediaService } from "../media.js";
15
15
  import { POST_BODY_HTML_VERSION } from "../../lib/post-body-html.js";
16
+ import { ValidationError } from "../../lib/errors.js";
16
17
  import type BetterSqlite3 from "better-sqlite3";
17
18
 
18
19
  function createMockStorage() {
@@ -90,6 +91,79 @@ describe("PostService", () => {
90
91
  expect(post.threadId).toBe(post.id);
91
92
  });
92
93
 
94
+ it("stores an empty list item with the paragraph the schema requires", async () => {
95
+ const post = await postService.create({
96
+ format: "note",
97
+ body: JSON.stringify({
98
+ type: "doc",
99
+ content: [
100
+ {
101
+ type: "orderedList",
102
+ content: [
103
+ { type: "listItem", content: [] },
104
+ {
105
+ type: "listItem",
106
+ content: [
107
+ {
108
+ type: "paragraph",
109
+ content: [{ type: "text", text: "Install" }],
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ ],
116
+ }),
117
+ });
118
+
119
+ const stored = JSON.parse(post.body ?? "");
120
+ expect(stored.content[0].content[0]).toEqual({
121
+ type: "listItem",
122
+ content: [{ type: "paragraph" }],
123
+ });
124
+ });
125
+
126
+ it("restores the creation and edit times an import passes", async () => {
127
+ const published = await postService.create({
128
+ format: "note",
129
+ bodyMarkdown: "Moved from another site",
130
+ publishedAt: 1700000500,
131
+ createdAt: 1700000000,
132
+ updatedAt: 1700009000,
133
+ });
134
+ expect(published).toMatchObject({
135
+ createdAt: 1700000000,
136
+ updatedAt: 1700009000,
137
+ lastActivityAt: 1700000500,
138
+ });
139
+
140
+ const draft = await postService.create({
141
+ format: "note",
142
+ status: "draft",
143
+ bodyMarkdown: "Unfinished",
144
+ createdAt: 1700000000,
145
+ updatedAt: 1700004000,
146
+ });
147
+ expect(draft).toMatchObject({
148
+ createdAt: 1700000000,
149
+ updatedAt: 1700004000,
150
+ lastActivityAt: 1700004000,
151
+ });
152
+
153
+ const backdated = await postService.create({
154
+ format: "note",
155
+ bodyMarkdown: "Only a creation time",
156
+ createdAt: 1700000000,
157
+ });
158
+ expect(backdated.updatedAt).toBe(1700000000);
159
+ });
160
+
161
+ it("rejects a body that isn't TipTap JSON", async () => {
162
+ await expect(
163
+ postService.create({ format: "note", body: "not json" }),
164
+ ).rejects.toThrow(ValidationError);
165
+ });
166
+
93
167
  it("creates a link post with commentary", async () => {
94
168
  const body = JSON.stringify({
95
169
  type: "doc",
@@ -570,20 +644,53 @@ describe("PostService", () => {
570
644
  const descending = await postService.list({
571
645
  sortOrder: "rating_desc",
572
646
  });
573
- const ascending = await postService.list({
574
- sortOrder: "rating_asc",
575
- });
576
647
 
577
648
  expect(descending.map((post) => post.bodyText)).toEqual([
578
649
  "five stars",
579
650
  "three stars",
580
651
  "unrated newest",
581
652
  ]);
582
- expect(ascending.map((post) => post.bodyText)).toEqual([
583
- "three stars",
584
- "five stars",
585
- "unrated newest",
653
+ });
654
+
655
+ it("orders by time for an explicit newest, ratings and all", async () => {
656
+ // `newest` used to have no branch of its own in the ORDER BY and fell
657
+ // into the one written for the ascending rating order, so anything that
658
+ // named it — a smart collection page, most visibly — got its posts
659
+ // grouped by rating instead of dated. Rated rows here on purpose.
660
+ await postService.create({
661
+ format: "note",
662
+ bodyMarkdown: "oldest, best",
663
+ publishedAt: 1000,
664
+ rating: 5,
665
+ });
666
+ await postService.create({
667
+ format: "note",
668
+ bodyMarkdown: "middle, worst",
669
+ publishedAt: 2000,
670
+ rating: 1,
671
+ });
672
+ await postService.create({
673
+ format: "note",
674
+ bodyMarkdown: "newest, unrated",
675
+ publishedAt: 3000,
676
+ });
677
+
678
+ const ordered = await postService.list({ sortOrder: "newest" });
679
+ expect(ordered.map((post) => post.bodyText)).toEqual([
680
+ "newest, unrated",
681
+ "middle, worst",
682
+ "oldest, best",
586
683
  ]);
684
+
685
+ // The keyset cursor reads the same keys or pagination skips rows.
686
+ expect(
687
+ (
688
+ await postService.list({
689
+ sortOrder: "newest",
690
+ cursor: ordered[0]?.id,
691
+ })
692
+ ).map((post) => post.bodyText),
693
+ ).toEqual(["middle, worst", "oldest, best"]);
587
694
  });
588
695
 
589
696
  it("orders drafts by updatedAt descending", async () => {
@@ -1570,7 +1677,7 @@ describe("PostService", () => {
1570
1677
  expect(updated?.pinnedAt).toBeTypeOf("number");
1571
1678
  });
1572
1679
 
1573
- it("updates slug and its page navigation URL", async () => {
1680
+ it("updates slug and title on its page navigation entry", async () => {
1574
1681
  const post = await postService.create({
1575
1682
  format: "note",
1576
1683
  title: "Test page",
@@ -1585,11 +1692,15 @@ describe("PostService", () => {
1585
1692
 
1586
1693
  const updated = await postService.update(post.id, {
1587
1694
  slug: "new-slug",
1695
+ title: "Renamed page",
1588
1696
  });
1589
1697
 
1590
1698
  expect(updated?.slug).toBe("new-slug");
1699
+ // The URL is synced on write; the title is read through, so renaming the
1700
+ // page carries its navigation entry along with no sync step at all.
1591
1701
  expect(await navItemService.getById(navItem.id)).toMatchObject({
1592
- label: "Test page",
1702
+ label: "",
1703
+ targetTitle: "Renamed page",
1593
1704
  url: "/new-slug",
1594
1705
  });
1595
1706
  });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import {
3
3
  createTestDatabase,
4
4
  DEFAULT_TEST_SITE_ID,
@@ -170,7 +170,6 @@ describe("SettingsService", () => {
170
170
  siteDescription: "",
171
171
  siteFooter: "",
172
172
  siteLanguage: "en",
173
- cjkSerifFont: "off",
174
173
  showJantBrandingOnHome: false,
175
174
  mainRssFeed: "featured",
176
175
  timeZone: "UTC",
@@ -361,6 +360,57 @@ describe("SettingsService", () => {
361
360
  const result = await settingsService.isOnboardingComplete();
362
361
  expect(result).toBe(true);
363
362
  });
363
+
364
+ it("reports the three first-run states", async () => {
365
+ expect(await settingsService.getOnboardingStatus()).toBe("pending");
366
+
367
+ await settingsService.markSiteProvisioned();
368
+ expect(await settingsService.getOnboardingStatus()).toBe("provisioned");
369
+ // A provisioned site is real, but setup is not finished with it.
370
+ expect(await settingsService.isOnboardingComplete()).toBe(false);
371
+
372
+ await settingsService.completeOnboarding();
373
+ expect(await settingsService.getOnboardingStatus()).toBe("completed");
374
+ });
375
+
376
+ it("finishes setup when the author confirms the language they write in", async () => {
377
+ await settingsService.markSiteProvisioned();
378
+ // The control plane's guess, taken from the locale the owner browsed it
379
+ // in, is what the site starts with.
380
+ await settingsService.set("SITE_LANGUAGE", "zh-Hans");
381
+
382
+ await settingsService.confirmFirstRunLanguage(
383
+ { siteLanguage: "en", browserLanguage: "zh-Hans" },
384
+ { oldLanguage: "zh-Hans" },
385
+ );
386
+
387
+ expect(await settingsService.get("SITE_LANGUAGE")).toBe("en");
388
+ // English site, Chinese dashboard — the split the question exists for.
389
+ expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBe("zh-Hans");
390
+ expect(await settingsService.isOnboardingComplete()).toBe(true);
391
+ });
392
+
393
+ it("leaves the dashboard following when the browser sent none", async () => {
394
+ await settingsService.confirmFirstRunLanguage(
395
+ { siteLanguage: "zh-TW", browserLanguage: "" },
396
+ { oldLanguage: "en" },
397
+ );
398
+
399
+ expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-TW");
400
+ expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBeNull();
401
+ });
402
+
403
+ it("leaves the dashboard following the language the author confirmed", async () => {
404
+ // An English browser says nothing about someone who just answered
405
+ // "Simplified Chinese" — the dashboard follows that answer.
406
+ await settingsService.confirmFirstRunLanguage(
407
+ { siteLanguage: "zh-Hans", browserLanguage: "en-US" },
408
+ { oldLanguage: "en" },
409
+ );
410
+
411
+ expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-Hans");
412
+ expect(await settingsService.get("DASHBOARD_LANGUAGE")).toBeNull();
413
+ });
364
414
  });
365
415
 
366
416
  describe("grouped settings updates", () => {
@@ -390,7 +440,6 @@ describe("SettingsService", () => {
390
440
  const result = await settingsService.updateLocaleSettings(
391
441
  {
392
442
  siteLanguage: "zh-Hans",
393
- cjkSerifFont: "zh-Hans",
394
443
  timeZone: "America/New_York",
395
444
  },
396
445
  {
@@ -400,29 +449,32 @@ describe("SettingsService", () => {
400
449
 
401
450
  expect(result.languageChanged).toBe(true);
402
451
  expect(await settingsService.get("SITE_LANGUAGE")).toBe("zh-Hans");
403
- expect(await settingsService.get("CJK_SERIF_FONT")).toBe("zh-Hans");
404
452
  expect(await settingsService.get("TIME_ZONE")).toBe("America/New_York");
405
453
  });
406
454
 
407
- it("removes CJK_SERIF_FONT when set to off", async () => {
408
- await settingsService.set("CJK_SERIF_FONT", "zh-Hans");
455
+ // The Language and General pages each own part of this group, so an absent
456
+ // field has to mean "leave it alone" rather than "clear it".
457
+ it("leaves out-of-scope fields untouched", async () => {
458
+ await settingsService.set("SITE_LANGUAGE", "ja");
459
+ await settingsService.set("TIME_ZONE", "Asia/Tokyo");
460
+
409
461
  await settingsService.updateLocaleSettings(
410
- {
411
- siteLanguage: "en",
412
- cjkSerifFont: "off",
413
- timeZone: "UTC",
414
- },
415
- { oldLanguage: "en" },
462
+ { timeZone: "America/New_York" },
463
+ { oldLanguage: "ja" },
416
464
  );
465
+ expect(await settingsService.get("SITE_LANGUAGE")).toBe("ja");
417
466
 
418
- expect(await settingsService.get("CJK_SERIF_FONT")).toBeNull();
467
+ await settingsService.updateLocaleSettings(
468
+ { siteLanguage: "en" },
469
+ { oldLanguage: "ja" },
470
+ );
471
+ expect(await settingsService.get("TIME_ZONE")).toBe("America/New_York");
419
472
  });
420
473
 
421
474
  it("stores the base locale when language input is blank", async () => {
422
475
  const result = await settingsService.updateLocaleSettings(
423
476
  {
424
477
  siteLanguage: "",
425
- cjkSerifFont: "off",
426
478
  timeZone: "UTC",
427
479
  },
428
480
  {
@@ -439,7 +491,6 @@ describe("SettingsService", () => {
439
491
  {
440
492
  siteLanguage: "fr",
441
493
  dashboardLanguage: "zh-Hant",
442
- cjkSerifFont: "off",
443
494
  timeZone: "UTC",
444
495
  },
445
496
  { oldLanguage: "fr", oldDashboardLanguage: "" },
@@ -456,7 +507,6 @@ describe("SettingsService", () => {
456
507
  {
457
508
  siteLanguage: "en",
458
509
  dashboardLanguage: "",
459
- cjkSerifFont: "off",
460
510
  timeZone: "UTC",
461
511
  },
462
512
  { oldLanguage: "en", oldDashboardLanguage: "zh-Hans" },
@@ -481,7 +531,6 @@ describe("SettingsService", () => {
481
531
  {
482
532
  siteLanguage: "en",
483
533
  dashboardLanguage: "fr",
484
- cjkSerifFont: "off",
485
534
  timeZone: "UTC",
486
535
  },
487
536
  { oldLanguage: "en" },
@@ -509,4 +558,166 @@ describe("SettingsService", () => {
509
558
  ).toBeNull();
510
559
  });
511
560
  });
561
+
562
+ /**
563
+ * The ping exists so a self-hosted site can be found at all, and it says
564
+ * "read me now" rather than "list me". What it fires on is therefore the
565
+ * interesting part: the two moments the answer changes, and neither of the
566
+ * moments it does not.
567
+ */
568
+ describe("updateDiscoverSetting", () => {
569
+ it("stores the choice explicitly, including off", async () => {
570
+ await settingsService.updateDiscoverSetting("off", { demoMode: false });
571
+ expect(await settingsService.get("DISCOVER")).toBe("off");
572
+
573
+ await settingsService.updateDiscoverSetting("featured", {
574
+ demoMode: false,
575
+ });
576
+ expect(await settingsService.get("DISCOVER")).toBe("featured");
577
+ });
578
+
579
+ // A site that has never used the control has never told anyone it exists,
580
+ // so confirming the default is exactly as much of an opt-in as ticking a
581
+ // box that was off.
582
+ it("announces when an untouched site opts in", async () => {
583
+ const result = await settingsService.updateDiscoverSetting("latest", {
584
+ demoMode: false,
585
+ });
586
+ expect(result.shouldAnnounce).toBe(true);
587
+ });
588
+
589
+ it("announces when a site that had opted out comes back", async () => {
590
+ await settingsService.updateDiscoverSetting("off", { demoMode: false });
591
+ const result = await settingsService.updateDiscoverSetting("latest", {
592
+ demoMode: false,
593
+ });
594
+ expect(result.shouldAnnounce).toBe(true);
595
+ });
596
+
597
+ // The directory already knows about a listed site; which of its posts it
598
+ // draws from is not news.
599
+ it("says nothing when an enrolled site changes which posts it offers", async () => {
600
+ await settingsService.updateDiscoverSetting("latest", {
601
+ demoMode: false,
602
+ });
603
+ const result = await settingsService.updateDiscoverSetting("featured", {
604
+ demoMode: false,
605
+ });
606
+ expect(result.shouldAnnounce).toBe(false);
607
+ });
608
+
609
+ // The half that used to be missing. A directory that is not told keeps the
610
+ // blog until its next scheduled read of the feed, and the owner watches
611
+ // something they just removed sit there for another hour.
612
+ it("announces when a listed site opts out", async () => {
613
+ await settingsService.updateDiscoverSetting("latest", {
614
+ demoMode: false,
615
+ });
616
+ const result = await settingsService.updateDiscoverSetting("off", {
617
+ demoMode: false,
618
+ });
619
+ expect(result.shouldAnnounce).toBe(true);
620
+ });
621
+
622
+ // Nothing changed, so there is nothing to say: this site was not in any
623
+ // directory a moment ago either.
624
+ it("says nothing when a site that was never listed sets off", async () => {
625
+ const result = await settingsService.updateDiscoverSetting("off", {
626
+ demoMode: false,
627
+ });
628
+ expect(result.shouldAnnounce).toBe(false);
629
+ });
630
+
631
+ it("never announces a demo site", async () => {
632
+ const result = await settingsService.updateDiscoverSetting("latest", {
633
+ demoMode: true,
634
+ });
635
+ expect(result.shouldAnnounce).toBe(false);
636
+ });
637
+ });
638
+
639
+ /**
640
+ * The stored outcome is the owner's record of the announcement, and it does
641
+ * not always reach them — a hosted site's settings page hides the status
642
+ * block entirely. The log is how a deployment answers "did that get through"
643
+ * when the dashboard cannot.
644
+ */
645
+ describe("announceToDiscover", () => {
646
+ afterEach(() => {
647
+ vi.restoreAllMocks();
648
+ });
649
+
650
+ function stubDirectory(response: Response | Error) {
651
+ return vi
652
+ .spyOn(globalThis, "fetch")
653
+ .mockImplementation(() =>
654
+ response instanceof Error
655
+ ? Promise.reject(response)
656
+ : Promise.resolve(response.clone()),
657
+ );
658
+ }
659
+
660
+ it("records and logs an announcement that got through", async () => {
661
+ stubDirectory(new Response(null, { status: 202 }));
662
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
663
+
664
+ const outcome = await settingsService.announceToDiscover({
665
+ endpoint: "https://cloud.example/api/discover/ping",
666
+ feedUrl: "https://blog.example/latest/feed",
667
+ });
668
+
669
+ expect(outcome.ok).toBe(true);
670
+ expect(
671
+ JSON.parse(
672
+ (await settingsService.get("DISCOVER_ANNOUNCE_STATE")) ?? "",
673
+ ),
674
+ ).toMatchObject({
675
+ ok: true,
676
+ feedUrl: "https://blog.example/latest/feed",
677
+ });
678
+ expect(log).toHaveBeenCalledWith(
679
+ expect.stringContaining("https://cloud.example/api/discover/ping"),
680
+ );
681
+ });
682
+
683
+ // The failure this exists for: a directory that answers, but not about
684
+ // this site. Silently stored, it reads from the dashboard like a success.
685
+ it("logs the reason an announcement failed", async () => {
686
+ stubDirectory(new Response(null, { status: 404 }));
687
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
688
+
689
+ const outcome = await settingsService.announceToDiscover({
690
+ endpoint: "https://jant.me/api/discover/ping",
691
+ feedUrl: "https://blog.example/latest/feed",
692
+ });
693
+
694
+ expect(outcome).toMatchObject({ ok: false, status: 404 });
695
+ expect(error).toHaveBeenCalledWith(
696
+ expect.stringContaining("The directory answered 404."),
697
+ );
698
+ });
699
+
700
+ // The caller orphans this promise behind `waitUntil`, so a rejection has
701
+ // nobody to reject to — on a Node runtime an unhandled one ends the
702
+ // process. A settings row that could not be written is worth a log, not
703
+ // the site.
704
+ it("resolves even when the outcome cannot be recorded", async () => {
705
+ stubDirectory(new Response(null, { status: 202 }));
706
+ vi.spyOn(console, "log").mockImplementation(() => {});
707
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
708
+ vi.spyOn(settingsService, "set").mockRejectedValue(
709
+ new Error("D1_ERROR: database is locked"),
710
+ );
711
+
712
+ const outcome = await settingsService.announceToDiscover({
713
+ endpoint: "https://jant.me/api/discover/ping",
714
+ feedUrl: "https://blog.example/latest/feed",
715
+ });
716
+
717
+ expect(outcome.ok).toBe(true);
718
+ expect(error).toHaveBeenCalledWith(
719
+ expect.stringContaining("database is locked"),
720
+ );
721
+ });
722
+ });
512
723
  });
@@ -53,9 +53,9 @@ describe("SiteAdminService", () => {
53
53
 
54
54
  expect(navigationRows).toEqual([
55
55
  { systemKey: "featured", placement: "header" },
56
- { systemKey: "collections", placement: "header" },
57
56
  { systemKey: "archive", placement: "header" },
58
- { systemKey: "rss", placement: "more" },
57
+ { systemKey: "collections", placement: "more" },
58
+ { systemKey: "subscribe", placement: "more" },
59
59
  { systemKey: "settings", placement: "more" },
60
60
  ]);
61
61
  });
@@ -182,12 +182,12 @@ describe("SiteAdminService", () => {
182
182
  expect(recovered.site.id).toBe(first.site.id);
183
183
  expect(navigationRows.map((row) => row.systemKey)).toEqual([
184
184
  "featured",
185
- "collections",
186
185
  "archive",
187
- "rss",
186
+ "collections",
187
+ "subscribe",
188
188
  "settings",
189
189
  ]);
190
- expect(onboarding.value).toBe("completed");
190
+ expect(onboarding.value).toBe("provisioned");
191
191
  });
192
192
 
193
193
  it("rejects reuse of an idempotency key with different key or primary host", async () => {