@jant/core 0.6.16 → 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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -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-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  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-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  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__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -29,7 +29,7 @@ import {
29
29
  } from "../lib/jant-branding.js";
30
30
  import { getRootActivityAt } from "../db/thread-activity.js";
31
31
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
32
- import { extractBodyText } from "../lib/summary.js";
32
+ import { extractBodyText, extractTimelineSummary } from "../lib/summary.js";
33
33
  import { getMediaUrl, getPublicUrlForProvider } from "../lib/image.js";
34
34
  import { render as renderMarkdown } from "../lib/markdown.js";
35
35
  import { formatRelativeAge, toISOString } from "../lib/time.js";
@@ -85,6 +85,7 @@ import type { StorageDriver } from "../lib/storage.js";
85
85
  import { base64ToUint8Array } from "../lib/favicon.js";
86
86
  import {
87
87
  SYSTEM_NAV_KEYS,
88
+ isFeedNavKey,
88
89
  type Collection,
89
90
  type Media,
90
91
  type NavItem,
@@ -110,6 +111,10 @@ export interface SiteConfig {
110
111
  siteUrl: string;
111
112
  siteDescription: string;
112
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[];
113
118
  showJantBrandingOnHome: boolean;
114
119
  /** Whether anonymous JSON reads are enabled on the source Jant site. */
115
120
  publicApiEnabled: boolean;
@@ -117,6 +122,8 @@ export interface SiteConfig {
117
122
  rssFeedsEnabled: boolean;
118
123
  /** "latest" or "featured" — drives the default RSS nav link in the exported site. */
119
124
  mainRssFeed: string;
125
+ /** "list" or "grid" — the layout the exported archive page opens with. */
126
+ archiveDefaultLayout: string;
120
127
  siteFooter: string;
121
128
  showHeaderAvatar: boolean;
122
129
  siteAvatarUrl: string;
@@ -137,7 +144,13 @@ export interface SiteConfig {
137
144
  sitePathPrefix?: string;
138
145
  navItems: Pick<
139
146
  NavItem,
140
- "type" | "systemKey" | "label" | "url" | "position" | "placement"
147
+ | "type"
148
+ | "systemKey"
149
+ | "label"
150
+ | "targetTitle"
151
+ | "url"
152
+ | "position"
153
+ | "placement"
141
154
  >[];
142
155
  /** Items per page for Hugo pagination — kept in sync with the main site's PAGE_SIZE. */
143
156
  pageSize: number;
@@ -255,7 +268,10 @@ export function createExportService(
255
268
  async generateHugoFiles() {
256
269
  const collectionDirectoryDataPromise =
257
270
  typeof services.collections.listDirectoryData === "function"
258
- ? 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 })
259
275
  : Promise.resolve(null);
260
276
 
261
277
  // 1. Query all data
@@ -304,14 +320,30 @@ export function createExportService(
304
320
  allPosts,
305
321
  collectionsByRoot,
306
322
  );
307
- const exportedCollectionDirectoryItems =
308
- buildExportedCollectionDirectoryItems(
309
- collectionDirectoryData?.items ??
310
- allCollections.map((collection) => ({
311
- 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) => ({
312
341
  type: "collection" as const,
313
342
  collection,
314
- })),
343
+ }));
344
+ const exportedCollectionDirectoryItems =
345
+ buildExportedCollectionDirectoryItems(
346
+ exportableDirectoryItems,
315
347
  collectionSlugMap,
316
348
  collectionMetrics,
317
349
  );
@@ -397,13 +429,28 @@ export function createExportService(
397
429
  const usedSlugs = new Set<string>();
398
430
  for (const s of slugMap.values()) usedSlugs.add(s);
399
431
  for (const s of collectionSlugMap.values()) usedSlugs.add(s);
400
- if (!usedSlugs.has("featured")) {
432
+ const hasFeaturedSection = !usedSlugs.has("featured");
433
+ if (hasFeaturedSection) {
401
434
  exportFiles.push({
402
435
  path: "content/featured/_index.md",
403
436
  content: await buildFeaturedSection(siteConfig.rssFeedsEnabled),
404
437
  });
405
438
  }
406
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
+
407
454
  // Single data file consumed by templates via `hugo.Data.jant`. The
408
455
  // collection directory lives on the same object as `directory` so
409
456
  // everything Jant owns round-trips in one place.
@@ -546,7 +593,7 @@ export function createExportService(
546
593
 
547
594
  exportFiles.push({
548
595
  path: "README.md",
549
- content: buildReadme(siteConfig.siteName),
596
+ content: buildReadme(siteConfig, hasFeaturedSection),
550
597
  });
551
598
  exportFiles.push({
552
599
  path: ".gitignore",
@@ -676,6 +723,25 @@ function buildExportedCollectionEntriesForThread(
676
723
  return resolved;
677
724
  }
678
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
+
679
745
  function collectionEntriesToRefs(
680
746
  entries: readonly ExportedCollectionEntry[],
681
747
  ): HugoCollectionRef[] {
@@ -886,7 +952,13 @@ async function buildThreadBundle(
886
952
  format: root.format,
887
953
  status: root.status,
888
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,
889
960
  summary_text: getArchiveSummaryText(root) ?? undefined,
961
+ truncated: isPostTruncated(root) || undefined,
890
962
  link_url: root.format === "link" && root.url ? root.url : undefined,
891
963
  source_name: root.format === "quote" && root.title ? root.title : undefined,
892
964
  source_url: root.format === "quote" && root.url ? root.url : undefined,
@@ -969,6 +1041,7 @@ async function buildThreadBundle(
969
1041
  status: reply.status,
970
1042
  visibility: reply.visibility,
971
1043
  summary_text: getArchiveSummaryText(reply) ?? undefined,
1044
+ truncated: isPostTruncated(reply) || undefined,
972
1045
  link_url: reply.format === "link" && reply.url ? reply.url : undefined,
973
1046
  source_name:
974
1047
  reply.format === "quote" && reply.title ? reply.title : undefined,
@@ -1356,13 +1429,21 @@ function buildExportedCollectionMetrics(
1356
1429
  // Nav item resolution
1357
1430
  // ---------------------------------------------------------------------------
1358
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
+ */
1359
1440
  function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
1360
1441
  if (item.label) return item.label;
1361
1442
  if (item.systemKey) {
1362
1443
  const definition = SYSTEM_NAV_KEYS[item.systemKey as SystemNavKey];
1363
1444
  if (definition) return definition.defaultLabel;
1364
1445
  }
1365
- return item.label;
1446
+ return item.targetTitle?.trim() || item.label;
1366
1447
  }
1367
1448
 
1368
1449
  /**
@@ -1389,7 +1470,10 @@ function resolveNavItemUrl(
1389
1470
  }
1390
1471
  if (item.systemKey === "collections") return "/collections/";
1391
1472
  if (item.systemKey === "archive") return "/archive/";
1392
- 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") {
1393
1477
  return mainRssFeed === "featured" ? "/featured/index.xml" : "/index.xml";
1394
1478
  }
1395
1479
  return item.url;
@@ -1468,6 +1552,7 @@ function buildHugoToml(config: SiteConfig): string {
1468
1552
  "[params]",
1469
1553
  ` description = "${escapeTomlString(config.siteDescription)}"`,
1470
1554
  ` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1555
+ ` archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
1471
1556
  ` public_api_enabled = ${config.publicApiEnabled}`,
1472
1557
  ` rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1473
1558
  ` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
@@ -1510,7 +1595,10 @@ function buildJantDataToml(
1510
1595
  `site_name = "${escapeTomlString(config.siteName)}"`,
1511
1596
  `site_description = "${escapeTomlString(config.siteDescription)}"`,
1512
1597
  `site_language = "${escapeTomlString(config.siteLanguage)}"`,
1598
+ `multilingual_enabled = ${config.multilingualEnabled}`,
1599
+ `additional_languages = "${escapeTomlString(config.additionalLanguages.join(","))}"`,
1513
1600
  `main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1601
+ `archive_default_layout = "${escapeTomlString(config.archiveDefaultLayout)}"`,
1514
1602
  `public_api_enabled = ${config.publicApiEnabled}`,
1515
1603
  `rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1516
1604
  `show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
@@ -1550,10 +1638,12 @@ function buildJantDataToml(
1550
1638
  // `settings` is authenticated-only and has no corresponding page in the
1551
1639
  // static Hugo site — drop it at export time so it never shows up in nav.
1552
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.
1553
1643
  if (
1554
1644
  !config.rssFeedsEnabled &&
1555
1645
  item.type === "system" &&
1556
- item.systemKey === "rss"
1646
+ isFeedNavKey(item.systemKey)
1557
1647
  ) {
1558
1648
  continue;
1559
1649
  }
@@ -1614,9 +1704,95 @@ function buildJantDataToml(
1614
1704
  }
1615
1705
 
1616
1706
  // ---------------------------------------------------------------------------
1617
- // README + .gitignore
1707
+ // README + .gitignore + _redirects
1618
1708
  // ---------------------------------------------------------------------------
1619
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
+
1620
1796
  function buildGitignore(): string {
1621
1797
  return `# Hugo build output
1622
1798
  public/
@@ -1634,7 +1810,90 @@ Thumbs.db
1634
1810
  `;
1635
1811
  }
1636
1812
 
1637
- 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
+ : "";
1638
1897
  return `# ${siteName} — Hugo Export
1639
1898
 
1640
1899
  This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Hugo](https://gohugo.io/).
@@ -1679,7 +1938,7 @@ hugo --minify
1679
1938
 
1680
1939
  The output goes to the \`public/\` directory. Upload it to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, etc.).
1681
1940
 
1682
- ## Project structure
1941
+ ${feedsSection}## Project structure
1683
1942
 
1684
1943
  \`\`\`
1685
1944
  hugo.toml — Site configuration (baseURL, title, theme, params)
@@ -1695,7 +1954,7 @@ content/
1695
1954
  data/
1696
1955
  jant.toml — Nav items, branding, display preferences, ordered collections directory
1697
1956
  themes/jant/ — Bundled Hugo theme (overrideable via layouts/ at the site root)
1698
- static/ — Copy files here to add them to the published site
1957
+ static/ — Copy files here to add them to the published site${staticListing}
1699
1958
  \`\`\`
1700
1959
 
1701
1960
  ## Customizing
@@ -1721,7 +1980,7 @@ Safe to re-run; files already on disk are reused. Anything that fails to downloa
1721
1980
  ## Notes
1722
1981
 
1723
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.
1724
- - \`/{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}
1725
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.
1726
1985
  - Posts with \`draft: true\` in front matter are only built when you pass \`--buildDrafts\` to \`hugo\` / \`hugo serve\`.
1727
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";