@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -449,6 +449,30 @@ describe("Posts API Routes", () => {
449
449
  expect(body.attachments).toEqual([]);
450
450
  });
451
451
 
452
+ it("keeps the creation and edit times a restore sends", async () => {
453
+ const { app } = createTestApp({ authenticated: true });
454
+ app.route("/api/posts", postsApiRoutes);
455
+
456
+ const res = await app.request("/api/posts", {
457
+ method: "POST",
458
+ headers: { "Content-Type": "application/json" },
459
+ body: JSON.stringify({
460
+ format: "note",
461
+ bodyMarkdown: "Moved from another site",
462
+ publishedAt: 1700000500,
463
+ createdAt: 1700000000,
464
+ updatedAt: 1700009000,
465
+ }),
466
+ });
467
+
468
+ expect(res.status).toBe(201);
469
+ expect(await res.json()).toMatchObject({
470
+ publishedAt: 1700000500,
471
+ createdAt: 1700000000,
472
+ updatedAt: 1700009000,
473
+ });
474
+ });
475
+
452
476
  it("creates a post with bodyMarkdown", async () => {
453
477
  const { app } = createTestApp({ authenticated: true });
454
478
  app.route("/api/posts", postsApiRoutes);
@@ -38,9 +38,9 @@ describe("Settings API Routes", () => {
38
38
  expect(body.settings.SITE_NAME).toBe("Jant");
39
39
  expect(body.settings.SITE_DESCRIPTION).toBe("");
40
40
  expect(body.settings.SITE_LANGUAGE).toBe("en");
41
- expect(body.settings.PAGE_SIZE).toBe("50");
42
- expect(body.settings.SEARCH_PAGE_SIZE).toBe("50");
43
- expect(body.settings.ARCHIVE_PAGE_SIZE).toBe("50");
41
+ expect(body.settings.PAGE_SIZE).toBe("25");
42
+ expect(body.settings.SEARCH_PAGE_SIZE).toBe("25");
43
+ expect(body.settings.ARCHIVE_PAGE_SIZE).toBe("25");
44
44
  expect(body.settings.RSS_PUBLISH_DELAY_SECONDS).toBe("0");
45
45
  });
46
46
 
@@ -199,6 +199,27 @@ describe("Settings API Routes", () => {
199
199
  expect(body.details.rejectedKeys).toContain("AUTH_SECRET");
200
200
  });
201
201
 
202
+ it("rejects the multilingual keys, which only the language page may write", async () => {
203
+ const { app } = createTestApp({ authenticated: true });
204
+ app.route("/api/settings", settingsApiRoutes);
205
+
206
+ const res = await app.request("/api/settings", {
207
+ method: "PUT",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({
210
+ MULTILINGUAL_ENABLED: "true",
211
+ ADDITIONAL_LANGUAGES: "en",
212
+ }),
213
+ });
214
+
215
+ // Writing these directly would skip the confirmation that stamps
216
+ // existing posts and the URL-prefix conflict check.
217
+ expect(res.status).toBe(400);
218
+ const body = await res.json();
219
+ expect(body.details.rejectedKeys).toContain("MULTILINGUAL_ENABLED");
220
+ expect(body.details.rejectedKeys).toContain("ADDITIONAL_LANGUAGES");
221
+ });
222
+
202
223
  it("rejects internal keys", async () => {
203
224
  const { app } = createTestApp({ authenticated: true });
204
225
  app.route("/api/settings", settingsApiRoutes);
@@ -3,6 +3,7 @@ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
3
  import { DEFAULT_TEST_SITE_ID } from "../../../__tests__/helpers/db.js";
4
4
  import { telegramWebhookRoutes } from "../telegram.js";
5
5
  import type { StorageDriver } from "../../../lib/storage.js";
6
+ import type { DeferredPendingEnv } from "../../../lib/deferred.js";
6
7
 
7
8
  const BOT_ID = "111111";
8
9
  const BOT_TOKEN = `${BOT_ID}:AA-test-token`;
@@ -117,13 +118,13 @@ function setup(
117
118
 
118
119
  // The webhook ACKs Telegram immediately and runs the album flush in
119
120
  // detached promises so subsequent webhooks on the same chat aren't
120
- // serialized behind it. Tests have no `executionCtx.waitUntil`, so the
121
- // route falls back to writing each promise into `env.__telegramPending`
122
- // — collect them here so tests can await the flush before asserting.
121
+ // serialized behind it. Tests have no `executionCtx.waitUntil`, so
122
+ // `runDeferred` falls back to writing each promise into
123
+ // `env.__pendingDeferred` — collect them here so tests can await the flush
124
+ // before asserting.
123
125
  const pending: Promise<unknown>[] = [];
124
126
  ctx.app.use("/api/telegram/*", async (c, next) => {
125
- (c.env as { __telegramPending?: Promise<unknown>[] }).__telegramPending =
126
- pending;
127
+ (c.env as DeferredPendingEnv).__pendingDeferred = pending;
127
128
  return next();
128
129
  });
129
130
  ctx.app.route("/api/telegram", telegramWebhookRoutes);
@@ -46,6 +46,40 @@ describe("Upload API Routes", () => {
46
46
  });
47
47
  });
48
48
 
49
+ it("pages the media list with nextCursor", async () => {
50
+ const { app, services } = createTestApp({ authenticated: true });
51
+ app.route("/api/upload", uploadApiRoutes);
52
+
53
+ for (const name of ["a", "b", "c"]) {
54
+ await services.media.create({
55
+ filename: `${name}.webp`,
56
+ originalName: `${name}.webp`,
57
+ mimeType: "image/webp",
58
+ size: 1024,
59
+ storageKey: `media/${name}.webp`,
60
+ });
61
+ }
62
+
63
+ const seen: string[] = [];
64
+ let cursor: string | null = null;
65
+ for (let page = 0; page < 4; page += 1) {
66
+ const res = await app.request(
67
+ `/api/upload?limit=2${cursor ? `&cursor=${cursor}` : ""}`,
68
+ );
69
+ expect(res.status).toBe(200);
70
+ const body = (await res.json()) as {
71
+ media: { filename: string }[];
72
+ nextCursor: string | null;
73
+ };
74
+ seen.push(...body.media.map((media) => media.filename));
75
+ cursor = body.nextCursor;
76
+ if (!cursor) break;
77
+ }
78
+
79
+ expect(seen.sort()).toEqual(["a.webp", "b.webp", "c.webp"]);
80
+ expect(cursor).toBeNull();
81
+ });
82
+
49
83
  it("returns a single media item", async () => {
50
84
  const { app, services } = createTestApp({ authenticated: true });
51
85
  app.route("/api/upload", uploadApiRoutes);
@@ -9,9 +9,11 @@ import type { AppVariables } from "../../types/app-context.js";
9
9
  import { requireAuthApi } from "../../middleware/auth.js";
10
10
  import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
11
11
  import {
12
- CollectionDirectoryItemIdSchema,
12
+ CollectionDirectoryRowIdSchema,
13
13
  CollectionDescriptionValueSchema,
14
+ CollectionIdSchema,
14
15
  CollectionSortOrderSchema,
16
+ ContentLanguageSchema,
15
17
  CreateCollectionDirectoryItemSchema,
16
18
  CreateCollectionSchema,
17
19
  PostIdSchema,
@@ -36,12 +38,24 @@ const ThreadAssignSchema = z.object({
36
38
  });
37
39
 
38
40
  const MoveSchema = z.object({
39
- after: CollectionDirectoryItemIdSchema.nullable().optional(),
40
- before: CollectionDirectoryItemIdSchema.nullable().optional(),
41
+ after: CollectionDirectoryRowIdSchema.nullable().optional(),
42
+ before: CollectionDirectoryRowIdSchema.nullable().optional(),
43
+ });
44
+
45
+ const SlugQuerySchema = z.object({
46
+ mode: z.literal("check"),
47
+ slug: z.string().trim().toLowerCase().min(1).max(200),
48
+ collectionId: CollectionIdSchema.optional(),
41
49
  });
42
50
 
43
51
  const ListCollectionsQuerySchema = z.object({
44
52
  view: z.enum(["compose"]).optional(),
53
+ /**
54
+ * Content language of the calling view. The directory's Thread counts are
55
+ * narrowed by it, so the page and this endpoint have to be asked the same
56
+ * question — see `jant-collection-directory`.
57
+ */
58
+ lang: ContentLanguageSchema.optional(),
45
59
  });
46
60
 
47
61
  // List collections (includes Thread counts and directory items)
@@ -50,6 +64,11 @@ collectionsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
50
64
 
51
65
  if (query.view === "compose") {
52
66
  c.header("Cache-Control", "no-store");
67
+ // Smart collections are deliberately absent. A post cannot be added to one
68
+ // by hand — its conditions decide, permanently — so offering it here would
69
+ // be a control that does nothing. Every "add this post to a collection"
70
+ // surface leaves them out for the same reason; see the smart collections
71
+ // design notes on the asymmetry, which is not a gap to be filled in later.
53
72
  const collections = await c.var.services.collections.listByRecentActivity();
54
73
  return c.json({
55
74
  collections,
@@ -57,10 +76,14 @@ collectionsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
57
76
  });
58
77
  }
59
78
 
60
- const directoryData = await c.var.services.collections.listDirectoryData();
79
+ const directoryData = await c.var.services.collections.listDirectoryData({
80
+ isAuthenticated: c.var.isAuthenticated,
81
+ lang: query.lang,
82
+ });
61
83
 
62
84
  return c.json({
63
85
  collections: directoryData.collections,
86
+ smartCollections: directoryData.smartCollections,
64
87
  directoryItems: directoryData.directoryItems,
65
88
  });
66
89
  });
@@ -98,13 +121,17 @@ collectionsApiRoutes.put(
98
121
  );
99
122
 
100
123
  // Move directory item — must be before /:id
124
+ //
125
+ // The moved row and its neighbours may each be named by a collection or smart
126
+ // collection id: those are the rows the directory renders without one of its
127
+ // own. The service places them before it moves anything.
101
128
  collectionsApiRoutes.put(
102
129
  "/directory-items/:id/move",
103
130
  requireAuthApi(),
104
131
  async (c) => {
105
- const id = parseIdParam(
132
+ const id = parseValidated(
133
+ CollectionDirectoryRowIdSchema,
106
134
  c.req.param("id"),
107
- ID_PREFIX.collectionDirectoryItem,
108
135
  );
109
136
  const body = parseValidated(MoveSchema, await c.req.json());
110
137
 
@@ -135,6 +162,22 @@ collectionsApiRoutes.delete(
135
162
  },
136
163
  );
137
164
 
165
+ /**
166
+ * Address availability, in the shape smart collections already use.
167
+ *
168
+ * Declared before `/:id` so the literal path wins. The editing dialog asks
169
+ * while the author types, so a clash is answered where the decision is being
170
+ * made rather than on write.
171
+ */
172
+ collectionsApiRoutes.get("/slug", requireAuthApi(), async (c) => {
173
+ const query = parseValidated(SlugQuerySchema, c.req.query());
174
+ const available = await c.var.services.collections.checkSlugAvailability(
175
+ query.slug,
176
+ query.collectionId,
177
+ );
178
+ return c.json({ slug: query.slug, available });
179
+ });
180
+
138
181
  // Get single collection
139
182
  collectionsApiRoutes.get("/:id", requirePublicApiAccess(), async (c) => {
140
183
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.collection);
@@ -63,7 +63,6 @@ customUrlsApiRoutes.post("/", requireAuthApi(), async (c) => {
63
63
  targetId,
64
64
  toPath: body.toPath,
65
65
  redirectType,
66
- archiveQuery: body.archiveQuery,
67
66
  });
68
67
 
69
68
  return c.json(customUrl, 201);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Discover API Routes
3
+ *
4
+ * The one address a Discover directory calls besides the feeds it polls.
5
+ * `GET /api/discover/posts` says, for each post ID it names, whether the post
6
+ * is in this site's Latest feed and whether its Thread is in the featured
7
+ * feed. The feed cannot say this about a post pushed past its length, and the
8
+ * permalink cannot say it about a post hidden from Latest or unfeatured, which
9
+ * is still a live page — so the directory asks instead of guessing.
10
+ *
11
+ * Specified in `docs/feeds.md`. A directory never builds the address: every
12
+ * feed names it in `<jant:discover status="…">`.
13
+ */
14
+
15
+ import { Hono } from "hono";
16
+ import { z } from "zod";
17
+ import type { Bindings } from "../../types.js";
18
+ import type { AppVariables } from "../../types/app-context.js";
19
+ import { DISCOVER_STATUS_MAX_IDS } from "../../lib/discover.js";
20
+ import {
21
+ featuredFeedSelection,
22
+ getRssPublishedBefore,
23
+ latestFeedSelection,
24
+ } from "../../lib/feed-policy.js";
25
+ import { createTypeIdSchema, ID_PREFIX } from "../../lib/ids.js";
26
+ import { ContentLanguageSchema, parseValidated } from "../../lib/schemas.js";
27
+
28
+ type Env = { Bindings: Bindings; Variables: AppVariables };
29
+
30
+ export const discoverApiRoutes = new Hono<Env>();
31
+
32
+ const PostStatusQuerySchema = z.object({
33
+ id: z
34
+ .array(createTypeIdSchema(ID_PREFIX.post))
35
+ .min(1, "Name at least one post with id=")
36
+ .max(
37
+ DISCOVER_STATUS_MAX_IDS,
38
+ `Ask about at most ${DISCOVER_STATUS_MAX_IDS} posts at a time`,
39
+ ),
40
+ /** The language view the answer is about, as a language feed is scoped. */
41
+ lang: ContentLanguageSchema.optional(),
42
+ });
43
+
44
+ /** Where one post stands in the feeds a directory reads. */
45
+ export interface DiscoverPostStatus {
46
+ id: string;
47
+ /** In the Latest feed. */
48
+ latest: boolean;
49
+ /** Its Thread is in the featured feed. */
50
+ featured: boolean;
51
+ }
52
+
53
+ discoverApiRoutes.get("/posts", async (c) => {
54
+ // It answers for a site that takes part, and follows the Discover setting
55
+ // rather than `PUBLIC_API_ENABLED`: it says nothing the public feeds do not,
56
+ // and a site that turned its JSON API off has not left Discover.
57
+ if (c.var.appConfig.discover === "none") return c.notFound();
58
+
59
+ const query = parseValidated(PostStatusQuerySchema, {
60
+ id: c.req.queries("id") ?? [],
61
+ lang: c.req.query("lang"),
62
+ });
63
+ const ids = [...new Set(query.id)];
64
+ // The feeds hold a new post back for the RSS delay, and so does this: the
65
+ // answer is about the feeds as a reader would fetch them now.
66
+ const publishedBefore = getRssPublishedBefore(
67
+ c.var.appConfig.rssPublishDelaySeconds,
68
+ );
69
+
70
+ const [latest, featured] = await Promise.all([
71
+ c.var.services.posts.list({
72
+ ...latestFeedSelection({ lang: query.lang, publishedBefore }),
73
+ ids,
74
+ limit: ids.length,
75
+ }),
76
+ c.var.services.posts.listFeaturedThreadRootIds({
77
+ ...featuredFeedSelection({ lang: query.lang, publishedBefore }),
78
+ threadIds: ids,
79
+ limit: ids.length,
80
+ }),
81
+ ]);
82
+ const inLatest = new Set(latest.map((post) => post.id));
83
+ const inFeatured = new Set(featured);
84
+
85
+ // Every ID asked about is answered, in the order asked. A post that is
86
+ // deleted, private, a draft, a reply, or not this site's at all comes back
87
+ // `false` on both: to a directory they all mean the same thing.
88
+ return c.json({
89
+ posts: ids.map((id): DiscoverPostStatus => ({
90
+ id,
91
+ latest: inLatest.has(id),
92
+ featured: inFeatured.has(id),
93
+ })),
94
+ });
95
+ });
@@ -23,10 +23,13 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
23
23
  siteUrl: appConfig.siteUrl,
24
24
  siteDescription: appConfig.siteDescription,
25
25
  siteLanguage: appConfig.siteLanguage,
26
+ multilingualEnabled: appConfig.multilingualEnabled,
27
+ additionalLanguages: appConfig.additionalLanguages,
26
28
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
27
29
  publicApiEnabled: appConfig.publicApiEnabled,
28
30
  rssFeedsEnabled: appConfig.rssFeedsEnabled,
29
31
  mainRssFeed: appConfig.mainRssFeed,
32
+ archiveDefaultLayout: appConfig.archiveDefaultLayout,
30
33
  siteFooter: appConfig.siteFooter,
31
34
  showHeaderAvatar: appConfig.showHeaderAvatar,
32
35
  siteAvatarUrl: appConfig.siteAvatarUrl,
@@ -54,12 +57,12 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
54
57
  storage: c.var.storage,
55
58
  },
56
59
  );
57
- const zip = await exportService.generateHugoSite();
58
- return new Response(zip, {
60
+ // Streamed: stored media is read as the archive goes out, so the response
61
+ // starts at once and memory stays flat whatever the site's size.
62
+ return new Response(await exportService.generateHugoSite(), {
59
63
  headers: {
60
64
  "Content-Type": "application/zip",
61
65
  "Content-Disposition": 'attachment; filename="jant-export.zip"',
62
- "Content-Length": String(zip.byteLength),
63
66
  },
64
67
  });
65
68
  });
@@ -94,8 +94,10 @@ describe("Internal site admin routes", () => {
94
94
 
95
95
  expect(siteRows).toEqual([{ key: "demo-cloud" }]);
96
96
  expect(domainRows).toEqual([{ host: "demo-cloud.example.com" }]);
97
+ // Provisioned, not completed: the site is real and servable, but its
98
+ // owner has not yet confirmed the language they write in.
97
99
  expect(settingRows).toEqual([
98
- { key: "ONBOARDING_STATUS", value: "completed" },
100
+ { key: "ONBOARDING_STATUS", value: "provisioned" },
99
101
  { key: "SITE_LANGUAGE", value: "en" },
100
102
  { key: "SITE_NAME", value: "Demo Cloud" },
101
103
  ]);
@@ -136,7 +138,7 @@ describe("Internal site admin routes", () => {
136
138
  .all(body.siteId) as { key: string; value: string }[];
137
139
 
138
140
  expect(settingRows).toEqual([
139
- { key: "ONBOARDING_STATUS", value: "completed" },
141
+ { key: "ONBOARDING_STATUS", value: "provisioned" },
140
142
  { key: "SITE_LANGUAGE", value: "en" },
141
143
  { key: "SITE_NAME", value: "Locale Demo" },
142
144
  { key: "TIME_ZONE", value: "Etc/GMT-8" },
@@ -405,6 +407,28 @@ describe("Internal site admin routes", () => {
405
407
  VALUES (?, 'SITE_AVATAR', 'sites/${siteId}/avatar.webp', 1774200003)`,
406
408
  )
407
409
  .run(siteId);
410
+ // A smart collection filtering by a collection on the same site. The pair
411
+ // is the shape this endpoint used to die on: the condition column once
412
+ // carried `ON DELETE restrict`, so clearing `collection` threw partway
413
+ // through a delete that runs without a transaction on SQLite and D1.
414
+ sqlite
415
+ .prepare(
416
+ `INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at")
417
+ VALUES ('col_delete_1', ?, 'Notes', 'newest', 1774200004, 1774200004)`,
418
+ )
419
+ .run(siteId);
420
+ sqlite
421
+ .prepare(
422
+ `INSERT INTO "smart_collection" ("id", "site_id", "title", "collection_id", "format", "sort", "created_at", "updated_at")
423
+ VALUES ('smc_delete_1', ?, 'Untitled notes', 'col_delete_1', 'note', 'newest', 1774200005, 1774200005)`,
424
+ )
425
+ .run(siteId);
426
+ sqlite
427
+ .prepare(
428
+ `INSERT INTO "path_registry" ("id", "site_id", "path", "kind", "smart_collection_id", "created_at", "updated_at")
429
+ VALUES ('pth_delete_1', ?, 'untitled-notes', 'slug', 'smc_delete_1', 1774200006, 1774200006)`,
430
+ )
431
+ .run(siteId);
408
432
 
409
433
  const deleteRes = await app.request(`/api/internal/sites/${siteId}`, {
410
434
  method: "DELETE",
@@ -436,6 +460,19 @@ describe("Internal site admin routes", () => {
436
460
  'SELECT COUNT(*) AS count FROM "site_setting" WHERE "site_id" = ?',
437
461
  )
438
462
  .get(siteId) as { count: number };
463
+ const deletedCollectionCount = sqlite
464
+ .prepare('SELECT COUNT(*) AS count FROM "collection" WHERE "site_id" = ?')
465
+ .get(siteId) as { count: number };
466
+ const deletedSmartCollectionCount = sqlite
467
+ .prepare(
468
+ 'SELECT COUNT(*) AS count FROM "smart_collection" WHERE "site_id" = ?',
469
+ )
470
+ .get(siteId) as { count: number };
471
+ const deletedPathCount = sqlite
472
+ .prepare(
473
+ 'SELECT COUNT(*) AS count FROM "path_registry" WHERE "site_id" = ?',
474
+ )
475
+ .get(siteId) as { count: number };
439
476
  const defaultSiteCount = sqlite
440
477
  .prepare('SELECT COUNT(*) AS count FROM "site" WHERE "id" = ?')
441
478
  .get(DEFAULT_TEST_SITE_ID) as { count: number };
@@ -445,6 +482,9 @@ describe("Internal site admin routes", () => {
445
482
  expect(deletedMemberCount.count).toBe(0);
446
483
  expect(deletedPostCount.count).toBe(0);
447
484
  expect(deletedSettingsCount.count).toBe(0);
485
+ expect(deletedCollectionCount.count).toBe(0);
486
+ expect(deletedSmartCollectionCount.count).toBe(0);
487
+ expect(deletedPathCount.count).toBe(0);
448
488
  expect(defaultSiteCount.count).toBe(1);
449
489
  });
450
490
 
@@ -240,7 +240,6 @@ internalSitesRoutes.get(
240
240
  return new Response(archive.zip, {
241
241
  headers: {
242
242
  "Content-Disposition": `attachment; filename="${archive.filename}"`,
243
- "Content-Length": String(archive.zip.byteLength),
244
243
  "Content-Type": "application/zip",
245
244
  },
246
245
  });
@@ -16,8 +16,9 @@ import {
16
16
  parseValidated,
17
17
  } from "../../lib/schemas.js";
18
18
  import { assertFound, parseIdParam, NotFoundError } from "../../lib/errors.js";
19
+ import { AddressQuerySchema, requestInternalPath } from "../../lib/address.js";
20
+ import { toPublicPath } from "../../lib/url.js";
19
21
  import { ID_PREFIX } from "../../lib/ids.js";
20
- import { getCollectionPagePath } from "../../lib/collection-paths.js";
21
22
  import { renderSiteHeaderHtml } from "../../lib/site-header-fragment.js";
22
23
 
23
24
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -69,6 +70,51 @@ navItemsApiRoutes.get("/pages", requireAuthApi(), async (c) => {
69
70
  return c.json({ pages });
70
71
  });
71
72
 
73
+ /**
74
+ * What a pasted address could become in navigation.
75
+ *
76
+ * The author is holding a URL, not a title, so searching titles cannot answer
77
+ * them. The rules stay in the service; this only says which address was asked
78
+ * about, so the picker can name it back.
79
+ */
80
+ navItemsApiRoutes.get("/resolve", requireAuthApi(), async (c) => {
81
+ const { url } = parseValidated(AddressQuerySchema, c.req.query());
82
+ const path = requestInternalPath(c, url);
83
+ if (path === null) {
84
+ return c.json({ resolution: { kind: "external", address: url.trim() } });
85
+ }
86
+
87
+ const resolution = await c.var.services.navItems.resolveNavTarget(path);
88
+ const address = toPublicPath(path, c.var.appConfig.sitePathPrefix);
89
+
90
+ if (resolution.status === "page") {
91
+ return c.json({
92
+ resolution: { kind: "page", address, page: resolution.page },
93
+ });
94
+ }
95
+ if (resolution.status === "collection") {
96
+ return c.json({
97
+ resolution: {
98
+ kind: "collection",
99
+ address,
100
+ collection: resolution.collection,
101
+ },
102
+ });
103
+ }
104
+
105
+ if (resolution.status === "smart_collection") {
106
+ return c.json({
107
+ resolution: {
108
+ kind: "smart_collection",
109
+ address,
110
+ smartCollection: resolution.smartCollection,
111
+ },
112
+ });
113
+ }
114
+
115
+ return c.json({ resolution: { kind: resolution.status, address } });
116
+ });
117
+
72
118
  // Move nav item (requires auth) — must be before /:id
73
119
  navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
74
120
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.navItem);
@@ -98,16 +144,17 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
98
144
  placement: body.placement,
99
145
  });
100
146
  } else if (body.type === "collection") {
101
- const collection = await c.var.services.collections.getById(
102
- body.collectionId,
103
- );
104
- if (!collection) throw new NotFoundError("Collection");
105
-
106
147
  item = await c.var.services.navItems.create({
107
148
  type: "collection",
108
149
  collectionId: body.collectionId,
109
- label: body.label || collection.title,
110
- url: getCollectionPagePath(collection.slug),
150
+ label: body.label,
151
+ placement: body.placement,
152
+ });
153
+ } else if (body.type === "smart_collection") {
154
+ item = await c.var.services.navItems.create({
155
+ type: "smart_collection",
156
+ smartCollectionId: body.smartCollectionId,
157
+ label: body.label,
111
158
  placement: body.placement,
112
159
  });
113
160
  } else if (body.type === "page") {