@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. 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,
@@ -211,7 +213,9 @@ composeRoutes.post("/thread", async (c) => {
211
213
  postSchemas.map((data, index) => ({
212
214
  data: {
213
215
  format: data.format,
214
- slug: index === 0 ? data.slug || undefined : undefined,
216
+ // Every post may carry its own permalink; blank means the service
217
+ // generates one (title-derived for the root, random id for replies).
218
+ slug: data.slug || undefined,
215
219
  title:
216
220
  data.format === "quote"
217
221
  ? data.sourceName || undefined
@@ -229,7 +233,12 @@ composeRoutes.post("/thread", async (c) => {
229
233
  collectionIds: index === 0 ? data.collectionIds : undefined,
230
234
  replyToId: index === 0 ? data.replyToId : undefined,
231
235
  quietReply: data.quietReply,
232
- publishedAt: index === 0 ? data.publishedAt : undefined,
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,
239
+ // Replies may carry their own date; when they don't, the service
240
+ // inherits the root's rather than stamping "now".
241
+ publishedAt: data.publishedAt,
233
242
  },
234
243
  attachments: data.attachments,
235
244
  })),
@@ -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(