@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
@@ -27,8 +27,9 @@ import {
27
27
  getDefaultJantAppleTouchIconBytes,
28
28
  getDefaultJantFaviconIcoBytes,
29
29
  } from "../lib/jant-branding.js";
30
+ import { getRootActivityAt } from "../db/thread-activity.js";
30
31
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
31
- import { extractBodyText } from "../lib/summary.js";
32
+ import { extractBodyText, extractTimelineSummary } from "../lib/summary.js";
32
33
  import { getMediaUrl, getPublicUrlForProvider } from "../lib/image.js";
33
34
  import { render as renderMarkdown } from "../lib/markdown.js";
34
35
  import { formatRelativeAge, toISOString } from "../lib/time.js";
@@ -84,6 +85,7 @@ import type { StorageDriver } from "../lib/storage.js";
84
85
  import { base64ToUint8Array } from "../lib/favicon.js";
85
86
  import {
86
87
  SYSTEM_NAV_KEYS,
88
+ isFeedNavKey,
87
89
  type Collection,
88
90
  type Media,
89
91
  type NavItem,
@@ -109,6 +111,10 @@ export interface SiteConfig {
109
111
  siteUrl: string;
110
112
  siteDescription: string;
111
113
  siteLanguage: string;
114
+ /** Whether the source site serves per-language browsing views. */
115
+ multilingualEnabled: boolean;
116
+ /** Non-primary content languages, in switcher order. */
117
+ additionalLanguages: readonly string[];
112
118
  showJantBrandingOnHome: boolean;
113
119
  /** Whether anonymous JSON reads are enabled on the source Jant site. */
114
120
  publicApiEnabled: boolean;
@@ -116,6 +122,8 @@ export interface SiteConfig {
116
122
  rssFeedsEnabled: boolean;
117
123
  /** "latest" or "featured" — drives the default RSS nav link in the exported site. */
118
124
  mainRssFeed: string;
125
+ /** "list" or "grid" — the layout the exported archive page opens with. */
126
+ archiveDefaultLayout: string;
119
127
  siteFooter: string;
120
128
  showHeaderAvatar: boolean;
121
129
  siteAvatarUrl: string;
@@ -136,7 +144,13 @@ export interface SiteConfig {
136
144
  sitePathPrefix?: string;
137
145
  navItems: Pick<
138
146
  NavItem,
139
- "type" | "systemKey" | "label" | "url" | "position" | "placement"
147
+ | "type"
148
+ | "systemKey"
149
+ | "label"
150
+ | "targetTitle"
151
+ | "url"
152
+ | "position"
153
+ | "placement"
140
154
  >[];
141
155
  /** Items per page for Hugo pagination — kept in sync with the main site's PAGE_SIZE. */
142
156
  pageSize: number;
@@ -254,7 +268,10 @@ export function createExportService(
254
268
  async generateHugoFiles() {
255
269
  const collectionDirectoryDataPromise =
256
270
  typeof services.collections.listDirectoryData === "function"
257
- ? services.collections.listDirectoryData()
271
+ ? // The export builds a public static site, so it sees what a
272
+ // signed-out reader sees. Language narrowing is not applied: the
273
+ // export walks every language itself.
274
+ services.collections.listDirectoryData({ isAuthenticated: false })
258
275
  : Promise.resolve(null);
259
276
 
260
277
  // 1. Query all data
@@ -303,14 +320,30 @@ export function createExportService(
303
320
  allPosts,
304
321
  collectionsByRoot,
305
322
  );
306
- const exportedCollectionDirectoryItems =
307
- buildExportedCollectionDirectoryItems(
308
- collectionDirectoryData?.items ??
309
- allCollections.map((collection) => ({
310
- id: collection.id,
323
+ // Smart collections are left out of a static export. Their membership is
324
+ // a query, and the exported site has no database to run it against, so
325
+ // the honest options are "omit" or "freeze today's matches under a name
326
+ // that promises to keep updating". Round-tripping them is out of scope
327
+ // (see the smart collections design notes); omitting is what that
328
+ // decision means here.
329
+ const exportableDirectoryItems: ExportCollectionDirectorySourceItem[] =
330
+ collectionDirectoryData?.items
331
+ ? collectionDirectoryData.items
332
+ .filter((item) => item.type !== "smart_collection")
333
+ .map((item) => ({
334
+ type: item.type as "collection" | "divider" | "link",
335
+ label: item.label,
336
+ url: item.url,
337
+ description: item.description,
338
+ collection: item.collection,
339
+ }))
340
+ : allCollections.map((collection) => ({
311
341
  type: "collection" as const,
312
342
  collection,
313
- })),
343
+ }));
344
+ const exportedCollectionDirectoryItems =
345
+ buildExportedCollectionDirectoryItems(
346
+ exportableDirectoryItems,
314
347
  collectionSlugMap,
315
348
  collectionMetrics,
316
349
  );
@@ -396,13 +429,28 @@ export function createExportService(
396
429
  const usedSlugs = new Set<string>();
397
430
  for (const s of slugMap.values()) usedSlugs.add(s);
398
431
  for (const s of collectionSlugMap.values()) usedSlugs.add(s);
399
- if (!usedSlugs.has("featured")) {
432
+ const hasFeaturedSection = !usedSlugs.has("featured");
433
+ if (hasFeaturedSection) {
400
434
  exportFiles.push({
401
435
  path: "content/featured/_index.md",
402
436
  content: await buildFeaturedSection(siteConfig.rssFeedsEnabled),
403
437
  });
404
438
  }
405
439
 
440
+ // Feed subscribers are the one audience the export can lose silently:
441
+ // their reader polls a `/feed` address that this site does not serve.
442
+ // With feeds off there is nothing to redirect to, and the live site had
443
+ // no feeds to have been subscribed to either.
444
+ if (siteConfig.rssFeedsEnabled) {
445
+ exportFiles.push({
446
+ path: "static/_redirects",
447
+ content: buildFeedRedirects(
448
+ siteConfig.mainRssFeed,
449
+ hasFeaturedSection,
450
+ ),
451
+ });
452
+ }
453
+
406
454
  // Single data file consumed by templates via `hugo.Data.jant`. The
407
455
  // collection directory lives on the same object as `directory` so
408
456
  // everything Jant owns round-trips in one place.
@@ -545,7 +593,7 @@ export function createExportService(
545
593
 
546
594
  exportFiles.push({
547
595
  path: "README.md",
548
- content: buildReadme(siteConfig.siteName),
596
+ content: buildReadme(siteConfig, hasFeaturedSection),
549
597
  });
550
598
  exportFiles.push({
551
599
  path: ".gitignore",
@@ -675,6 +723,25 @@ function buildExportedCollectionEntriesForThread(
675
723
  return resolved;
676
724
  }
677
725
 
726
+ /**
727
+ * Whether the site's timeline cuts this post's text short.
728
+ *
729
+ * Shares the boundary with the served feed rather than approximating it: Hugo
730
+ * has its own summary rule and would cut somewhere else, so the answer is
731
+ * computed here and carried as front matter.
732
+ *
733
+ * @param post - The post being exported
734
+ * @returns true when the timeline shows less than the post's page
735
+ * @example
736
+ * isPostTruncated(longArticle); // true
737
+ */
738
+ function isPostTruncated(post: Post): boolean {
739
+ return (
740
+ extractTimelineSummary(post.body, !!post.title, { namespace: post.id })
741
+ ?.hasMore === true
742
+ );
743
+ }
744
+
678
745
  function collectionEntriesToRefs(
679
746
  entries: readonly ExportedCollectionEntry[],
680
747
  ): HugoCollectionRef[] {
@@ -885,7 +952,13 @@ async function buildThreadBundle(
885
952
  format: root.format,
886
953
  status: root.status,
887
954
  visibility: root.visibility,
955
+ // Language is Thread-uniform, so only the root emits it; the importer
956
+ // re-derives replies from their root. `translation_group` is an opaque
957
+ // shared key — importing the whole site keeps translated posts linked.
958
+ language: root.language ?? undefined,
959
+ translation_group: root.translationGroupId ?? undefined,
888
960
  summary_text: getArchiveSummaryText(root) ?? undefined,
961
+ truncated: isPostTruncated(root) || undefined,
889
962
  link_url: root.format === "link" && root.url ? root.url : undefined,
890
963
  source_name: root.format === "quote" && root.title ? root.title : undefined,
891
964
  source_url: root.format === "quote" && root.url ? root.url : undefined,
@@ -968,12 +1041,14 @@ async function buildThreadBundle(
968
1041
  status: reply.status,
969
1042
  visibility: reply.visibility,
970
1043
  summary_text: getArchiveSummaryText(reply) ?? undefined,
1044
+ truncated: isPostTruncated(reply) || undefined,
971
1045
  link_url: reply.format === "link" && reply.url ? reply.url : undefined,
972
1046
  source_name:
973
1047
  reply.format === "quote" && reply.title ? reply.title : undefined,
974
1048
  source_url: reply.format === "quote" && reply.url ? reply.url : undefined,
975
1049
  quote_text: reply.quoteText ?? undefined,
976
1050
  rating: reply.rating ?? undefined,
1051
+ quiet_reply: reply.quietReply ? true : undefined,
977
1052
  featured_at:
978
1053
  reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
979
1054
  pinned_at:
@@ -1326,11 +1401,7 @@ function buildExportedCollectionMetrics(
1326
1401
  continue;
1327
1402
  }
1328
1403
 
1329
- const activityAt =
1330
- post.lastActivityAt ??
1331
- post.publishedAt ??
1332
- post.updatedAt ??
1333
- post.createdAt;
1404
+ const activityAt = getRootActivityAt(post);
1334
1405
  const threadCollections = collectionsByThread.get(post.id) ?? [];
1335
1406
 
1336
1407
  for (const collection of threadCollections) {
@@ -1358,13 +1429,21 @@ function buildExportedCollectionMetrics(
1358
1429
  // Nav item resolution
1359
1430
  // ---------------------------------------------------------------------------
1360
1431
 
1432
+ /**
1433
+ * Label to bake into the static site.
1434
+ *
1435
+ * Mirrors `getNavItemDisplayLabel`: a stored label is the author's own words
1436
+ * and wins, an empty one means the item follows what it points at. The export
1437
+ * has no i18n runtime, so built-in destinations fall back to their English
1438
+ * default rather than a translated one.
1439
+ */
1361
1440
  function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
1362
1441
  if (item.label) return item.label;
1363
1442
  if (item.systemKey) {
1364
1443
  const definition = SYSTEM_NAV_KEYS[item.systemKey as SystemNavKey];
1365
1444
  if (definition) return definition.defaultLabel;
1366
1445
  }
1367
- return item.label;
1446
+ return item.targetTitle?.trim() || item.label;
1368
1447
  }
1369
1448
 
1370
1449
  /**
@@ -1391,7 +1470,10 @@ function resolveNavItemUrl(
1391
1470
  }
1392
1471
  if (item.systemKey === "collections") return "/collections/";
1393
1472
  if (item.systemKey === "archive") return "/archive/";
1394
- if (item.systemKey === "rss") {
1473
+ // `subscribe` addresses a Jant route, and the exported site has no page at
1474
+ // it. Rather than exporting a nav entry that 404s, it resolves to the feed
1475
+ // that page would have recommended — the same file `rss` points at.
1476
+ if (item.systemKey === "rss" || item.systemKey === "subscribe") {
1395
1477
  return mainRssFeed === "featured" ? "/featured/index.xml" : "/index.xml";
1396
1478
  }
1397
1479
  return item.url;
@@ -1470,6 +1552,7 @@ function buildHugoToml(config: SiteConfig): string {
1470
1552
  "[params]",
1471
1553
  ` description = "${escapeTomlString(config.siteDescription)}"`,
1472
1554
  ` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1555
+ ` archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
1473
1556
  ` public_api_enabled = ${config.publicApiEnabled}`,
1474
1557
  ` rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1475
1558
  ` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
@@ -1512,7 +1595,10 @@ function buildJantDataToml(
1512
1595
  `site_name = "${escapeTomlString(config.siteName)}"`,
1513
1596
  `site_description = "${escapeTomlString(config.siteDescription)}"`,
1514
1597
  `site_language = "${escapeTomlString(config.siteLanguage)}"`,
1598
+ `multilingual_enabled = ${config.multilingualEnabled}`,
1599
+ `additional_languages = "${escapeTomlString(config.additionalLanguages.join(","))}"`,
1515
1600
  `main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1601
+ `archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
1516
1602
  `public_api_enabled = ${config.publicApiEnabled}`,
1517
1603
  `rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1518
1604
  `show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
@@ -1552,10 +1638,12 @@ function buildJantDataToml(
1552
1638
  // `settings` is authenticated-only and has no corresponding page in the
1553
1639
  // static Hugo site — drop it at export time so it never shows up in nav.
1554
1640
  if (item.systemKey === "settings") continue;
1641
+ // Both feed entries resolve to a feed file, so with feeds off both would
1642
+ // export a link to something that was never written.
1555
1643
  if (
1556
1644
  !config.rssFeedsEnabled &&
1557
1645
  item.type === "system" &&
1558
- item.systemKey === "rss"
1646
+ isFeedNavKey(item.systemKey)
1559
1647
  ) {
1560
1648
  continue;
1561
1649
  }
@@ -1616,9 +1704,95 @@ function buildJantDataToml(
1616
1704
  }
1617
1705
 
1618
1706
  // ---------------------------------------------------------------------------
1619
- // README + .gitignore
1707
+ // README + .gitignore + _redirects
1620
1708
  // ---------------------------------------------------------------------------
1621
1709
 
1710
+ /**
1711
+ * Build the `static/_redirects` file that keeps existing feed subscribers
1712
+ * working after the site moves to this export.
1713
+ *
1714
+ * Jant serves its feeds under `/feed` (`/feed`, `/latest/feed`,
1715
+ * `/featured/feed`, `/archive/feed`, `/{collection}/feed`); Hugo serves the
1716
+ * same documents as `index.xml` inside each section. The two namespaces do
1717
+ * not overlap, so every subscriber's saved address 404s the moment the
1718
+ * exported site goes up.
1719
+ *
1720
+ * Hugo's `aliases:` mechanism cannot cover this the way it covers post URLs:
1721
+ * an alias page is a meta-refresh plus a script, and feed readers fetch and
1722
+ * parse XML without running either. Only an HTTP redirect reaches them, which
1723
+ * is host configuration rather than Hugo output — so the export emits the one
1724
+ * format two common static hosts read verbatim (Cloudflare Pages and Netlify).
1725
+ * Hosts that do not read `_redirects` ignore the file; the README carries the
1726
+ * rule for them to translate.
1727
+ *
1728
+ * @param mainRssFeed - The site's main feed setting, "latest" or "featured".
1729
+ * @param hasFeaturedSection - Whether the export emitted a featured section.
1730
+ * It is skipped when a post or collection already owns the `featured` slug,
1731
+ * and pointing a redirect at a feed that was never built would turn a live
1732
+ * subscription into a 404 rather than leaving it merely stale.
1733
+ * @returns The `_redirects` file body.
1734
+ * @example
1735
+ * buildFeedRedirects("latest", true); // "# ...\n/feed /index.xml 301\n..."
1736
+ */
1737
+ function buildFeedRedirects(
1738
+ mainRssFeed: string,
1739
+ hasFeaturedSection: boolean,
1740
+ ): string {
1741
+ const LATEST = "/index.xml";
1742
+ const FEATURED = "/featured/index.xml";
1743
+ // `/feed` follows the site's main-feed setting, mirroring `renderMainFeed`.
1744
+ // Without a featured section there is nothing to point it at, so it falls
1745
+ // back to the latest feed: a wider feed than the reader subscribed to, but
1746
+ // a live one.
1747
+ const main =
1748
+ mainRssFeed === "featured" && hasFeaturedSection ? FEATURED : LATEST;
1749
+ const featured = hasFeaturedSection ? FEATURED : LATEST;
1750
+
1751
+ const rules: [string, string][] = [
1752
+ ["/feed", main],
1753
+ ["/latest/feed", LATEST],
1754
+ ["/featured/feed", featured],
1755
+ ["/archive/feed", "/archive/index.xml"],
1756
+ ];
1757
+ const legacy: [string, string][] = [
1758
+ ["/feed/latest", LATEST],
1759
+ ["/feed/all", LATEST],
1760
+ ["/feed/featured", featured],
1761
+ ["/feed/atom.xml", main],
1762
+ ["/feed/latest/atom.xml", LATEST],
1763
+ ["/feed/all/atom.xml", LATEST],
1764
+ ["/feed/featured/atom.xml", featured],
1765
+ ];
1766
+
1767
+ // The format only needs whitespace between the columns; padding to the
1768
+ // widest source path keeps the file readable for whoever has to translate
1769
+ // it into another host's redirect syntax.
1770
+ const width = Math.max(
1771
+ ...[...rules, ...legacy].map(([from]) => from.length),
1772
+ "/:slug/feed".length,
1773
+ );
1774
+ const line = ([from, to]: [string, string]) =>
1775
+ `${from.padEnd(width)} ${to} 301`;
1776
+
1777
+ return `# Feed addresses moved in this export.
1778
+ #
1779
+ # Jant served its feeds under /feed; Hugo serves them as index.xml inside each
1780
+ # section. Cloudflare Pages and Netlify read this file and issue the redirects
1781
+ # below, so readers who subscribed to the old addresses keep receiving posts.
1782
+ # Other hosts ignore this file — see README.md for the same rules to translate
1783
+ # into your host's configuration.
1784
+
1785
+ ${rules.map(line).join("\n")}
1786
+
1787
+ # Aliases the live site answered with a 308. They would die at the same moment.
1788
+ ${legacy.map(line).join("\n")}
1789
+
1790
+ # Collection feeds, by slug. Collections are the only root-level sections that
1791
+ # emit Atom, so any other slug 404s here exactly as it did on the live site.
1792
+ ${line(["/:slug/feed", "/:slug/index.xml"])}
1793
+ `;
1794
+ }
1795
+
1622
1796
  function buildGitignore(): string {
1623
1797
  return `# Hugo build output
1624
1798
  public/
@@ -1636,7 +1810,90 @@ Thumbs.db
1636
1810
  `;
1637
1811
  }
1638
1812
 
1639
- function buildReadme(siteName: string): string {
1813
+ /**
1814
+ * Render a two-column Markdown table with padded cells.
1815
+ *
1816
+ * The values differ per site, so building the rows by hand would leave the
1817
+ * committed README ragged for some exports and aligned for others.
1818
+ *
1819
+ * @param headers - The two column headers.
1820
+ * @param rows - Cell pairs; each cell is wrapped in backticks.
1821
+ * @returns The table as Markdown, without a trailing newline.
1822
+ * @example
1823
+ * renderMarkdownTable(["Jant", "This export"], [["/feed", "/index.xml"]]);
1824
+ */
1825
+ function renderMarkdownTable(
1826
+ headers: [string, string],
1827
+ rows: [string, string][],
1828
+ ): string {
1829
+ const cells = rows.map(
1830
+ ([from, to]) => [`\`${from}\``, `\`${to}\``] as [string, string],
1831
+ );
1832
+ const widths: [number, number] = [
1833
+ Math.max(headers[0].length, ...cells.map((r) => r[0].length)),
1834
+ Math.max(headers[1].length, ...cells.map((r) => r[1].length)),
1835
+ ];
1836
+ const line = (a: string, b: string) =>
1837
+ `| ${a.padEnd(widths[0])} | ${b.padEnd(widths[1])} |`;
1838
+ return [
1839
+ line(headers[0], headers[1]),
1840
+ `| ${"-".repeat(widths[0])} | ${"-".repeat(widths[1])} |`,
1841
+ ...cells.map((r) => line(r[0], r[1])),
1842
+ ].join("\n");
1843
+ }
1844
+
1845
+ /**
1846
+ * Build the export's README.
1847
+ *
1848
+ * @param config - The exported site's configuration.
1849
+ * @param hasFeaturedSection - Whether a featured section was emitted, which
1850
+ * decides where the feed redirects point.
1851
+ * @returns Markdown for `README.md` at the root of the export.
1852
+ * @example
1853
+ * buildReadme(config, true); // "# My Site — Hugo Export\n..."
1854
+ */
1855
+ function buildReadme(config: SiteConfig, hasFeaturedSection: boolean): string {
1856
+ const siteName = config.siteName;
1857
+ const featuredFeed = hasFeaturedSection
1858
+ ? "/featured/index.xml"
1859
+ : "/index.xml";
1860
+ const mainFeed =
1861
+ config.mainRssFeed === "featured" ? featuredFeed : "/index.xml";
1862
+ // Nothing to say about feeds on a site that publishes none, and no
1863
+ // `_redirects` file was written for it either.
1864
+ const staticListing = config.rssFeedsEnabled
1865
+ ? "\n _redirects — Feed redirects (see Feeds above)"
1866
+ : "";
1867
+ // The Notes list below says post URLs survive the move. Left alone, that
1868
+ // reads as a promise about every URL, and feed addresses are the exception.
1869
+ const feedNote = config.rssFeedsEnabled
1870
+ ? "\n- Feed addresses are the exception: they move to `index.xml` and stay reachable only through `static/_redirects`. See [Feeds](#feeds)."
1871
+ : "";
1872
+ // Nothing to say about feeds on a site that publishes none, and no
1873
+ // `_redirects` file was written for it either.
1874
+ const feedRows: [string, string][] = [
1875
+ ["/feed", mainFeed],
1876
+ ["/latest/feed", "/index.xml"],
1877
+ ["/featured/feed", featuredFeed],
1878
+ ["/archive/feed", "/archive/index.xml"],
1879
+ ["/{collection}/feed", "/{collection}/index.xml"],
1880
+ ];
1881
+ const feedTable = renderMarkdownTable(["Jant", "This export"], feedRows);
1882
+ const feedsSection = config.rssFeedsEnabled
1883
+ ? `## Feeds
1884
+
1885
+ The feed addresses changed. Jant served them under \`/feed\`; Hugo serves them as \`index.xml\` inside each section:
1886
+
1887
+ ${feedTable}
1888
+
1889
+ A reader who is already subscribed holds one of the old addresses, and a feed reader that gets a 404 stops delivering posts. \`static/_redirects\` maps every old address to its new one with a 301. Cloudflare Pages and Netlify read that file as published; on any other host, translate its rules into that host's redirect configuration before you point the domain here.
1890
+
1891
+ Hugo's \`aliases:\` cannot cover this. An alias page redirects with a meta refresh and a script, and feed readers fetch XML without running either — only an HTTP redirect reaches them.
1892
+
1893
+ The **Subscribe** entry in the site navigation points at \`${mainFeed}\`. The exported site has no \`/subscribe\` page; that page belongs to the Jant runtime.
1894
+
1895
+ `
1896
+ : "";
1640
1897
  return `# ${siteName} — Hugo Export
1641
1898
 
1642
1899
  This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Hugo](https://gohugo.io/).
@@ -1681,7 +1938,7 @@ hugo --minify
1681
1938
 
1682
1939
  The output goes to the \`public/\` directory. Upload it to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, etc.).
1683
1940
 
1684
- ## Project structure
1941
+ ${feedsSection}## Project structure
1685
1942
 
1686
1943
  \`\`\`
1687
1944
  hugo.toml — Site configuration (baseURL, title, theme, params)
@@ -1697,7 +1954,7 @@ content/
1697
1954
  data/
1698
1955
  jant.toml — Nav items, branding, display preferences, ordered collections directory
1699
1956
  themes/jant/ — Bundled Hugo theme (overrideable via layouts/ at the site root)
1700
- static/ — Copy files here to add them to the published site
1957
+ static/ — Copy files here to add them to the published site${staticListing}
1701
1958
  \`\`\`
1702
1959
 
1703
1960
  ## Customizing
@@ -1723,7 +1980,7 @@ Safe to re-run; files already on disk are reused. Anything that fails to downloa
1723
1980
  ## Notes
1724
1981
 
1725
1982
  - Each thread is a Hugo branch bundle. Replies live as nested leaf bundles with \`build.render = "never"\` so they do not produce standalone URLs; they render inside the thread page.
1726
- - \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor.
1983
+ - \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor.${feedNote}
1727
1984
  - Media is emitted under \`static/media/{id}.ext\` and referenced from a flat \`media:\` array on each post. When a storage provider has a configured public URL (R2/S3/local proxy), the exporter links to the provider URL instead of re-bundling the bytes.
1728
1985
  - Posts with \`draft: true\` in front matter are only built when you pass \`--buildDrafts\` to \`hugo\` / \`hugo serve\`.
1729
1986
  `;
@@ -21,6 +21,10 @@ import {
21
21
  createCollectionService,
22
22
  type CollectionService,
23
23
  } from "./collection.js";
24
+ import {
25
+ createSmartCollectionService,
26
+ type SmartCollectionService,
27
+ } from "./smart-collection.js";
24
28
  import { createSearchService, type SearchService } from "./search.js";
25
29
  import { createNavItemService, type NavItemService } from "./navigation.js";
26
30
  import { createAuthService, type AuthService } from "./auth.js";
@@ -36,6 +40,7 @@ import {
36
40
  type SiteProfileService,
37
41
  } from "./site-profile.js";
38
42
  import { createAboutPageService, type AboutPageService } from "./about-page.js";
43
+ import { createLanguageService, type LanguageService } from "./language.js";
39
44
  import {
40
45
  createUploadSessionService,
41
46
  type UploadSessionService,
@@ -57,6 +62,7 @@ export interface Services {
57
62
  media: MediaService;
58
63
  uploads: UploadSessionService;
59
64
  collections: CollectionService;
65
+ smartCollections: SmartCollectionService;
60
66
  search: SearchService;
61
67
  navItems: NavItemService;
62
68
  auth: AuthService;
@@ -66,6 +72,7 @@ export interface Services {
66
72
  siteMembers: SiteMemberService;
67
73
  siteProfile: SiteProfileService;
68
74
  aboutPage: AboutPageService;
75
+ language: LanguageService;
69
76
  githubAppInstallations: GitHubAppInstallationsService;
70
77
  telegram: TelegramService;
71
78
  }
@@ -82,6 +89,8 @@ export function createServices(
82
89
  siteResolutionMode?: "single-site" | "host-based";
83
90
  enforceHostedMediaQuota?: boolean;
84
91
  hostedControlPlane?: HostedControlPlaneClient | null;
92
+ /** Signing key for derived tokens, e.g. the account-deletion CSRF token. */
93
+ authSecret?: string;
85
94
  },
86
95
  ): Services {
87
96
  const databaseSchema = config?.schema ?? sqliteSchemaBundle;
@@ -89,7 +98,7 @@ export function createServices(
89
98
  const site = createSiteService(db, databaseSchema);
90
99
  const settings = createSettingsService(db, siteId, databaseSchema, dialect);
91
100
  const paths = createPathService(db, siteId, databaseSchema);
92
- const navItems = createNavItemService(db, siteId, databaseSchema);
101
+ const navItems = createNavItemService(db, siteId, databaseSchema, paths);
93
102
  const posts = createPostService(
94
103
  db,
95
104
  {
@@ -100,12 +109,21 @@ export function createServices(
100
109
  paths,
101
110
  databaseSchema,
102
111
  );
112
+ const smartCollections = createSmartCollectionService(
113
+ db,
114
+ siteId,
115
+ paths,
116
+ posts,
117
+ databaseSchema,
118
+ config?.slugIdLength ?? 5,
119
+ );
103
120
  const collections = createCollectionService(
104
121
  db,
105
122
  siteId,
106
123
  paths,
107
124
  databaseSchema,
108
125
  dialect,
126
+ smartCollections,
109
127
  );
110
128
  const media = createMediaService(db, siteId, databaseSchema, dialect, {
111
129
  enforceHostedQuota: config?.enforceHostedMediaQuota ?? false,
@@ -121,6 +139,7 @@ export function createServices(
121
139
  media,
122
140
  uploads: createUploadSessionService(db, siteId, media, databaseSchema),
123
141
  collections,
142
+ smartCollections,
124
143
  search: createSearchService(rawQuery, siteId, dialect),
125
144
  navItems,
126
145
  auth: createAuthService(
@@ -128,13 +147,16 @@ export function createServices(
128
147
  settings,
129
148
  {
130
149
  databaseDialect: dialect,
150
+ authSecret: config?.authSecret,
131
151
  },
132
152
  databaseSchema,
133
153
  ),
134
154
  apiTokens: createApiTokenService(db, siteId, databaseSchema),
135
- bootstrap: createBootstrapService(db, {
155
+ bootstrap: createBootstrapService(db, siteId, {
136
156
  schema: databaseSchema,
157
+ databaseDialect: dialect,
137
158
  bootstrapSite: config?.bootstrapSite,
159
+ siteResolutionMode: config?.siteResolutionMode,
138
160
  }),
139
161
  siteAdmin: createSiteAdminService(db, databaseSchema, dialect, {
140
162
  siteResolutionMode: config?.siteResolutionMode,
@@ -156,7 +178,9 @@ export function createServices(
156
178
  paths,
157
179
  posts,
158
180
  collections,
181
+ smartCollections,
159
182
  }),
183
+ language: createLanguageService({ settings, posts, paths }),
160
184
  githubAppInstallations: createGitHubAppInstallationsService(
161
185
  db,
162
186
  databaseSchema,
@@ -170,17 +194,24 @@ export type { SiteService } from "./site.js";
170
194
  export type { PathService } from "./path.js";
171
195
  export type { PostService, PostFilters, PostDeleteDeps } from "./post.js";
172
196
  export type { CustomUrlService } from "./custom-url.js";
197
+ export type {
198
+ SmartCollectionService,
199
+ SmartCollectionViewer,
200
+ } from "./smart-collection.js";
173
201
  export type { MediaService, MediaFilters } from "./media.js";
174
202
  export type { UploadSessionService } from "./upload-session.js";
175
203
  export type { CollectionService } from "./collection.js";
176
204
  export type { SearchService, SearchResult, SearchOptions } from "./search.js";
177
205
  export type { NavItemService } from "./navigation.js";
178
206
  export type { AboutPageService, AboutPageStatus } from "./about-page.js";
207
+ export type { LanguageService, LanguageState } from "./language.js";
179
208
  export type { AuthService, DeleteAccountDeps } from "./auth.js";
180
209
  export type { ApiTokenService } from "./api-token.js";
181
210
  export type {
182
211
  BootstrapService,
183
- CompleteInitialSetupData,
212
+ CompleteSiteSetupData,
213
+ CompleteSiteSetupDeps,
214
+ ProvisionOwnerAccountData,
184
215
  } from "./bootstrap.js";
185
216
  export type { SiteMemberService } from "./site-member.js";
186
217
  export type { SiteProfileService } from "./site-profile.js";