@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/README.md CHANGED
@@ -21,7 +21,7 @@ npm run dev
21
21
  For a traditional server deployment, use the official Docker image:
22
22
 
23
23
  - [`owenyoung/jant`](https://hub.docker.com/r/owenyoung/jant)
24
- - [Docker deployment guide](https://github.com/jant-me/jant/blob/main/docs/deployment-docker.md)
24
+ - [Docker deployment guide](https://jant.me/docs/deployment-docker)
25
25
 
26
26
  ## Tech Stack
27
27
 
@@ -35,16 +35,16 @@ For a traditional server deployment, use the official Docker image:
35
35
 
36
36
  ## Documentation
37
37
 
38
- - [Overview](https://github.com/jant-me/jant/blob/main/docs/overview.md)
39
- - [Getting Started](https://github.com/jant-me/jant/blob/main/docs/getting-started.md)
40
- - [Writing and Organizing Posts](https://github.com/jant-me/jant/blob/main/docs/writing-and-organizing.md)
41
- - [Deploy on Cloudflare](https://github.com/jant-me/jant/blob/main/docs/deployment.md)
42
- - [Deploy with Docker](https://github.com/jant-me/jant/blob/main/docs/deployment-docker.md)
43
- - [Configuration](https://github.com/jant-me/jant/blob/main/docs/configuration.md)
44
- - [Export and Import](https://github.com/jant-me/jant/blob/main/docs/export-and-import.md)
45
- - [Backups and Recovery](https://github.com/jant-me/jant/blob/main/docs/backups.md)
46
- - [Theming](https://github.com/jant-me/jant/blob/main/docs/theming.md)
47
- - [API Reference](https://github.com/jant-me/jant/blob/main/docs/API.md)
38
+ - [Overview](https://jant.me/docs)
39
+ - [Getting Started](https://jant.me/docs/getting-started)
40
+ - [Writing and Organizing Posts](https://jant.me/docs/writing-and-organizing)
41
+ - [Deploy on Cloudflare](https://jant.me/docs/deployment)
42
+ - [Deploy with Docker](https://jant.me/docs/deployment-docker)
43
+ - [Configuration](https://jant.me/docs/configuration)
44
+ - [Export and Import](https://jant.me/docs/export-and-import)
45
+ - [Backups and Recovery](https://jant.me/docs/backups)
46
+ - [Theming](https://jant.me/docs/theming)
47
+ - [API Reference](https://jant.me/docs/api)
48
48
 
49
49
  ## Contributing
50
50
 
@@ -6,14 +6,25 @@
6
6
  * Uses the same S3_* environment variables as media storage. Files with
7
7
  * content-hashed names (JS, fonts) are skipped if already present in the
8
8
  * bucket — they are immutable. Non-hashed files (e.g. CSS in older builds)
9
- * are always uploaded.
9
+ * are always uploaded. An object stored without the compression it should
10
+ * have is re-uploaded even though its key exists, so a change to the
11
+ * compression policy reaches assets whose content never changed.
12
+ *
13
+ * Text assets are uploaded brotli-compressed at maximum quality, with
14
+ * `Content-Encoding: br`. The CDN in front of the bucket compresses at a fixed
15
+ * low quality — measured as brotli q4, which is worse than its own gzip — and
16
+ * offers no way to raise it, so the only route to q11 is to compress here and
17
+ * store the result. A client that cannot take brotli is unaffected: the edge
18
+ * decompresses and re-encodes for it (verified against R2 behind a Cloudflare
19
+ * custom domain: `br` is passed through, `gzip` is re-encoded, `identity` is
20
+ * decompressed, all three byte-identical after decoding).
10
21
  *
11
22
  * Intended CI order: build → upload-assets → deploy container
12
23
  */
13
24
 
14
25
  import { readdir, readFile, stat } from "node:fs/promises";
15
26
  import { join, relative, extname } from "node:path";
16
- import { createReadStream } from "node:fs";
27
+ import { brotliCompressSync, constants } from "node:zlib";
17
28
  import { parseArgs } from "node:util";
18
29
  import { fileURLToPath } from "node:url";
19
30
  import { dirname, resolve } from "node:path";
@@ -41,6 +52,59 @@ function getContentType(filePath) {
41
52
  );
42
53
  }
43
54
 
55
+ /**
56
+ * Extensions worth pre-compressing. Fonts (woff2), images and icons already
57
+ * carry their own compression, and running brotli over them costs build time
58
+ * for nothing — usually for a slightly larger file.
59
+ */
60
+ const COMPRESSIBLE_EXTENSIONS = new Set([
61
+ ".js",
62
+ ".css",
63
+ ".json",
64
+ ".map",
65
+ ".svg",
66
+ ".txt",
67
+ ".html",
68
+ ]);
69
+
70
+ /**
71
+ * Brotli-compress an asset for storage, at a quality only a build can afford.
72
+ *
73
+ * Quality 11 is far too slow to run per request, which is why CDNs compress
74
+ * lower. Here it runs once per file per release, and every visitor gets the
75
+ * result.
76
+ *
77
+ * @param {string} filePath - Source path, used only for its extension.
78
+ * @param {Buffer} body - The file's bytes.
79
+ * @returns {Buffer | null} The compressed body, or `null` when the type is not
80
+ * worth compressing or compression failed to shrink it — upload `body` as-is
81
+ * and omit `Content-Encoding` in that case.
82
+ * @example
83
+ * const packed = compressForUpload("client.css", body);
84
+ * // packed.length < body.length, or null for a .woff2
85
+ */
86
+ export function compressForUpload(filePath, body) {
87
+ if (!COMPRESSIBLE_EXTENSIONS.has(extname(filePath).toLowerCase())) {
88
+ return null;
89
+ }
90
+ const compressed = brotliCompressSync(body, {
91
+ params: {
92
+ [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
93
+ [constants.BROTLI_PARAM_SIZE_HINT]: body.length,
94
+ },
95
+ });
96
+ // Tiny or already-dense files can come back bigger. Storing that would make
97
+ // every request pay for the encoding header and gain nothing.
98
+ return compressed.length < body.length ? compressed : null;
99
+ }
100
+
101
+ /** Human-readable byte count for the upload log. */
102
+ function formatBytes(bytes) {
103
+ return bytes >= 1024 * 1024
104
+ ? `${(bytes / 1024 / 1024).toFixed(1)} MB`
105
+ : `${(bytes / 1024).toFixed(1)} KB`;
106
+ }
107
+
44
108
  /**
45
109
  * Resolve the default source directory: packages/core/dist/client/_assets
46
110
  * relative to this file's location in packages/core/bin/commands/assets/.
@@ -64,8 +128,12 @@ async function walkDir(dir) {
64
128
  }
65
129
 
66
130
  async function loadS3(config) {
67
- const { S3Client, PutObjectCommand, ListObjectsV2Command } =
68
- await import("@aws-sdk/client-s3");
131
+ const {
132
+ S3Client,
133
+ PutObjectCommand,
134
+ ListObjectsV2Command,
135
+ HeadObjectCommand,
136
+ } = await import("@aws-sdk/client-s3");
69
137
  const forcePathStyle = !config.endpoint.includes("amazonaws.com");
70
138
  const client = new S3Client({
71
139
  endpoint: config.endpoint,
@@ -80,6 +148,7 @@ async function loadS3(config) {
80
148
  client,
81
149
  PutObjectCommand,
82
150
  ListObjectsV2Command,
151
+ HeadObjectCommand,
83
152
  bucket: config.bucket,
84
153
  };
85
154
  }
@@ -107,6 +176,53 @@ async function listExistingKeys(s3, prefix) {
107
176
  return keys;
108
177
  }
109
178
 
179
+ /**
180
+ * Of the keys that already exist, the ones stored without the compression they
181
+ * should have.
182
+ *
183
+ * Objects are content-addressed, so a key that exists is assumed to hold the
184
+ * right bytes and is skipped. That assumption breaks when the *encoding*
185
+ * changes rather than the content: every asset uploaded before compression was
186
+ * introduced keeps its key, keeps being skipped, and never picks it up. The
187
+ * CDN then compresses those at its own low quality on every request, which is
188
+ * the whole thing compression was meant to avoid.
189
+ *
190
+ * Only worth asking about types we would compress, and only when compression
191
+ * is on. A file that brotli cannot shrink is stored raw on purpose, so its
192
+ * missing `Content-Encoding` is correct and must not read as stale — otherwise
193
+ * it would be re-uploaded on every run, forever.
194
+ *
195
+ * @param {object} s3 - The client bundle from `loadS3`.
196
+ * @param {Array<{filePath: string, key: string}>} candidates - Local files
197
+ * whose key already exists in the bucket.
198
+ * @returns {Promise<Set<string>>} Keys to upload again.
199
+ * @example
200
+ * const stale = await findStaleEncodingKeys(s3, existing);
201
+ * // Set { "_assets/client-cjk-B7Z0snDu.css" }
202
+ */
203
+ async function findStaleEncodingKeys(s3, candidates) {
204
+ const { client, HeadObjectCommand, bucket } = s3;
205
+ const stale = new Set();
206
+ const CONCURRENCY = 25;
207
+
208
+ for (let i = 0; i < candidates.length; i += CONCURRENCY) {
209
+ await Promise.all(
210
+ candidates.slice(i, i + CONCURRENCY).map(async ({ filePath, key }) => {
211
+ const head = await client.send(
212
+ new HeadObjectCommand({ Bucket: bucket, Key: key }),
213
+ );
214
+ if (head.ContentEncoding === "br") return;
215
+ // Read it only now: the answer is only interesting for the few objects
216
+ // that are missing the encoding.
217
+ const body = await readFile(filePath);
218
+ if (compressForUpload(filePath, body) !== null) stale.add(key);
219
+ }),
220
+ );
221
+ }
222
+
223
+ return stale;
224
+ }
225
+
110
226
  export async function run(argv) {
111
227
  const { values } = parseArgs({
112
228
  args: argv,
@@ -115,6 +231,8 @@ export async function run(argv) {
115
231
  prefix: { type: "string", default: "_assets" },
116
232
  "source-dir": { type: "string" },
117
233
  "dry-run": { type: "boolean", default: false },
234
+ force: { type: "boolean", default: false },
235
+ "no-compress": { type: "boolean", default: false },
118
236
  },
119
237
  });
120
238
 
@@ -135,6 +253,12 @@ export async function run(argv) {
135
253
  console.log(
136
254
  " --dry-run Print what would be uploaded without uploading",
137
255
  );
256
+ console.log(
257
+ " --force Re-upload every key, including ones already compressed",
258
+ );
259
+ console.log(
260
+ " --no-compress Upload raw bytes instead of brotli-compressed",
261
+ );
138
262
  process.exit(0);
139
263
  }
140
264
 
@@ -171,6 +295,8 @@ export async function run(argv) {
171
295
  const sourceDir = values["source-dir"] ?? resolveDefaultSourceDir();
172
296
  const prefix = values.prefix.replace(/^\/+|\/+$/g, "");
173
297
  const dryRun = values["dry-run"];
298
+ const force = values.force;
299
+ const compress = !values["no-compress"];
174
300
 
175
301
  // Verify source directory exists
176
302
  try {
@@ -181,10 +307,12 @@ export async function run(argv) {
181
307
  process.exit(1);
182
308
  }
183
309
 
184
- console.log(`Source: ${sourceDir}`);
185
- console.log(`Bucket: ${bucket}`);
186
- console.log(`Prefix: ${prefix}`);
187
- if (dryRun) console.log("Dry run: no files will be uploaded");
310
+ console.log(`Source: ${sourceDir}`);
311
+ console.log(`Bucket: ${bucket}`);
312
+ console.log(`Prefix: ${prefix}`);
313
+ console.log(`Encoding: ${compress ? "brotli (quality 11)" : "raw"}`);
314
+ if (force) console.log("Force: re-uploading keys that already exist");
315
+ if (dryRun) console.log("Dry run: no files will be uploaded");
188
316
  console.log("");
189
317
 
190
318
  const s3 = await loadS3({
@@ -205,19 +333,57 @@ export async function run(argv) {
205
333
  let skipped = 0;
206
334
 
207
335
  const toUpload = [];
336
+ const alreadyThere = [];
208
337
  for (const filePath of files) {
209
338
  const relPath = relative(sourceDir, filePath).replace(/\\/g, "/");
210
339
  const key = prefix ? `${prefix}/${relPath}` : relPath;
211
- if (existingKeys.has(key)) {
212
- skipped++;
340
+ if (existingKeys.has(key) && !force) {
341
+ alreadyThere.push({ filePath, key });
213
342
  } else {
214
343
  toUpload.push({ filePath, key });
215
344
  }
216
345
  }
217
346
 
347
+ // A key that exists is normally left alone, but that skips objects stored
348
+ // before compression was introduced — they would keep their old encoding
349
+ // forever. Re-upload those, and only those.
350
+ let restored = 0;
351
+ if (compress && alreadyThere.length > 0) {
352
+ process.stdout.write("Checking stored encodings... ");
353
+ const stale = await findStaleEncodingKeys(s3, alreadyThere);
354
+ for (const entry of alreadyThere) {
355
+ if (stale.has(entry.key)) toUpload.push(entry);
356
+ else skipped++;
357
+ }
358
+ restored = stale.size;
359
+ console.log(
360
+ stale.size === 0
361
+ ? "all compressed"
362
+ : `${stale.size} stored uncompressed, re-uploading`,
363
+ );
364
+ } else {
365
+ skipped += alreadyThere.length;
366
+ }
367
+
368
+ // Bytes as stored, against bytes as built — what the compression bought.
369
+ let rawTotal = 0;
370
+ let storedTotal = 0;
371
+
372
+ /** Read a file and decide how it should be stored. */
373
+ async function prepare(filePath) {
374
+ const body = await readFile(filePath);
375
+ const packed = compress ? compressForUpload(filePath, body) : null;
376
+ rawTotal += body.length;
377
+ storedTotal += (packed ?? body).length;
378
+ return { body: packed ?? body, encoded: packed !== null };
379
+ }
380
+
218
381
  if (dryRun) {
219
- for (const { key } of toUpload) {
220
- console.log(` [dry-run] upload ${key}`);
382
+ for (const { filePath, key } of toUpload) {
383
+ const { body, encoded } = await prepare(filePath);
384
+ console.log(
385
+ ` [dry-run] upload ${key} (${formatBytes(body.length)}${encoded ? ", br" : ""})`,
386
+ );
221
387
  }
222
388
  uploaded = toUpload.length;
223
389
  } else {
@@ -226,16 +392,17 @@ export async function run(argv) {
226
392
  const batch = toUpload.slice(i, i + CONCURRENCY);
227
393
  await Promise.all(
228
394
  batch.map(async ({ filePath, key }) => {
229
- const body = await readFile(filePath);
395
+ const { body, encoded } = await prepare(filePath);
230
396
  const command = new s3.PutObjectCommand({
231
397
  Bucket: s3.bucket,
232
398
  Key: key,
233
399
  Body: body,
234
400
  ContentType: getContentType(filePath),
235
401
  CacheControl: "public, max-age=31536000, immutable",
402
+ ...(encoded ? { ContentEncoding: "br" } : {}),
236
403
  });
237
404
  await s3.client.send(command);
238
- process.stdout.write(` uploaded ${key}\n`);
405
+ process.stdout.write(` uploaded ${key}${encoded ? " (br)" : ""}\n`);
239
406
  uploaded++;
240
407
  }),
241
408
  );
@@ -243,5 +410,15 @@ export async function run(argv) {
243
410
  }
244
411
 
245
412
  console.log("");
246
- console.log(`Done. ${uploaded} uploaded, ${skipped} skipped.`);
413
+ console.log(
414
+ `Done. ${uploaded} uploaded, ${skipped} skipped` +
415
+ `${restored > 0 ? `, ${restored} re-uploaded for compression` : ""}.`,
416
+ );
417
+ if (compress && rawTotal > 0) {
418
+ const saved = rawTotal - storedTotal;
419
+ console.log(
420
+ `Stored ${formatBytes(storedTotal)} of ${formatBytes(rawTotal)} built ` +
421
+ `(${formatBytes(saved)} saved, ${((saved / rawTotal) * 100).toFixed(1)}%).`,
422
+ );
423
+ }
247
424
  }
@@ -1290,10 +1290,12 @@ async function apiCall(method, path, apiUrl, token, body) {
1290
1290
  cause === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" ||
1291
1291
  cause?.includes("certificate")
1292
1292
  ) {
1293
- console.error(`\nSSL certificate error connecting to ${apiUrl}`);
1294
- console.error("If using a local/self-signed certificate, run with:");
1295
- console.error(" NODE_TLS_REJECT_UNAUTHORIZED=0 jant import-site ...");
1296
- console.error("Or use: node --use-system-ca bin/jant.js import-site ...");
1293
+ console.error(`\nCertificate error connecting to ${apiUrl}`);
1294
+ console.error(
1295
+ "The certificate is not signed by a CA that Node trusts. For a local",
1296
+ );
1297
+ console.error("or self-signed target, trust this machine's CA store:");
1298
+ console.error(" NODE_OPTIONS=--use-system-ca jant site import ...");
1297
1299
  process.exit(1);
1298
1300
  }
1299
1301
  throw new Error(
@@ -1462,6 +1464,15 @@ function createRemoteTarget(apiUrl, token) {
1462
1464
  toRemotePostPayload(data),
1463
1465
  );
1464
1466
  },
1467
+ async linkTranslation(postId, otherPostId) {
1468
+ return apiCall(
1469
+ "POST",
1470
+ `/api/posts/${postId}/translations`,
1471
+ apiUrl,
1472
+ token,
1473
+ { postId: otherPostId },
1474
+ );
1475
+ },
1465
1476
  async createAlias(path, targetSlug) {
1466
1477
  return apiCall("POST", "/api/custom-urls", apiUrl, token, {
1467
1478
  path,
@@ -1743,6 +1754,13 @@ function getImportedPostStatus(frontMatter) {
1743
1754
  function shouldImportReplyQuietly(rootFrontMatter, replyFrontMatter) {
1744
1755
  if (getImportedPostStatus(replyFrontMatter) !== "published") return false;
1745
1756
 
1757
+ // Exports since quiet_reply became a stored column say so explicitly.
1758
+ if (typeof replyFrontMatter.quiet_reply === "boolean") {
1759
+ return replyFrontMatter.quiet_reply;
1760
+ }
1761
+
1762
+ // Older bundles: a reply published after the root's recorded last activity
1763
+ // can only have gotten there by skipping the bump, so it was quiet.
1746
1764
  const rootLastActivityAt = getImportedRootLastActivityAt(rootFrontMatter);
1747
1765
  const replyPublishedAt = parseImportTimestamp(replyFrontMatter.date);
1748
1766
  return (
@@ -1817,6 +1835,12 @@ function buildPostPayloadFromBundle(bundle, options) {
1817
1835
  : undefined,
1818
1836
  rating:
1819
1837
  typeof frontMatter.rating === "number" ? frontMatter.rating : undefined,
1838
+ // Roots only: replies take the root's language from the service, and a
1839
+ // reply bundle has no language of its own in the export.
1840
+ language:
1841
+ !options.replyToId && typeof frontMatter.language === "string"
1842
+ ? frontMatter.language
1843
+ : undefined,
1820
1844
  quietReply: options.quietReply ? true : undefined,
1821
1845
  };
1822
1846
 
@@ -1954,6 +1978,8 @@ export async function run(argv) {
1954
1978
  const apiUrl = url.replace(/\/$/, "");
1955
1979
  const skipRemoteMedia = values["skip-remote-media"];
1956
1980
  const target = dryRun ? null : createRemoteTarget(apiUrl, token);
1981
+ /** Exported translation-group ID → IDs of the posts created for it. */
1982
+ const translationGroups = new Map();
1957
1983
 
1958
1984
  // 1. Read source — directory or ZIP
1959
1985
  const inputPath = resolve(process.cwd(), values.path);
@@ -2318,6 +2344,18 @@ export async function run(argv) {
2318
2344
  try {
2319
2345
  post = await target.createPost(postData);
2320
2346
  postsCreated++;
2347
+ // Translation groups are rebuilt after every post exists: the group ID
2348
+ // in the export is opaque and its members can appear in any order, so
2349
+ // there is nothing to link to until the whole run is done.
2350
+ const groupKey =
2351
+ typeof rootFm.translation_group === "string"
2352
+ ? rootFm.translation_group.trim()
2353
+ : "";
2354
+ if (groupKey && post?.id) {
2355
+ const members = translationGroups.get(groupKey) ?? [];
2356
+ members.push(post.id);
2357
+ translationGroups.set(groupKey, members);
2358
+ }
2321
2359
  const replyInfo =
2322
2360
  rootBundle.children.length > 0
2323
2361
  ? ` (+${rootBundle.children.length} replies)`
@@ -2476,6 +2514,28 @@ export async function run(argv) {
2476
2514
  }
2477
2515
  }
2478
2516
 
2517
+ // Rebuild translation groups. Each member is linked to the first one, so a
2518
+ // group of N takes N-1 calls and never asks the server to merge two groups
2519
+ // — which it refuses, on purpose.
2520
+ let translationsLinked = 0;
2521
+ for (const [groupKey, members] of translationGroups) {
2522
+ if (members.length < 2) continue;
2523
+ const [anchor, ...rest] = members;
2524
+ for (const memberId of rest) {
2525
+ try {
2526
+ await target.linkTranslation(anchor, memberId);
2527
+ translationsLinked++;
2528
+ } catch (err) {
2529
+ // A clash here means the export itself held two posts in the same
2530
+ // language for one group. Say so and keep going: the posts are
2531
+ // imported either way, they are just not linked.
2532
+ console.warn(
2533
+ `Warning: could not link a translation in group "${groupKey}": ${err.message}`,
2534
+ );
2535
+ }
2536
+ }
2537
+ }
2538
+
2479
2539
  await target?.close();
2480
2540
 
2481
2541
  // 5. Summary
@@ -2484,6 +2544,9 @@ export async function run(argv) {
2484
2544
  console.log(` Posts created: ${postsCreated}`);
2485
2545
  console.log(` Replies created: ${repliesCreated}`);
2486
2546
  console.log(` Media uploaded: ${mediaUploaded}`);
2547
+ if (translationsLinked > 0) {
2548
+ console.log(` Translations linked: ${translationsLinked}`);
2549
+ }
2487
2550
  if (aliasesCreated > 0) {
2488
2551
  console.log(` Aliases created: ${aliasesCreated}`);
2489
2552
  }
@@ -7,7 +7,7 @@ import {
7
7
  bootstrapCliRuntime,
8
8
  getCliRuntimeLabel,
9
9
  } from "../lib/runtime-target.js";
10
- import { resolveCliSite } from "../lib/site-selection.js";
10
+ import { parseCliSiteSelector, resolveCliSite } from "../lib/site-selection.js";
11
11
 
12
12
  export async function run(argv) {
13
13
  const { values } = parseArgs({
@@ -29,7 +29,9 @@ export async function run(argv) {
29
29
  });
30
30
 
31
31
  if (values.help) {
32
- console.log("Usage: jant reset-password [--local | --remote | --node]");
32
+ console.log(
33
+ "Usage: jant reset-password [--local | --remote | --node] [--site <key|id> | --host <host> | --url <url>]",
34
+ );
33
35
  console.log("");
34
36
  console.log("Generate a password reset token (expires in 15 minutes).");
35
37
  console.log("");
@@ -37,7 +39,7 @@ export async function run(argv) {
37
39
  console.log(" --local Force local D1 instead of DATABASE_URL");
38
40
  console.log(" --remote Run against remote D1 database (default: local)");
39
41
  console.log(" --node Force Node runtime even if DATABASE_URL is unset");
40
- console.log(" --site Target site id");
42
+ console.log(" --site Target site key or id");
41
43
  console.log(" --host Target site host");
42
44
  console.log(" --url Target site URL");
43
45
  console.log(" --path-prefix Path prefix used with --host");
@@ -53,10 +55,20 @@ export async function run(argv) {
53
55
  "If DATABASE_URL or DATA_DIR is then set and no runtime flag is passed,",
54
56
  );
55
57
  console.log("this command uses the Node database runtime.");
58
+ console.log("");
59
+ console.log(
60
+ "With SITE_RESOLUTION_MODE=host-based, pass one of --site, --host, or --url.",
61
+ );
56
62
  process.exit(0);
57
63
  }
58
64
 
59
65
  const { runtime } = bootstrapCliRuntime(values);
66
+ const siteFlags = {
67
+ host: values.host,
68
+ pathPrefix: values["path-prefix"],
69
+ site: values.site,
70
+ url: values.url,
71
+ };
60
72
 
61
73
  const token = randomBytes(32).toString("hex");
62
74
  const hash = createHash("sha256").update(token).digest("hex");
@@ -65,11 +77,15 @@ export async function run(argv) {
65
77
  const timestamp = Math.floor(Date.now() / 1000);
66
78
 
67
79
  if (runtime === "node") {
80
+ const siteSelector = parseCliSiteSelector(siteFlags);
68
81
  const { createNodeCliRuntime } = await loadNodeRuntime();
69
82
  const nodeDatabase = await openNodeDatabase(process.env);
70
83
 
71
84
  try {
72
- const nodeRuntime = await createNodeCliRuntime(nodeDatabase.bindings);
85
+ const nodeRuntime = await createNodeCliRuntime(
86
+ nodeDatabase.bindings,
87
+ siteSelector,
88
+ );
73
89
  await nodeRuntime.services.settings.set("PASSWORD_RESET_TOKEN", value);
74
90
  } finally {
75
91
  await nodeDatabase.close();
@@ -95,10 +111,7 @@ export async function run(argv) {
95
111
  },
96
112
  {
97
113
  env: process.env,
98
- host: values.host,
99
- pathPrefix: values["path-prefix"],
100
- site: values.site,
101
- url: values.url,
114
+ ...siteFlags,
102
115
  },
103
116
  );
104
117
 
@@ -215,7 +215,7 @@ export async function run(argv) {
215
215
  console.log(
216
216
  " --output, -o Output directory or .zip file (default: jant-site-snapshot)",
217
217
  );
218
- console.log(" --site Target site id");
218
+ console.log(" --site Target site key or id");
219
219
  console.log(" --host Target site host");
220
220
  console.log(" --url Target site URL");
221
221
  console.log(" --path-prefix Path prefix used with --host");
@@ -204,7 +204,7 @@ export async function run(argv) {
204
204
  console.log(
205
205
  " --replace Replace the current content scope before importing",
206
206
  );
207
- console.log(" --site Target site id");
207
+ console.log(" --site Target site key or id");
208
208
  console.log(" --host Target site host");
209
209
  console.log(" --url Target site URL");
210
210
  console.log(" --path-prefix Path prefix used with --host");
@@ -1,4 +1,5 @@
1
1
  import { runLocalWrangler } from "./wrangler-cli.js";
2
+ import { extractWranglerJson } from "./wrangler-json.js";
2
3
 
3
4
  const DEFAULT_RETRY_ATTEMPTS = 4;
4
5
  const DEFAULT_RETRY_DELAY_MS = 500;
@@ -35,7 +36,7 @@ export function parseWranglerError(output) {
35
36
  }
36
37
 
37
38
  try {
38
- const parsed = JSON.parse(output.trim());
39
+ const parsed = JSON.parse(extractWranglerJson(output).trim());
39
40
  const error = Array.isArray(parsed) ? parsed[0]?.error : parsed?.error;
40
41
  if (!error?.text) {
41
42
  return undefined;
@@ -75,18 +76,6 @@ export function isRetryableWranglerD1Failure(output, error) {
75
76
  ].some((fragment) => combined.includes(fragment));
76
77
  }
77
78
 
78
- /**
79
- * Strip non-JSON preamble that Wrangler sometimes writes to stdout
80
- * (e.g. "Proxy environment variables detected…") before the actual JSON payload.
81
- */
82
- function extractJson(raw) {
83
- const idx = raw.indexOf("[");
84
- const idx2 = raw.indexOf("{");
85
- if (idx === -1 && idx2 === -1) return raw;
86
- const start = idx === -1 ? idx2 : idx2 === -1 ? idx : Math.min(idx, idx2);
87
- return raw.slice(start);
88
- }
89
-
90
79
  function getWranglerExecutionOptions(options) {
91
80
  const executionOptions = { ...options };
92
81
  for (const key of [
@@ -153,7 +142,7 @@ export function executeD1(sql, runtime, options = {}) {
153
142
 
154
143
  if (options.quiet) {
155
144
  const output = runWrangler([...args, "--json"], options);
156
- const parsed = JSON.parse(extractJson(output));
145
+ const parsed = JSON.parse(extractWranglerJson(output));
157
146
  const statements = Array.isArray(parsed) ? parsed : [parsed];
158
147
 
159
148
  for (const statement of statements) {
@@ -183,7 +172,7 @@ export function queryD1(sql, runtime, options = {}) {
183
172
  ),
184
173
  options,
185
174
  );
186
- const parsed = JSON.parse(extractJson(output));
175
+ const parsed = JSON.parse(extractWranglerJson(output));
187
176
  const statement = Array.isArray(parsed) ? parsed[0] : parsed;
188
177
 
189
178
  if (statement?.error?.text) {
@@ -208,7 +197,7 @@ export function executeD1File(filePath, runtime, options = {}) {
208
197
 
209
198
  if (options.quiet) {
210
199
  const output = runWrangler([...args, "--json"], options);
211
- const parsed = JSON.parse(extractJson(output));
200
+ const parsed = JSON.parse(extractWranglerJson(output));
212
201
  const statements = Array.isArray(parsed) ? parsed : [parsed];
213
202
 
214
203
  for (const statement of statements) {