@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
@@ -570,20 +570,53 @@ describe("PostService", () => {
570
570
  const descending = await postService.list({
571
571
  sortOrder: "rating_desc",
572
572
  });
573
- const ascending = await postService.list({
574
- sortOrder: "rating_asc",
575
- });
576
573
 
577
574
  expect(descending.map((post) => post.bodyText)).toEqual([
578
575
  "five stars",
579
576
  "three stars",
580
577
  "unrated newest",
581
578
  ]);
582
- expect(ascending.map((post) => post.bodyText)).toEqual([
583
- "three stars",
584
- "five stars",
585
- "unrated newest",
579
+ });
580
+
581
+ it("orders by time for an explicit newest, ratings and all", async () => {
582
+ // `newest` used to have no branch of its own in the ORDER BY and fell
583
+ // into the one written for the ascending rating order, so anything that
584
+ // named it — a smart collection page, most visibly — got its posts
585
+ // grouped by rating instead of dated. Rated rows here on purpose.
586
+ await postService.create({
587
+ format: "note",
588
+ bodyMarkdown: "oldest, best",
589
+ publishedAt: 1000,
590
+ rating: 5,
591
+ });
592
+ await postService.create({
593
+ format: "note",
594
+ bodyMarkdown: "middle, worst",
595
+ publishedAt: 2000,
596
+ rating: 1,
597
+ });
598
+ await postService.create({
599
+ format: "note",
600
+ bodyMarkdown: "newest, unrated",
601
+ publishedAt: 3000,
602
+ });
603
+
604
+ const ordered = await postService.list({ sortOrder: "newest" });
605
+ expect(ordered.map((post) => post.bodyText)).toEqual([
606
+ "newest, unrated",
607
+ "middle, worst",
608
+ "oldest, best",
586
609
  ]);
610
+
611
+ // The keyset cursor reads the same keys or pagination skips rows.
612
+ expect(
613
+ (
614
+ await postService.list({
615
+ sortOrder: "newest",
616
+ cursor: ordered[0]?.id,
617
+ })
618
+ ).map((post) => post.bodyText),
619
+ ).toEqual(["middle, worst", "oldest, best"]);
587
620
  });
588
621
 
589
622
  it("orders drafts by updatedAt descending", async () => {
@@ -1570,7 +1603,7 @@ describe("PostService", () => {
1570
1603
  expect(updated?.pinnedAt).toBeTypeOf("number");
1571
1604
  });
1572
1605
 
1573
- it("updates slug and its page navigation URL", async () => {
1606
+ it("updates slug and title on its page navigation entry", async () => {
1574
1607
  const post = await postService.create({
1575
1608
  format: "note",
1576
1609
  title: "Test page",
@@ -1585,11 +1618,15 @@ describe("PostService", () => {
1585
1618
 
1586
1619
  const updated = await postService.update(post.id, {
1587
1620
  slug: "new-slug",
1621
+ title: "Renamed page",
1588
1622
  });
1589
1623
 
1590
1624
  expect(updated?.slug).toBe("new-slug");
1625
+ // The URL is synced on write; the title is read through, so renaming the
1626
+ // page carries its navigation entry along with no sync step at all.
1591
1627
  expect(await navItemService.getById(navItem.id)).toMatchObject({
1592
- label: "Test page",
1628
+ label: "",
1629
+ targetTitle: "Renamed page",
1593
1630
  url: "/new-slug",
1594
1631
  });
1595
1632
  });
@@ -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 () => {