@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
@@ -14,6 +14,7 @@ import { SigninSchema } from "../../lib/schemas.js";
14
14
  import { buildPageTitle } from "../../lib/page-title.js";
15
15
  import { getI18n } from "../../i18n/index.js";
16
16
  import { getHostedControlPlaneSigninUrl } from "../../lib/hosted-signin.js";
17
+ import { isCurrentSiteMember } from "../../middleware/auth.js";
17
18
  import { isSafeInternalRedirect, toPublicPath } from "../../lib/url.js";
18
19
 
19
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -128,7 +129,10 @@ signinRoutes.get("/signin", async (c) => {
128
129
  ? rawRedirect
129
130
  : undefined;
130
131
 
131
- if (c.var.isAuthenticated) {
132
+ // Membership, not merely a session: `requireAuth` sends non-members here, so
133
+ // reading `isAuthenticated` instead would send them straight back and the two
134
+ // would volley until the browser refused to follow another hop.
135
+ if (await isCurrentSiteMember(c)) {
132
136
  return c.redirect(
133
137
  toPublicPath(redirect ?? "/", c.var.appConfig.sitePathPrefix),
134
138
  );
@@ -144,14 +148,12 @@ signinRoutes.get("/signin", async (c) => {
144
148
  }
145
149
 
146
150
  const i18n = getI18n(c);
147
- const isSetup = c.req.query("setup") !== undefined;
151
+ // No `?setup` counterpart: setup signs its new owner in on the spot and
152
+ // hands them straight to the next screen, so nobody arrives here from it.
148
153
  const isReset = c.req.query("reset") !== undefined;
149
- let toast: { message: string } | undefined;
150
- if (isSetup) {
151
- toast = { message: "Account created. Sign in to get started." };
152
- } else if (isReset) {
153
- toast = { message: "Password reset. Sign in with your new password." };
154
- }
154
+ const toast = isReset
155
+ ? { message: "Password reset. Sign in with your new password." }
156
+ : undefined;
155
157
 
156
158
  return c.html(
157
159
  <BaseLayout
@@ -89,6 +89,8 @@ composeRoutes.post("/", async (c) => {
89
89
  collectionIds: data.collectionIds,
90
90
  replyToId: data.replyToId,
91
91
  quietReply: data.quietReply,
92
+ language: data.language,
93
+ translationOfId: data.translationOfId,
92
94
  publishedAt: data.publishedAt,
93
95
  },
94
96
  data.attachments,
@@ -231,6 +233,9 @@ composeRoutes.post("/thread", async (c) => {
231
233
  collectionIds: index === 0 ? data.collectionIds : undefined,
232
234
  replyToId: index === 0 ? data.replyToId : undefined,
233
235
  quietReply: data.quietReply,
236
+ // Thread-level, so only the root states them; replies inherit.
237
+ language: index === 0 ? data.language : undefined,
238
+ translationOfId: index === 0 ? data.translationOfId : undefined,
234
239
  // Replies may carry their own date; when they don't, the service
235
240
  // inherits the root's rather than stamping "now".
236
241
  publishedAt: data.publishedAt,
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+
4
+ /**
5
+ * The archive custom URL is readable forever and creatable never.
6
+ *
7
+ * Hand-typing `format=note&title=none` into a settings text field is the
8
+ * problem smart collections replace. Removing it from the form is not the same
9
+ * as removing it — an endpoint that still accepts it has not stopped offering
10
+ * it — so the refusal lives in the schema and again in the service.
11
+ */
12
+ describe("archive custom URLs", () => {
13
+ it("refuses to create one", async () => {
14
+ const { services } = createTestApp({ authenticated: true });
15
+
16
+ await expect(
17
+ services.customUrls.create({
18
+ path: "/notes-only",
19
+ targetType: "archive",
20
+ archiveQuery: "format=note",
21
+ }),
22
+ ).rejects.toThrow(/smart collection/i);
23
+ });
24
+
25
+ it("still reads, lists, and deletes one that already exists", async () => {
26
+ const { services } = createTestApp({ authenticated: true });
27
+
28
+ // How a real legacy row exists: written before the create path was closed.
29
+ const record = await services.paths.create({
30
+ path: "/notes-only",
31
+ kind: "archive",
32
+ archiveQuery: "format=note&title=none",
33
+ });
34
+
35
+ const listed = await services.customUrls.list();
36
+ expect(listed.map((entry) => entry.path)).toContain("notes-only");
37
+ expect(
38
+ listed.find((entry) => entry.path === "notes-only")?.targetType,
39
+ ).toBe("archive");
40
+
41
+ expect(await services.customUrls.delete(record.id)).toBe(true);
42
+ expect(await services.paths.getByPath("/notes-only")).toBeNull();
43
+ });
44
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What the Custom URLs settings page shows for a legacy archive path.
3
+ *
4
+ * The row is the whole affordance: it prints the query the path holds, and its
5
+ * menu deletes. It deliberately offers no "turn into a smart collection" —
6
+ * that row *is* the registration of its own address, so the collection it would
7
+ * become could never be saved at the URL the offer was about.
8
+ */
9
+
10
+ import { describe, expect, it } from "vitest";
11
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
12
+ import { customUrlsRoutes } from "../custom-urls.js";
13
+
14
+ function createCustomUrlsTestApp() {
15
+ const testApp = createTestApp({ authenticated: true });
16
+ const { app } = testApp;
17
+
18
+ app.use("*", async (c, next) => {
19
+ c.set("publicPath", c.req.path);
20
+ c.set("publicRequestUrl", c.req.url);
21
+ await next();
22
+ });
23
+
24
+ app.route("/settings/custom-urls", customUrlsRoutes);
25
+
26
+ return testApp;
27
+ }
28
+
29
+ describe("custom URLs settings page", () => {
30
+ it("shows a legacy archive path and offers only deletion", async () => {
31
+ const { app, services } = createCustomUrlsTestApp();
32
+
33
+ // How a real legacy row exists: written before the create path was closed.
34
+ await services.paths.create({
35
+ path: "/notes-only",
36
+ kind: "archive",
37
+ archiveQuery: "format=note&title=none",
38
+ });
39
+
40
+ const res = await app.request("/settings/custom-urls");
41
+ expect(res.status).toBe(200);
42
+
43
+ const html = await res.text();
44
+ expect(html).toContain("/notes-only");
45
+ expect(html).toContain("/archive?format=note&amp;title=none");
46
+ expect(html).toContain('data-custom-url-action="delete"');
47
+ expect(html).not.toContain('data-custom-url-action="upgrade"');
48
+ });
49
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tests for POST /settings/account/delete-account.
3
+ *
4
+ * The route is a POST returning a Datastar redirect, so it can be exercised
5
+ * end to end without the SSR setup the settings pages need.
6
+ */
7
+
8
+ import { describe, expect, it, vi } from "vitest";
9
+ import { Hono } from "hono";
10
+ import { createI18n } from "../../../i18n/index.js";
11
+ import { settingsRoutes } from "../settings.js";
12
+ import type { Bindings } from "../../../types.js";
13
+ import type { AppVariables } from "../../../types/app-context.js";
14
+
15
+ type Env = { Bindings: Bindings; Variables: AppVariables };
16
+
17
+ const CLEARED_SESSION_COOKIE =
18
+ "better-auth.session_token=; Path=/; Max-Age=0; HttpOnly";
19
+
20
+ function createDeleteAccountTestApp() {
21
+ const calls: string[] = [];
22
+
23
+ const signOut = vi.fn(async () => {
24
+ calls.push("signOut");
25
+ return new Response(null, {
26
+ headers: { "Set-Cookie": CLEARED_SESSION_COOKIE },
27
+ });
28
+ });
29
+ const deleteAllData = vi.fn(async () => {
30
+ calls.push("deleteAllData");
31
+ });
32
+
33
+ const app = new Hono<Env>();
34
+ app.use("*", async (c, next) => {
35
+ c.env = {} as Bindings;
36
+ c.set("auth", { api: { signOut } } as unknown as AppVariables["auth"]);
37
+ c.set("services", {
38
+ auth: {
39
+ validateDeleteCsrfToken: async (_token: string, sessionId: string) =>
40
+ sessionId === "ses_test",
41
+ deleteAllData,
42
+ },
43
+ } as unknown as AppVariables["services"]);
44
+ c.set("storage", null);
45
+ c.set("session", {
46
+ session: { id: "ses_test" },
47
+ user: { id: "usr_test" },
48
+ } as AppVariables["session"]);
49
+ c.set("isAuthenticated", true);
50
+ c.set("currentSite", {
51
+ createdAt: 0,
52
+ id: "sit_test",
53
+ key: "default",
54
+ status: "active",
55
+ updatedAt: 0,
56
+ });
57
+ c.set("appConfig", {
58
+ siteName: "Jant",
59
+ sitePathPrefix: "",
60
+ } as AppVariables["appConfig"]);
61
+ c.set("lang", "en");
62
+ c.set("i18n", createI18n("en"));
63
+ await next();
64
+ });
65
+ app.route("/settings", settingsRoutes);
66
+
67
+ return { app, calls, signOut, deleteAllData };
68
+ }
69
+
70
+ function requestDelete(app: Hono<Env>) {
71
+ return app.request("/settings/account/delete-account", {
72
+ method: "POST",
73
+ headers: { "x-csrf-token": "valid-token" },
74
+ });
75
+ }
76
+
77
+ describe("POST /settings/account/delete-account", () => {
78
+ it("clears the session cookie so the browser keeps no ticket to the deleted site", async () => {
79
+ const { app } = createDeleteAccountTestApp();
80
+
81
+ const res = await requestDelete(app);
82
+
83
+ expect(res.status).toBe(200);
84
+ expect(res.headers.getSetCookie()).toContain(CLEARED_SESSION_COOKIE);
85
+ await expect(res.text()).resolves.toContain("/setup");
86
+ });
87
+
88
+ it("signs out before the wipe, while the session row still exists", async () => {
89
+ const { app, calls } = createDeleteAccountTestApp();
90
+
91
+ await requestDelete(app);
92
+
93
+ expect(calls).toEqual(["signOut", "deleteAllData"]);
94
+ });
95
+
96
+ it("still wipes the site when signing out fails", async () => {
97
+ const { app, calls, signOut, deleteAllData } = createDeleteAccountTestApp();
98
+ signOut.mockRejectedValueOnce(new Error("no session"));
99
+
100
+ const res = await requestDelete(app);
101
+
102
+ expect(res.status).toBe(200);
103
+ expect(calls).toEqual(["deleteAllData"]);
104
+ expect(deleteAllData).toHaveBeenCalledOnce();
105
+ });
106
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Tests for the Jant Discover settings routes.
3
+ *
4
+ * Both are POSTs with no SSR, so `settingsRoutes` can be mounted and driven
5
+ * directly. What they are really guarding is the contract with
6
+ * `client/settings-bridge.ts`: it sends `Accept: application/json` and parses
7
+ * the body with `readJsonObject`, which throws on anything else — so a route
8
+ * that answers with a Datastar toast reads to the owner as a failed save no
9
+ * matter what the server did.
10
+ */
11
+
12
+ import { describe, expect, it, vi } from "vitest";
13
+ import { Hono } from "hono";
14
+ import { createI18n } from "../../../i18n/index.js";
15
+ import { settingsRoutes } from "../settings.js";
16
+ import type { Bindings } from "../../../types.js";
17
+ import type { AppVariables } from "../../../types/app-context.js";
18
+
19
+ type Env = { Bindings: Bindings; Variables: AppVariables };
20
+
21
+ function createDiscoverTestApp(
22
+ overrides: {
23
+ env?: Partial<Bindings>;
24
+ storedDiscover?: string;
25
+ demoMode?: boolean;
26
+ rssFeedsEnabled?: boolean;
27
+ } = {},
28
+ ) {
29
+ const announceToDiscover = vi.fn(async () => ({
30
+ at: 0,
31
+ ok: true,
32
+ feedUrl: "https://blog.example/latest/feed",
33
+ }));
34
+ const updateDiscoverSetting = vi.fn(async () => ({ shouldAnnounce: true }));
35
+
36
+ const app = new Hono<Env>();
37
+ app.use("*", async (c, next) => {
38
+ c.env = (overrides.env ?? {}) as Bindings;
39
+ c.set("services", {
40
+ settings: { announceToDiscover, updateDiscoverSetting },
41
+ } as unknown as AppVariables["services"]);
42
+ c.set("allSettings", {
43
+ ...(overrides.storedDiscover === undefined
44
+ ? {}
45
+ : { DISCOVER: overrides.storedDiscover }),
46
+ });
47
+ c.set("isAuthenticated", true);
48
+ c.set("appConfig", {
49
+ demoMode: overrides.demoMode ?? false,
50
+ noindex: false,
51
+ rssFeedsEnabled: overrides.rssFeedsEnabled ?? true,
52
+ siteUrl: "https://blog.example",
53
+ sitePathPrefix: "",
54
+ } as AppVariables["appConfig"]);
55
+ c.set("lang", "en");
56
+ c.set("i18n", createI18n("en"));
57
+ await next();
58
+ });
59
+ app.route("/settings", settingsRoutes);
60
+
61
+ return { app, announceToDiscover, updateDiscoverSetting };
62
+ }
63
+
64
+ function postJson(app: Hono<Env>, path: string, body: unknown = {}) {
65
+ return app.request(path, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ // Exactly what `settings-bridge.ts` sends.
70
+ Accept: "application/json",
71
+ },
72
+ body: JSON.stringify(body),
73
+ });
74
+ }
75
+
76
+ describe("POST /settings/general/discover/announce", () => {
77
+ it("answers JSON, so the bridge does not read a success as a failure", async () => {
78
+ const { app, announceToDiscover } = createDiscoverTestApp({
79
+ storedDiscover: "latest",
80
+ });
81
+
82
+ const res = await postJson(app, "/settings/general/discover/announce");
83
+
84
+ expect(res.status).toBe(200);
85
+ // The assertion that matters: a parseable body. A Datastar toast is
86
+ // `text/html`, and `readJsonObject` throws on it.
87
+ await expect(res.json()).resolves.toMatchObject({
88
+ status: "ok",
89
+ toast: expect.stringContaining("Announcing your site"),
90
+ });
91
+ expect(announceToDiscover).toHaveBeenCalledOnce();
92
+ });
93
+
94
+ it("says so in JSON when there is nothing to announce to", async () => {
95
+ const { app, announceToDiscover } = createDiscoverTestApp({
96
+ storedDiscover: "latest",
97
+ env: { DISCOVER_PING_URL: "" },
98
+ });
99
+
100
+ const res = await postJson(app, "/settings/general/discover/announce");
101
+
102
+ await expect(res.json()).resolves.toMatchObject({
103
+ status: "ok",
104
+ toast: expect.stringContaining("no directory"),
105
+ });
106
+ expect(announceToDiscover).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it("never announces a demo site", async () => {
110
+ const { app, announceToDiscover } = createDiscoverTestApp({
111
+ storedDiscover: "latest",
112
+ demoMode: true,
113
+ });
114
+
115
+ await postJson(app, "/settings/general/discover/announce");
116
+
117
+ expect(announceToDiscover).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it("does not announce a feed address that would answer 404", async () => {
121
+ // Feeds are off, so `/latest/feed` does not exist to be polled.
122
+ const { app, announceToDiscover } = createDiscoverTestApp({
123
+ storedDiscover: "latest",
124
+ rssFeedsEnabled: false,
125
+ });
126
+
127
+ await postJson(app, "/settings/general/discover/announce");
128
+
129
+ expect(announceToDiscover).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it("still answers a Datastar toast when JSON was not asked for", async () => {
133
+ const { app } = createDiscoverTestApp({ storedDiscover: "latest" });
134
+
135
+ const res = await app.request("/settings/general/discover/announce", {
136
+ method: "POST",
137
+ });
138
+
139
+ expect(res.headers.get("Content-Type")).toContain("text/html");
140
+ });
141
+ });
142
+
143
+ describe("POST /settings/general/discover", () => {
144
+ it("announces the feed the newly saved mode names, not the stored one", async () => {
145
+ // `allSettings` is the snapshot taken before the request ran, so it still
146
+ // holds the previous answer on the save that triggers the announcement.
147
+ // `featured` is what older releases stored, and it names a different feed
148
+ // from the `latest` the control now writes.
149
+ const { app, announceToDiscover } = createDiscoverTestApp({
150
+ storedDiscover: "featured",
151
+ });
152
+
153
+ await postJson(app, "/settings/general/discover", {
154
+ discover: "latest",
155
+ });
156
+
157
+ expect(announceToDiscover).toHaveBeenCalledWith(
158
+ expect.objectContaining({
159
+ feedUrl: "https://blog.example/latest/feed",
160
+ }),
161
+ );
162
+ });
163
+
164
+ /**
165
+ * A ping means "read me now", and the moment a blog leaves is the moment
166
+ * that matters most: without it the directory keeps the blog until its next
167
+ * scheduled read, and the owner watches something they just removed sit
168
+ * there for another hour.
169
+ */
170
+ it("pings the always-served feed when the owner switches Discover off", async () => {
171
+ const { app, announceToDiscover } = createDiscoverTestApp({
172
+ storedDiscover: "featured",
173
+ });
174
+
175
+ await postJson(app, "/settings/general/discover", { discover: "off" });
176
+
177
+ // `/featured/feed` is not it: the mode that named it is gone, and the
178
+ // declaration a directory has to read sits in every feed.
179
+ expect(announceToDiscover).toHaveBeenCalledWith(
180
+ expect.objectContaining({
181
+ feedUrl: "https://blog.example/latest/feed",
182
+ }),
183
+ );
184
+ });
185
+
186
+ // The other ways a site resolves to `none` are not an owner saying stop, and
187
+ // the address they would name answers 404.
188
+ it("sends no stop ping from a site whose feeds are off", async () => {
189
+ const { app, announceToDiscover } = createDiscoverTestApp({
190
+ storedDiscover: "latest",
191
+ rssFeedsEnabled: false,
192
+ });
193
+
194
+ await postJson(app, "/settings/general/discover", { discover: "off" });
195
+
196
+ expect(announceToDiscover).not.toHaveBeenCalled();
197
+ });
198
+
199
+ it("does not announce when the save was not an opt-in", async () => {
200
+ const { app, announceToDiscover, updateDiscoverSetting } =
201
+ createDiscoverTestApp({ storedDiscover: "latest" });
202
+ updateDiscoverSetting.mockResolvedValueOnce({ shouldAnnounce: false });
203
+
204
+ await postJson(app, "/settings/general/discover", {
205
+ discover: "latest",
206
+ });
207
+
208
+ // A request the schema rejects never reaches the service, and would pass
209
+ // the assertion below for the wrong reason.
210
+ expect(updateDiscoverSetting).toHaveBeenCalledOnce();
211
+ expect(announceToDiscover).not.toHaveBeenCalled();
212
+ });
213
+ });
@@ -272,6 +272,7 @@ function CustomUrlsListContent({
272
272
  class="custom-url-menu"
273
273
  role="menu"
274
274
  data-custom-url-menu
275
+ popover="manual"
275
276
  hidden
276
277
  >
277
278
  <button
@@ -434,15 +435,6 @@ function NewCustomUrlContent({
434
435
  }),
435
436
  )}
436
437
  </option>
437
- <option value="archive">
438
- {i18n._(
439
- msg({
440
- message: "Archive",
441
- comment:
442
- "@context: Custom URL type option for filtered archive view",
443
- }),
444
- )}
445
- </option>
446
438
  </select>
447
439
  </div>
448
440
 
@@ -494,33 +486,6 @@ function NewCustomUrlContent({
494
486
  </div>
495
487
  </div>
496
488
 
497
- <div data-show="$targetType === 'archive'" class="field">
498
- <label class="label">
499
- {i18n._(
500
- msg({
501
- message: "Query Parameters",
502
- comment: "@context: Archive custom URL query params field",
503
- }),
504
- )}
505
- </label>
506
- <input
507
- type="text"
508
- data-bind="archiveQuery"
509
- class="input"
510
- placeholder="format=note&title=none&visibility=public&view=list"
511
- />
512
- <p class="text-xs text-muted-foreground mt-1">
513
- {i18n._(
514
- msg({
515
- message:
516
- "Archive filter parameters (e.g. format=note&view=list)",
517
- comment:
518
- "@context: Help text for archive custom URL query params",
519
- }),
520
- )}
521
- </p>
522
- </div>
523
-
524
489
  <div
525
490
  data-show="$targetType === 'post' || $targetType === 'collection'"
526
491
  class="field"
@@ -705,7 +670,6 @@ customUrlsRoutes.post("/", async (c) => {
705
670
  targetId,
706
671
  toPath: body.toPath,
707
672
  redirectType,
708
- archiveQuery: body.archiveQuery,
709
673
  });
710
674
 
711
675
  return dsRedirect(