@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -90,6 +90,7 @@ function buildFixtureServices(): ServicesArg {
90
90
  ["pst_root", "hello-world"],
91
91
  ["pst_reply", "hello-reply"],
92
92
  ]),
93
+ listStandalonePaths: async () => [],
93
94
  getPostAliases: async () => new Map(),
94
95
  getCollectionSlugMap: async () => new Map([["col-1", "ideas"]]),
95
96
  },
@@ -196,6 +197,9 @@ describe("Hugo smoke build", () => {
196
197
  "public/hello-world/index.html",
197
198
  // Alias page for the reply slug (root aliases include /hello-reply/).
198
199
  "public/hello-reply/index.html",
200
+ // The feed redirects are useless unless Hugo copies them to the root of
201
+ // the published directory, where Cloudflare Pages and Netlify look.
202
+ "public/_redirects",
199
203
  ]) {
200
204
  expect(await fileExists(join(tempDir, rel)), `missing ${rel}`).toBe(true);
201
205
  }
@@ -218,6 +222,32 @@ describe("Hugo smoke build", () => {
218
222
  expect(collectionFeed).toContain("Follow-up");
219
223
  expect(collectionFeed.match(/<entry>/g)).toHaveLength(1);
220
224
 
225
+ // The exported feed carries the same entry payload the served one does,
226
+ // minus what Known Limits in docs/internal/feed-contract.md records. A
227
+ // template that stops compiling these silently drops them from every
228
+ // self-hosted export.
229
+ expect(collectionFeed).toContain('xmlns:jant="https://jant.me/ns"');
230
+ expect(collectionFeed).toContain(
231
+ 'xmlns:media="http://search.yahoo.com/mrss/"',
232
+ );
233
+ expect(collectionFeed).toContain("<jant:format>note</jant:format>");
234
+ // Nothing in this thread is cut, so the flag stays absent.
235
+ expect(collectionFeed).not.toContain("<jant:truncated/>");
236
+ expect(collectionFeed).toContain(
237
+ '<category term="ideas" label="Ideas" jant:page="https://example.com/ideas/"/>',
238
+ );
239
+ // Media RSS describes the attachment; the content shows it in the same
240
+ // `data-post-media` container the site marks its gallery strip with.
241
+ expect(collectionFeed).toContain('medium="image"');
242
+ expect(collectionFeed).toContain('width="800" height="600"');
243
+ expect(collectionFeed).toContain(
244
+ '<media:description type="plain">A photo</media:description>',
245
+ );
246
+ expect(collectionFeed).toContain("<div data-post-media>");
247
+ // An image is already shown full size inside a link to the original, so it
248
+ // gets no enclosure.
249
+ expect(collectionFeed).not.toContain('rel="enclosure"');
250
+
221
251
  const featuredHtml = await readFile(
222
252
  join(tempDir, "public/featured/index.html"),
223
253
  "utf-8",
@@ -234,4 +264,364 @@ describe("Hugo smoke build", () => {
234
264
  expect(featuredFeed).toContain("Follow-up");
235
265
  expect(featuredFeed.match(/<entry>/g)).toHaveLength(1);
236
266
  }, 60_000);
267
+
268
+ // Two feed details the served feed gets right and the template has to
269
+ // match by hand: author text inside an attribute, and the entry-level
270
+ // truncation flag on a thread.
271
+ it("escapes image alt in the feed and marks a thread truncated by a folded-in reply", async () => {
272
+ if (!hugoOk) {
273
+ console.log("hugo binary not found on PATH — skipping hugo build test");
274
+ return;
275
+ }
276
+
277
+ const para = (text: string) => ({
278
+ type: "paragraph",
279
+ content: [{ type: "text", text }],
280
+ });
281
+ const root = makePost({
282
+ id: "pst_blue",
283
+ slug: "blue-room",
284
+ title: "The blue room",
285
+ threadId: "pst_blue",
286
+ });
287
+ // The leading reply is cut by the timeline; the newest is not. The old
288
+ // rule looked only at the newest reply and would have left the entry
289
+ // unmarked. A titleless note is cut at NOTE_SUMMARY_MAX_CHARS with at
290
+ // least NOTE_SUMMARY_MIN_HIDDEN_CHARS left over, so four long paragraphs.
291
+ const longReply = makePost({
292
+ id: "pst_long",
293
+ slug: "blue-long",
294
+ title: null,
295
+ replyToId: "pst_blue",
296
+ threadId: "pst_blue",
297
+ createdAt: 1773020000,
298
+ publishedAt: 1773020000,
299
+ body: JSON.stringify({
300
+ type: "doc",
301
+ content: ["Alpha", "Beta", "Gamma", "Omega"].map((word) =>
302
+ para(`${word} `.repeat(90).trim()),
303
+ ),
304
+ }),
305
+ });
306
+ const shortReply = makePost({
307
+ id: "pst_short",
308
+ slug: "blue-short",
309
+ title: null,
310
+ replyToId: "pst_blue",
311
+ threadId: "pst_blue",
312
+ createdAt: 1773030000,
313
+ publishedAt: 1773030000,
314
+ body: JSON.stringify({ type: "doc", content: [para("Short enough.")] }),
315
+ });
316
+ // A quote and a backslash: Go's `%q` would turn both into `\"` and `\\`.
317
+ const alt = 'The "Blue" room, C:\\photos';
318
+ const media = makeMedia({ id: "med_blue", filename: "blue.webp", alt });
319
+
320
+ const services = {
321
+ posts: { list: async () => [root, longReply, shortReply] },
322
+ paths: {
323
+ getPostSlugMap: async () =>
324
+ new Map([
325
+ ["pst_blue", "blue-room"],
326
+ ["pst_long", "blue-long"],
327
+ ["pst_short", "blue-short"],
328
+ ]),
329
+ listStandalonePaths: async () => [],
330
+ getPostAliases: async () => new Map(),
331
+ getCollectionSlugMap: async () => new Map(),
332
+ },
333
+ collections: {
334
+ list: async () => [],
335
+ listDirectoryData: async () => ({
336
+ collections: [],
337
+ items: [],
338
+ directoryItems: [],
339
+ }),
340
+ getCollectionsByPostIds: async () => new Map(),
341
+ getCollectionEntriesByThreadIds: async () => new Map(),
342
+ },
343
+ media: {
344
+ getByPostIds: async () => new Map([["pst_blue", [media]]]),
345
+ },
346
+ } as unknown as ServicesArg;
347
+
348
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-feed-"));
349
+ try {
350
+ const files = await createExportService(
351
+ services,
352
+ makeSiteConfig(),
353
+ ).generateHugoFiles();
354
+ for (const file of files) {
355
+ const target = join(siteDir, file.path);
356
+ await mkdir(dirname(target), { recursive: true });
357
+ const data =
358
+ typeof file.content === "string"
359
+ ? new TextEncoder().encode(file.content)
360
+ : file.content;
361
+ await writeFile(target, data);
362
+ }
363
+ await writeFile(
364
+ join(siteDir, "content/blue-room/med_blue.webp"),
365
+ "PHOTO",
366
+ );
367
+
368
+ const { code, stdout, stderr } = await runHugo(siteDir);
369
+ if (code !== 0) {
370
+ console.error("hugo stdout:", stdout);
371
+ console.error("hugo stderr:", stderr);
372
+ }
373
+ expect(code).toBe(0);
374
+ expect(stderr).not.toMatch(/\bERROR\b/);
375
+
376
+ const feed = await readFile(join(siteDir, "public/index.xml"), "utf-8");
377
+ expect(feed.match(/<entry>/g)).toHaveLength(1);
378
+
379
+ // The attribute is XML-escaped like the caption next to it, not
380
+ // Go-quoted: `"` becomes a character reference and `\` stays itself.
381
+ const escapedAlt = "The &#34;Blue&#34; room, C:\\photos";
382
+ expect(feed).toContain(`alt="${escapedAlt}"`);
383
+ expect(feed).toContain(`<figcaption>${escapedAlt}</figcaption>`);
384
+ expect(feed).not.toContain('alt="The \\"');
385
+
386
+ expect(feed).toContain("<jant:truncated/>");
387
+ } finally {
388
+ await rm(siteDir, { recursive: true, force: true });
389
+ }
390
+ }, 60_000);
391
+
392
+ it("never emits an alias page that redirects to nowhere", async () => {
393
+ if (!hugoOk) {
394
+ console.log("hugo binary not found on PATH — skipping hugo build test");
395
+ return;
396
+ }
397
+
398
+ const root = makePost({
399
+ id: "pst_root",
400
+ slug: "hello-world",
401
+ title: "Hello World",
402
+ threadId: "pst_root",
403
+ });
404
+ const draft = makePost({
405
+ id: "pst_draft",
406
+ slug: "secret-plan",
407
+ title: "Secret Plan",
408
+ threadId: "pst_draft",
409
+ status: "draft",
410
+ });
411
+ const services = {
412
+ posts: { list: async () => [root, draft] },
413
+ paths: {
414
+ getPostSlugMap: async () =>
415
+ new Map([
416
+ ["pst_root", "hello-world"],
417
+ ["pst_draft", "secret-plan"],
418
+ ]),
419
+ listStandalonePaths: async () => [],
420
+ getPostAliases: async () =>
421
+ new Map([
422
+ ["pst_root", ["/blog/hello-world/"]],
423
+ ["pst_draft", ["/blog/nixos-setup/"]],
424
+ ]),
425
+ getCollectionSlugMap: async () => new Map(),
426
+ },
427
+ collections: {
428
+ list: async () => [],
429
+ listDirectoryData: async () => ({
430
+ collections: [],
431
+ items: [],
432
+ directoryItems: [],
433
+ }),
434
+ getCollectionsByPostIds: async () => new Map(),
435
+ getCollectionEntriesByThreadIds: async () => new Map(),
436
+ },
437
+ media: { getByPostIds: async () => new Map() },
438
+ } as unknown as ServicesArg;
439
+
440
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-alias-"));
441
+ try {
442
+ const files = await createExportService(
443
+ services,
444
+ makeSiteConfig(),
445
+ ).generateHugoFiles();
446
+ for (const file of files) {
447
+ const target = join(siteDir, file.path);
448
+ await mkdir(dirname(target), { recursive: true });
449
+ const data =
450
+ typeof file.content === "string"
451
+ ? new TextEncoder().encode(file.content)
452
+ : file.content;
453
+ await writeFile(target, data);
454
+ }
455
+ // A page whose `aliases:` were written by hand rather than by Jant —
456
+ // editing content on GitHub is a supported workflow, so the theme
457
+ // cannot assume every alias came from the exporter.
458
+ await mkdir(join(siteDir, "content/hand-written"), { recursive: true });
459
+ await writeFile(
460
+ join(siteDir, "content/hand-written/_index.md"),
461
+ [
462
+ "---",
463
+ 'title: "Hand written"',
464
+ 'date: "2026-03-09T00:00:00.000Z"',
465
+ 'slug: "hand-written"',
466
+ 'type: "post"',
467
+ "draft: true",
468
+ "aliases:",
469
+ ' - "/blog/hand-written/"',
470
+ "---",
471
+ "",
472
+ "Body.",
473
+ "",
474
+ ].join("\n"),
475
+ );
476
+
477
+ const { code, stdout, stderr } = await runHugo(siteDir);
478
+ if (code !== 0) {
479
+ console.error("hugo stdout:", stdout);
480
+ console.error("hugo stderr:", stderr);
481
+ }
482
+ expect(code).toBe(0);
483
+
484
+ // The published thread's alias redirects where it always did.
485
+ const liveAlias = await readFile(
486
+ join(siteDir, "public/blog/hello-world/index.html"),
487
+ "utf-8",
488
+ );
489
+ expect(liveAlias).toContain(
490
+ 'content="0; url=https://example.com/hello-world/"',
491
+ );
492
+ // `jsonify` alone is escaped into a JS string by Hugo's contextual
493
+ // escaping, which leaves the historical-root-alias branch comparing
494
+ // single characters and never taking. `safeJS` keeps it an array.
495
+ expect(liveAlias).toContain('["/blog/hello-world/"]');
496
+ expect(liveAlias).not.toContain("'[\"/blog/hello-world/\"]'");
497
+
498
+ // The unpublished thread gets no alias page at all: the exporter left
499
+ // `aliases:` off a root Hugo was never going to build.
500
+ expect(
501
+ await fileExists(join(siteDir, "public/blog/nixos-setup/index.html")),
502
+ ).toBe(false);
503
+
504
+ // The hand-written one does get a page, and it has to be inert. An
505
+ // empty `url=` is read as "reload this page", so the alias would
506
+ // refresh itself forever.
507
+ const orphanAlias = await readFile(
508
+ join(siteDir, "public/blog/hand-written/index.html"),
509
+ "utf-8",
510
+ );
511
+ expect(orphanAlias).not.toContain("http-equiv");
512
+ expect(orphanAlias).toContain("This page is not available");
513
+ expect(orphanAlias).toContain('content="noindex,nofollow"');
514
+ } finally {
515
+ await rm(siteDir, { recursive: true, force: true });
516
+ }
517
+ }, 60_000);
518
+
519
+ it("folds a long thread's preview the way the site does", async () => {
520
+ if (!hugoOk) {
521
+ console.log("hugo binary not found on PATH — skipping hugo build test");
522
+ return;
523
+ }
524
+
525
+ const words = ["one", "two", "three", "four", "five", "six", "seven"];
526
+ const root = makePost({
527
+ id: "pst_fold",
528
+ slug: "fold-root",
529
+ title: "A long thread",
530
+ threadId: "pst_fold",
531
+ });
532
+ const replies = words.map((word, index) =>
533
+ makePost({
534
+ id: `pst_fold_${word}`,
535
+ slug: `fold-${word}`,
536
+ title: null,
537
+ replyToId: "pst_fold",
538
+ threadId: "pst_fold",
539
+ createdAt: 1773020000 + index * 1000,
540
+ publishedAt: 1773020000 + index * 1000,
541
+ body: JSON.stringify({
542
+ type: "doc",
543
+ content: [
544
+ {
545
+ type: "paragraph",
546
+ content: [{ type: "text", text: `Reply ${word} of the fold.` }],
547
+ },
548
+ ],
549
+ }),
550
+ }),
551
+ );
552
+
553
+ const services = {
554
+ posts: { list: async () => [root, ...replies] },
555
+ paths: {
556
+ getPostSlugMap: async () =>
557
+ new Map([
558
+ ["pst_fold", "fold-root"],
559
+ ...words.map((word): [string, string] => [
560
+ `pst_fold_${word}`,
561
+ `fold-${word}`,
562
+ ]),
563
+ ]),
564
+ listStandalonePaths: async () => [],
565
+ getPostAliases: async () => new Map(),
566
+ getCollectionSlugMap: async () => new Map(),
567
+ },
568
+ collections: {
569
+ list: async () => [],
570
+ listDirectoryData: async () => ({
571
+ collections: [],
572
+ items: [],
573
+ directoryItems: [],
574
+ }),
575
+ getCollectionsByPostIds: async () => new Map(),
576
+ getCollectionEntriesByThreadIds: async () => new Map(),
577
+ },
578
+ media: { getByPostIds: async () => new Map() },
579
+ } as unknown as ServicesArg;
580
+
581
+ const siteDir = await mkdtemp(join(tmpdir(), "jant-hugo-fold-"));
582
+ try {
583
+ const files = await createExportService(
584
+ services,
585
+ makeSiteConfig(),
586
+ ).generateHugoFiles();
587
+ for (const file of files) {
588
+ const target = join(siteDir, file.path);
589
+ await mkdir(dirname(target), { recursive: true });
590
+ const data =
591
+ typeof file.content === "string"
592
+ ? new TextEncoder().encode(file.content)
593
+ : file.content;
594
+ await writeFile(target, data);
595
+ }
596
+
597
+ const { code, stdout, stderr } = await runHugo(siteDir);
598
+ if (code !== 0) {
599
+ console.error("hugo stdout:", stdout);
600
+ console.error("hugo stderr:", stderr);
601
+ }
602
+ expect(code).toBe(0);
603
+
604
+ const html = await readFile(join(siteDir, "public/index.html"), "utf-8");
605
+ // Core's fold: the first two replies, a gap, the two before the newest,
606
+ // and the newest as the hero. Seven replies hide two.
607
+ for (const shown of ["one", "two", "five", "six", "seven"]) {
608
+ expect(html).toContain(`Reply ${shown} of the fold.`);
609
+ }
610
+ for (const hidden of ["three", "four"]) {
611
+ expect(html).not.toContain(`Reply ${hidden} of the fold.`);
612
+ }
613
+ expect(html).toContain("2 more posts");
614
+ // The gap leads to the first hidden reply, as the site's does. The
615
+ // build is minified, so attribute quotes are not guaranteed.
616
+ expect(html).toMatch(/thread-preview-gap"? href="?[^" >]*\/fold-three\//);
617
+ // A reply's card links to the reply's own address — its alias on the
618
+ // root — not to an empty href, since reply pages are never rendered.
619
+ expect(html).toMatch(
620
+ /post-card-permalink"? href="?[^" >]*\/fold-seven\//,
621
+ );
622
+ expect(html).not.toMatch(/post-card-permalink"? href(?=[ >])/);
623
+ } finally {
624
+ await rm(siteDir, { recursive: true, force: true });
625
+ }
626
+ }, 60_000);
237
627
  });
@@ -53,6 +53,7 @@ function buildRoundtripServices(opts: {
53
53
  },
54
54
  paths: {
55
55
  getPostSlugMap: async () => slugMap,
56
+ listStandalonePaths: async () => [],
56
57
  getPostAliases: async () => opts.aliasMap ?? new Map(),
57
58
  getCollectionSlugMap: async () => collectionSlugMap,
58
59
  },
@@ -264,4 +265,97 @@ describe("export → import round-trip", () => {
264
265
  );
265
266
  expect(firstFm).toEqual(secondFm);
266
267
  });
268
+ // Replies written in one go share a second, and a migrated archive puts
269
+ // hundreds of roots on the same second. Jant breaks those ties by ID; the
270
+ // export records the order and the importer creates posts in it, so the
271
+ // new IDs tie-break the same way.
272
+ it("keeps the order of posts that share a second", async () => {
273
+ const second = 1773014400;
274
+ const root = makePost({
275
+ id: "pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
276
+ slug: "thread",
277
+ threadId: "pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
278
+ createdAt: 1773000000,
279
+ publishedAt: second,
280
+ updatedAt: 1773090000,
281
+ });
282
+ // IDs in thread order are the reverse of the slugs' alphabetical order.
283
+ const replies = [
284
+ ["pst_01bbbbbbbbbbbbbbbbbbbbbbb1", "zeta"],
285
+ ["pst_01bbbbbbbbbbbbbbbbbbbbbbb2", "mu"],
286
+ ["pst_01bbbbbbbbbbbbbbbbbbbbbbb3", "alpha"],
287
+ ].map(([id, slug]) =>
288
+ makePost({
289
+ id,
290
+ slug,
291
+ replyToId: root.id,
292
+ threadId: root.id,
293
+ createdAt: second,
294
+ publishedAt: second,
295
+ updatedAt: second,
296
+ }),
297
+ );
298
+ const olderSameSecondRoot = makePost({
299
+ id: "pst_01000000000000000000000000",
300
+ slug: "zzz-older-same-second",
301
+ threadId: "pst_01000000000000000000000000",
302
+ publishedAt: second,
303
+ });
304
+ const video = makeMedia({
305
+ id: "med-video",
306
+ filename: "clip.mp4",
307
+ mimeType: "video/mp4",
308
+ durationSeconds: 11,
309
+ });
310
+
311
+ const services = buildRoundtripServices({
312
+ // Listed out of order on purpose.
313
+ posts: [replies[2], root, replies[0], olderSameSecondRoot, replies[1]],
314
+ collections: [],
315
+ mediaByPost: new Map([[root.id, [video]]]),
316
+ collectionEntriesByThread: new Map(),
317
+ });
318
+ const files = await createExportService(
319
+ services,
320
+ makeSiteConfig(),
321
+ ).generateHugoFiles();
322
+ await writeExportToDir(files, tempDir);
323
+ await mkdir(join(tempDir, "static", "media"), { recursive: true });
324
+ await writeFile(join(tempDir, "static/media/med-video.mp4"), "video");
325
+
326
+ const walked = await importTestHelpers.walkHugoContent(tempDir);
327
+ expect(walked.rootBundles.map((bundle) => bundle.slug)).toEqual([
328
+ "zzz-older-same-second",
329
+ "thread",
330
+ ]);
331
+ const thread = walked.rootBundles[1];
332
+ expect(thread.children.map((bundle) => bundle.slug)).toEqual([
333
+ "zeta",
334
+ "mu",
335
+ "alpha",
336
+ ]);
337
+ expect(thread.children.map((bundle) => bundle.frontMatter.weight)).toEqual([
338
+ 1, 2, 3,
339
+ ]);
340
+
341
+ // Creation and edit times come back with the payload.
342
+ const payload = importTestHelpers.buildPostPayloadFromBundle(thread, {
343
+ bodyMarkdown: "",
344
+ attachments: [],
345
+ memberships: { entries: [], ids: [] },
346
+ replyToId: null,
347
+ });
348
+ expect(payload).toMatchObject({
349
+ publishedAt: second,
350
+ createdAt: 1773000000,
351
+ updatedAt: 1773090000,
352
+ });
353
+
354
+ const [videoSpec] = await Promise.all(
355
+ (thread.frontMatter.media ?? []).map((entry) =>
356
+ importTestHelpers.mediaSpecFromJantMedia(entry, tempDir),
357
+ ),
358
+ );
359
+ expect(videoSpec?.durationSeconds).toBe(11);
360
+ });
267
361
  });