@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
@@ -31,9 +31,9 @@ describe("resolveConfig", () => {
31
31
  expect(config.publicApiEnabled).toBe(true);
32
32
  expect(config.rssFeedsEnabled).toBe(true);
33
33
  expect(config.demoMode).toBe(false);
34
- expect(config.pageSize).toBe(50);
35
- expect(config.searchPageSize).toBe(50);
36
- expect(config.archivePageSize).toBe(50);
34
+ expect(config.pageSize).toBe(25);
35
+ expect(config.searchPageSize).toBe(25);
36
+ expect(config.archivePageSize).toBe(25);
37
37
  expect(config.rssFeedLimit).toBe(50);
38
38
  expect(config.rssPublishDelaySeconds).toBe(300);
39
39
  });
@@ -52,6 +52,73 @@ describe("resolveConfig", () => {
52
52
  expect(config.siteName).toBe("FromEnv");
53
53
  });
54
54
 
55
+ // A settings row holding an empty string is not always the same thing. The
56
+ // editor decides: a boolean, a number, and an enum with fixed options cannot
57
+ // be saved empty, so an empty one of those came from a path that skipped
58
+ // validation — a snapshot import replaying raw SQL — and the environment
59
+ // variable should still be heard.
60
+ // Text can legitimately be empty, and then the row wins.
61
+ describe("empty settings rows", () => {
62
+ it("lets an environment value through an empty boolean row", () => {
63
+ const config = resolveConfig(makeEnv({ NOINDEX: "true" }), {
64
+ NOINDEX: "",
65
+ });
66
+
67
+ expect(config.noindex).toBe(true);
68
+ });
69
+
70
+ it("falls back to the default for an empty boolean row with no env value", () => {
71
+ const config = resolveConfig(makeEnv(), { NOINDEX: "" });
72
+
73
+ expect(config.noindex).toBe(false);
74
+ });
75
+
76
+ it("still lets a saved boolean row override the environment", () => {
77
+ const config = resolveConfig(makeEnv({ NOINDEX: "true" }), {
78
+ NOINDEX: "false",
79
+ });
80
+
81
+ expect(config.noindex).toBe(false);
82
+ });
83
+
84
+ it("lets an environment value through an empty numeric row", () => {
85
+ const config = resolveConfig(makeEnv({ PAGE_SIZE: "20" }), {
86
+ PAGE_SIZE: "",
87
+ });
88
+
89
+ expect(config.pageSize).toBe(20);
90
+ });
91
+
92
+ it("keeps an empty enum row ahead of the environment when empty is an option", () => {
93
+ // `DASHBOARD_LANGUAGE` lists "" among its options — it is how the
94
+ // dashboard is told to follow the site language.
95
+ const config = resolveConfig(makeEnv({ DASHBOARD_LANGUAGE: "zh-Hans" }), {
96
+ DASHBOARD_LANGUAGE: "",
97
+ });
98
+
99
+ expect(config.dashboardLanguage).toBe("");
100
+ });
101
+
102
+ it("keeps an empty text row ahead of the environment", () => {
103
+ const config = resolveConfig(
104
+ makeEnv({ SITE_FOOTER: "From environment" }),
105
+ { SITE_FOOTER: "" },
106
+ );
107
+
108
+ expect(config.siteFooter).toBe("");
109
+ });
110
+
111
+ it("lets an environment value through an empty enum row", () => {
112
+ // `MAIN_RSS_FEED` offers "featured" and "latest" and nothing else, so an
113
+ // empty row is as impossible to save as an empty boolean.
114
+ const config = resolveConfig(makeEnv({ MAIN_RSS_FEED: "latest" }), {
115
+ MAIN_RSS_FEED: "",
116
+ });
117
+
118
+ expect(config.mainRssFeed).toBe("latest");
119
+ });
120
+ });
121
+
55
122
  it("lets an explicit empty DB value override an environment fallback", () => {
56
123
  const config = resolveConfig(
57
124
  makeEnv({ SITE_DESCRIPTION: "From environment" }),
@@ -81,6 +148,34 @@ describe("resolveConfig", () => {
81
148
  expect(config.mainRssFeed).toBe("featured");
82
149
  });
83
150
 
151
+ it("resolves archiveDefaultLayout from DB, env, and defaults", () => {
152
+ const defaultConfig = resolveConfig(makeEnv(), {});
153
+ expect(defaultConfig.archiveDefaultLayout).toBe("list");
154
+
155
+ const envConfig = resolveConfig(
156
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "grid" }),
157
+ {},
158
+ );
159
+ expect(envConfig.archiveDefaultLayout).toBe("grid");
160
+
161
+ const dbConfig = resolveConfig(
162
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "list" }),
163
+ {
164
+ ARCHIVE_DEFAULT_LAYOUT: "grid",
165
+ },
166
+ );
167
+ expect(dbConfig.archiveDefaultLayout).toBe("grid");
168
+ });
169
+
170
+ it("falls back when the archive layout setting is invalid", () => {
171
+ const config = resolveConfig(
172
+ makeEnv({ ARCHIVE_DEFAULT_LAYOUT: "carousel" }),
173
+ {},
174
+ );
175
+
176
+ expect(config.archiveDefaultLayout).toBe("list");
177
+ });
178
+
84
179
  it("normalizes legacy time zone values from the database", () => {
85
180
  const config = resolveConfig(makeEnv(), { TIME_ZONE: "Beijing" });
86
181
 
@@ -172,6 +267,39 @@ describe("resolveConfig", () => {
172
267
  expect(config.siteAvatarUrl).toBe(
173
268
  "https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
174
269
  );
270
+ // Without a transform there is nothing to size it down with, so the
271
+ // header falls back to the canonical asset.
272
+ expect(config.siteAvatarThumbUrl).toBe(config.siteAvatarUrl);
273
+ });
274
+
275
+ it("sizes the header avatar down when an image transform is configured", () => {
276
+ const config = resolveConfig(
277
+ makeEnv({
278
+ R2_PUBLIC_URL: "https://r2.example.com",
279
+ IMAGE_TRANSFORM_URL: "https://r2.example.com/cdn-cgi/image",
280
+ STORAGE_DRIVER: "r2",
281
+ }),
282
+ {
283
+ SITE_AVATAR:
284
+ "media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
285
+ },
286
+ );
287
+
288
+ // The canonical URL stays full-resolution: it is what the social card and
289
+ // the settings editor need.
290
+ expect(config.siteAvatarUrl).toBe(
291
+ "https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
292
+ );
293
+ expect(config.siteAvatarThumbUrl).toBe(
294
+ "https://r2.example.com/cdn-cgi/image/width=128,quality=80,format=auto,fit=scale-down/https://r2.example.com/media/sit_test00000000000000000000000/assets/avatar/avatar.jpg",
295
+ );
296
+ });
297
+
298
+ it("leaves both avatar URLs empty without a stored avatar", () => {
299
+ const config = resolveConfig(makeEnv({}), {});
300
+
301
+ expect(config.siteAvatarUrl).toBe("");
302
+ expect(config.siteAvatarThumbUrl).toBe("");
175
303
  });
176
304
 
177
305
  it("returns empty siteAvatarUrl when no avatar set", () => {
@@ -9,11 +9,12 @@ import {
9
9
  CreatePostApiSchema,
10
10
  UpdatePostSchema,
11
11
  UpdatePostApiSchema,
12
- SetupSchema,
12
+ SetupAccountSchema,
13
13
  SigninSchema,
14
14
  UpdateSiteSettingsSchema,
15
15
  normalizeEmail,
16
16
  parseFormData,
17
+ PostFilterSelectionSchema,
17
18
  parseFormDataOptional,
18
19
  validateAttachmentCount,
19
20
  } from "../schemas.js";
@@ -60,10 +61,17 @@ describe("RedirectTypeSchema", () => {
60
61
  expect(RedirectTypeSchema.parse("302")).toBe("302");
61
62
  });
62
63
 
64
+ // The custom URL list answers `redirectType: 301`; sending that back to
65
+ // create one has to work.
66
+ it("accepts 301 and 302 as numbers, as the API answers them", () => {
67
+ expect(RedirectTypeSchema.parse(301)).toBe("301");
68
+ expect(RedirectTypeSchema.parse(302)).toBe("302");
69
+ });
70
+
63
71
  it("rejects other values", () => {
64
72
  expect(() => RedirectTypeSchema.parse("200")).toThrow();
65
73
  expect(() => RedirectTypeSchema.parse("404")).toThrow();
66
- expect(() => RedirectTypeSchema.parse(301)).toThrow();
74
+ expect(() => RedirectTypeSchema.parse(308)).toThrow();
67
75
  });
68
76
  });
69
77
 
@@ -75,10 +83,9 @@ describe("normalizeEmail", () => {
75
83
  });
76
84
  });
77
85
 
78
- describe("SetupSchema", () => {
86
+ describe("SetupAccountSchema", () => {
79
87
  it("normalizes email before returning parsed data", () => {
80
- const result = SetupSchema.parse({
81
- siteName: "Jant",
88
+ const result = SetupAccountSchema.parse({
82
89
  email: " Admin@Example.COM ",
83
90
  password: "password123",
84
91
  });
@@ -617,6 +624,25 @@ describe("CreatePostApiSchema", () => {
617
624
  status: "published",
618
625
  };
619
626
 
627
+ it("accepts the creation and edit times a restore passes", () => {
628
+ expect(
629
+ CreatePostApiSchema.parse({
630
+ ...validPost,
631
+ createdAt: 1700000000,
632
+ updatedAt: 1700009000,
633
+ }),
634
+ ).toMatchObject({ createdAt: 1700000000, updatedAt: 1700009000 });
635
+ });
636
+
637
+ it("leaves record timestamps out of updates", () => {
638
+ expect(
639
+ UpdatePostApiSchema.safeParse({ createdAt: 1700000000 }).success,
640
+ ).toBe(false);
641
+ expect(
642
+ UpdatePostApiSchema.safeParse({ updatedAt: 1700000000 }).success,
643
+ ).toBe(false);
644
+ });
645
+
620
646
  it("accepts ordered attachment inputs", () => {
621
647
  const mediaId = createEntityId("media");
622
648
  const result = CreatePostApiSchema.parse({
@@ -816,3 +842,41 @@ describe("validateAttachmentCount", () => {
816
842
  );
817
843
  });
818
844
  });
845
+
846
+ describe("PostFilterSelectionSchema", () => {
847
+ it("accepts a selection a smart collection may store", () => {
848
+ const result = PostFilterSelectionSchema.safeParse({
849
+ format: "note",
850
+ media: "any",
851
+ visibility: "featured",
852
+ collection: ["col_01m0f291t3fzvte3vj2g8d611z"],
853
+ });
854
+ expect(result.success).toBe(true);
855
+ });
856
+
857
+ it("accepts the empty selection, which means every post", () => {
858
+ expect(PostFilterSelectionSchema.safeParse({}).success).toBe(true);
859
+ });
860
+
861
+ it("refuses the private visibility a smart collection can never name", () => {
862
+ expect(
863
+ PostFilterSelectionSchema.safeParse({ visibility: "private" }).success,
864
+ ).toBe(false);
865
+ });
866
+
867
+ it("refuses a key that is not a dimension", () => {
868
+ expect(
869
+ PostFilterSelectionSchema.safeParse({ language: "en" }).success,
870
+ ).toBe(false);
871
+ });
872
+
873
+ it("names the condition it rejected", () => {
874
+ const result = PostFilterSelectionSchema.safeParse({ year: 1970 });
875
+ expect(result.success).toBe(false);
876
+ if (result.success) return;
877
+ expect(result.error.issues[0]?.path).toEqual(["year"]);
878
+ expect(result.error.issues[0]?.message).toBe(
879
+ "Invalid value for the year condition",
880
+ );
881
+ });
882
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderSiteUnavailablePage } from "../site-unavailable-page.js";
3
+
4
+ describe("renderSiteUnavailablePage", () => {
5
+ it("links back to the configured hosting provider", () => {
6
+ const html = renderSiteUnavailablePage({
7
+ HOSTED_CONTROL_PLANE_BASE_URL: "https://cloud-jant.localtest.me",
8
+ HOSTED_CONTROL_PLANE_PROVIDER_NAME: "Jant Cloud",
9
+ SITE_RESOLUTION_MODE: "host-based",
10
+ });
11
+
12
+ expect(html).toContain("This site is offline");
13
+ expect(html).toContain(
14
+ '<a href="https://cloud-jant.localtest.me/app">Jant Cloud</a>',
15
+ );
16
+ });
17
+
18
+ it("stays provider-neutral when no control plane is configured", () => {
19
+ const html = renderSiteUnavailablePage({});
20
+
21
+ expect(html).toContain("There's nothing to read here right now");
22
+ expect(html).toContain("sign in to your hosting provider");
23
+ expect(html).not.toContain("<a ");
24
+ });
25
+ });
@@ -141,4 +141,74 @@ describe("generatePostSlug", () => {
141
141
  ).rejects.toThrow("Could not generate a unique slug");
142
142
  });
143
143
  });
144
+
145
+ describe("language suffix", () => {
146
+ /** Every slug is taken except the ones listed. */
147
+ const onlyAvailable = (...free: string[]) => {
148
+ const allowed = new Set(free);
149
+ return async (slug: string) => allowed.has(slug);
150
+ };
151
+
152
+ it("is not used when the base slug is free", async () => {
153
+ const slug = await generatePostSlug({
154
+ title: "Book Review",
155
+ idLength: 5,
156
+ languageSuffix: "en",
157
+ isAvailable: alwaysAvailable,
158
+ });
159
+ expect(slug).toBe("book-review");
160
+ });
161
+
162
+ it("is tried before a random suffix when the base collides", async () => {
163
+ const slug = await generatePostSlug({
164
+ title: "Book Review",
165
+ idLength: 5,
166
+ languageSuffix: "en",
167
+ isAvailable: onlyAvailable("book-review-en"),
168
+ });
169
+ expect(slug).toBe("book-review-en");
170
+ });
171
+
172
+ it("keeps the multi-part tag intact", async () => {
173
+ const slug = await generatePostSlug({
174
+ title: "Book Review",
175
+ idLength: 5,
176
+ languageSuffix: "zh-hant",
177
+ isAvailable: onlyAvailable("book-review-zh-hant"),
178
+ });
179
+ expect(slug).toBe("book-review-zh-hant");
180
+ });
181
+
182
+ it("falls back to a random suffix when the language slug is taken too", async () => {
183
+ const slug = await generatePostSlug({
184
+ title: "Book Review",
185
+ idLength: 5,
186
+ languageSuffix: "en",
187
+ isAvailable: async (candidate: string) =>
188
+ candidate !== "book-review" && candidate !== "book-review-en",
189
+ });
190
+ expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
191
+ });
192
+
193
+ it("goes straight to a random suffix without one", async () => {
194
+ const slug = await generatePostSlug({
195
+ title: "Book Review",
196
+ idLength: 5,
197
+ isAvailable: async (candidate: string) => candidate !== "book-review",
198
+ });
199
+ expect(slug).toMatch(/^book-review-[a-z0-9]{5}$/);
200
+ });
201
+
202
+ it("skips a language candidate that lands on a reserved path", async () => {
203
+ const slug = await generatePostSlug({
204
+ title: "feed",
205
+ idLength: 5,
206
+ languageSuffix: "en",
207
+ isAvailable: alwaysAvailable,
208
+ });
209
+ // "feed" is reserved, so the base is never offered and the language
210
+ // candidate is never reached — the random fallback owns this case.
211
+ expect(slug).toMatch(/^feed-[a-z0-9]{5}$/);
212
+ });
213
+ });
144
214
  });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { slugify } from "../slugify.js";
3
+
4
+ describe("slugify", () => {
5
+ it("converts text to lowercase hyphenated slug", () => {
6
+ expect(slugify("Hello World")).toBe("hello-world");
7
+ });
8
+
9
+ it("removes special characters", () => {
10
+ expect(slugify("Hello World! This is a Test.")).toBe(
11
+ "hello-world-this-is-a-test",
12
+ );
13
+ });
14
+
15
+ it("collapses multiple spaces", () => {
16
+ expect(slugify("Multiple Spaces")).toBe("multiple-spaces");
17
+ });
18
+
19
+ it("trims leading/trailing whitespace and hyphens", () => {
20
+ expect(slugify(" Multiple Spaces ")).toBe("multiple-spaces");
21
+ });
22
+
23
+ it("replaces underscores with hyphens", () => {
24
+ expect(slugify("hello_world")).toBe("hello-world");
25
+ });
26
+
27
+ it("handles already-slugified text", () => {
28
+ expect(slugify("already-a-slug")).toBe("already-a-slug");
29
+ });
30
+
31
+ it("handles empty string", () => {
32
+ expect(slugify("")).toBe("");
33
+ });
34
+
35
+ it("transliterates accented characters", () => {
36
+ expect(slugify("café & résumé")).toBe("cafe-and-resume");
37
+ });
38
+
39
+ it("converts Chinese characters to pinyin", () => {
40
+ expect(slugify("书评")).toBe("shu-ping");
41
+ });
42
+
43
+ it("handles mixed Chinese and English text", () => {
44
+ expect(slugify("我的 Blog")).toBe("wo-de-blog");
45
+ });
46
+
47
+ it("handles CJK characters with spaces", () => {
48
+ expect(slugify("电影 评论")).toBe("dian-ying-ping-lun");
49
+ });
50
+
51
+ it("romanizes Korean titles", () => {
52
+ expect(slugify("안녕하세요 세계")).toBe("annyeonghaseyo-segye");
53
+ expect(slugify("개발 일지 3일차")).toBe("gaebal-ilji-3ilcha");
54
+ });
55
+
56
+ it("romanizes kana-only Japanese titles", () => {
57
+ expect(slugify("カタカナタイトル")).toBe("katakanataitoru");
58
+ expect(slugify("ハンバーガー")).toBe("hanbaga");
59
+ });
60
+
61
+ it("keeps Latin words in mostly-kana Japanese titles", () => {
62
+ expect(slugify("React入門ガイド")).toBe("react-gaido");
63
+ });
64
+
65
+ it("returns empty for kanji-heavy Japanese titles instead of garbling them", () => {
66
+ // Kanji carries the meaning; without a reading dictionary any output
67
+ // would be gibberish, so callers should fall back to a random ID.
68
+ expect(slugify("日本語のタイトルです")).toBe("");
69
+ expect(slugify("東京タワーに行った")).toBe("");
70
+ expect(slugify("こんにちは世界")).toBe("");
71
+ });
72
+
73
+ it("returns empty for scripts with no usable transliteration", () => {
74
+ expect(slugify("שלום עולם")).toBe("");
75
+ expect(slugify("สวัสดีชาวโลก")).toBe("");
76
+ });
77
+
78
+ it("returns empty when most of the title is untransliterable", () => {
79
+ expect(slugify("שלום world")).toBe("");
80
+ });
81
+
82
+ it("returns empty for emoji-only titles", () => {
83
+ expect(slugify("🎉🎉")).toBe("");
84
+ });
85
+ });
@@ -210,6 +210,38 @@ describe("createS3Driver", () => {
210
210
  "x-amz-checksum-sha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
211
211
  });
212
212
  });
213
+
214
+ it("omits a request checksum when the caller did not supply one", async () => {
215
+ const driver = createS3Driver({
216
+ endpoint: "https://s3.example.com",
217
+ bucket: "jant-media",
218
+ accessKeyId: "access-key",
219
+ secretAccessKey: "secret-key",
220
+ region: "auto",
221
+ });
222
+
223
+ const presigned = await driver.presignPut?.("media/test.webp", {
224
+ contentType: "image/webp",
225
+ contentDisposition: "inline",
226
+ cacheControl: "public, max-age=31536000, immutable",
227
+ expiresInSeconds: 900,
228
+ });
229
+
230
+ if (!presigned) {
231
+ throw new Error("Expected presigned PUT target");
232
+ }
233
+
234
+ // Presigning runs with an empty body, so a default request checksum would
235
+ // pin CRC32-of-nothing to the URL and every real upload would 400.
236
+ const params = new URL(presigned.url).searchParams;
237
+ expect(params.has("x-amz-checksum-crc32")).toBe(false);
238
+ expect(params.has("x-amz-sdk-checksum-algorithm")).toBe(false);
239
+ expect(presigned.headers).toEqual({
240
+ "Content-Type": "image/webp",
241
+ "Content-Disposition": "inline",
242
+ "Cache-Control": "public, max-age=31536000, immutable",
243
+ });
244
+ });
213
245
  });
214
246
 
215
247
  describe("createR2Driver", () => {
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ foldThreadReplies,
4
+ getThreadHiddenCount,
5
+ THREAD_LEADING_REPLIES,
6
+ THREAD_TRAILING_REPLIES,
7
+ } from "../thread-fold.js";
8
+
9
+ const reply = (n: number) => ({ id: `reply-${n}` });
10
+ const replies = (count: number) =>
11
+ Array.from({ length: count }, (_unused, index) => reply(index + 1));
12
+
13
+ const ids = (list: { id: string }[]) => list.map((item) => item.id);
14
+
15
+ describe("foldThreadReplies", () => {
16
+ it("has no fold for a thread with no replies", () => {
17
+ expect(foldThreadReplies([])).toBeNull();
18
+ });
19
+
20
+ // The window is the first two plus the last three, so anything up to six
21
+ // posts arrives whole and there is nothing for a gap link to stand for.
22
+ it.each([1, 2, 3, 4, 5])("hides nothing with %i replies", (count) => {
23
+ const fold = foldThreadReplies(replies(count));
24
+
25
+ expect(fold?.hiddenCount).toBe(0);
26
+ expect(fold?.firstHiddenReply).toBeNull();
27
+ // Every reply is shown exactly once, the hero included.
28
+ const shown = [
29
+ ...ids(fold?.leadingReplies ?? []),
30
+ ...ids(fold?.trailingReplies ?? []),
31
+ fold?.latestReply.id,
32
+ ];
33
+ expect(new Set(shown).size).toBe(shown.length);
34
+ expect(shown.length).toBe(count);
35
+ });
36
+
37
+ // The shortest thread puts one reply in every role at once. It must still
38
+ // be rendered once, not three times.
39
+ it("shows a lone reply once, as the hero", () => {
40
+ const fold = foldThreadReplies(replies(1));
41
+
42
+ expect(fold?.latestReply.id).toBe("reply-1");
43
+ expect(fold?.leadingReplies).toEqual([]);
44
+ expect(fold?.trailingReplies).toEqual([]);
45
+ });
46
+
47
+ it("folds the middle out of a long thread", () => {
48
+ const fold = foldThreadReplies(replies(7));
49
+
50
+ expect(ids(fold?.leadingReplies ?? [])).toEqual(["reply-1", "reply-2"]);
51
+ expect(ids(fold?.trailingReplies ?? [])).toEqual(["reply-5", "reply-6"]);
52
+ expect(fold?.latestReply.id).toBe("reply-7");
53
+ expect(fold?.hiddenCount).toBe(2);
54
+ });
55
+
56
+ // The gap stands for a run of posts and opens the first of them, so a reader
57
+ // following it lands where the missing stretch starts.
58
+ it("points the gap at the earliest post it hides", () => {
59
+ expect(foldThreadReplies(replies(20))?.firstHiddenReply?.id).toBe(
60
+ "reply-3",
61
+ );
62
+ });
63
+
64
+ it("keeps the window fixed however long the thread runs", () => {
65
+ const fold = foldThreadReplies(replies(50));
66
+ const shown =
67
+ (fold?.leadingReplies.length ?? 0) +
68
+ (fold?.trailingReplies.length ?? 0) +
69
+ 1;
70
+
71
+ expect(shown).toBe(THREAD_LEADING_REPLIES + THREAD_TRAILING_REPLIES);
72
+ expect(fold?.hiddenCount).toBe(50 - shown);
73
+ });
74
+ });
75
+
76
+ describe("getThreadHiddenCount", () => {
77
+ // The site selects its buckets in SQL and they overlap on short threads, so
78
+ // the count has to dedupe rather than subtract two lengths.
79
+ it("counts a reply in two buckets once", () => {
80
+ expect(
81
+ getThreadHiddenCount({
82
+ leadingReplies: [reply(1), reply(2)],
83
+ trailingReplies: [reply(1)],
84
+ latestReply: reply(2),
85
+ totalReplyCount: 2,
86
+ }),
87
+ ).toBe(0);
88
+ });
89
+
90
+ it("counts what a long thread leaves out", () => {
91
+ expect(
92
+ getThreadHiddenCount({
93
+ leadingReplies: [reply(1), reply(2)],
94
+ trailingReplies: [reply(6), reply(7)],
95
+ latestReply: reply(8),
96
+ totalReplyCount: 8,
97
+ }),
98
+ ).toBe(3);
99
+ });
100
+
101
+ // A reply published between the count and the selection would otherwise
102
+ // report a negative number of hidden posts.
103
+ it("never goes below zero", () => {
104
+ expect(
105
+ getThreadHiddenCount({
106
+ leadingReplies: [reply(1), reply(2)],
107
+ trailingReplies: [],
108
+ latestReply: reply(3),
109
+ totalReplyCount: 1,
110
+ }),
111
+ ).toBe(0);
112
+ });
113
+ });