@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -1,4 +1,4 @@
1
- import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-DWn149b7.js";
1
+ import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-CsFx6F_M.js";
2
2
  import { r as getInstallationToken } from "./github-app-BbklkFmU.js";
3
3
  import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BgSiE71w.js";
4
4
  //#region src/lib/markdown-to-tiptap.ts
@@ -1,4 +1,4 @@
1
1
  import "./url-BMYO-Zlt.js";
2
- import "./export-DWn149b7.js";
3
- import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-Ckk0sJxK.js";
2
+ import "./export-CsFx6F_M.js";
3
+ import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-Cq1pzzOI.js";
4
4
  export { classifyRepoForSync, createGitHubSyncService };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { y as url_exports } from "./url-BMYO-Zlt.js";
2
- import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-2i7-WQHg.js";
3
- import { S as time_exports, a as markdown_exports } from "./export-DWn149b7.js";
2
+ import { C as toPostView, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, d as defaultFeedRenderer, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-C-oi_t8W.js";
3
+ import { B as STATUSES, L as MEDIA_KINDS, N as MAX_MEDIA_ATTACHMENTS, P as MAX_PINNED_POSTS, R as NAV_ITEM_TYPES, W as TEXT_ATTACHMENT_CONTENT_FORMATS, a as markdown_exports, j as FORMATS, w as time_exports, z as SORT_ORDERS } from "./export-CsFx6F_M.js";
4
4
  import "./env-OHRKGcMj.js";
5
- import "./github-sync-Ckk0sJxK.js";
5
+ import "./github-sync-Cq1pzzOI.js";
6
6
  export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
package/dist/node.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import "./url-BMYO-Zlt.js";
2
- import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-2i7-WQHg.js";
3
- import { t as createExportService } from "./export-DWn149b7.js";
2
+ import { A as buildThemeStyle, D as getCjkFontCssVariables, E as BUILTIN_FONT_THEMES, M as getPublicAssetBasePath, N as isAssetPath, O as getFontThemeCssVariables, a as getHostBasedStartupConfigurationIssues, c as getWebhookUrl, f as createStorageDriver, g as schema_exports, h as createNodeDatabase, i as createSiteService, j as BUILTIN_COLOR_THEMES, k as resolveCjkFontProfile, l as setMyCommands, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as resolveDatabaseDialect, p as pgSchemaBundle, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook } from "./app-C-oi_t8W.js";
3
+ import { t as createExportService } from "./export-CsFx6F_M.js";
4
4
  import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-OHRKGcMj.js";
5
- import "./github-sync-Ckk0sJxK.js";
5
+ import "./github-sync-Cq1pzzOI.js";
6
6
  import { drizzle } from "drizzle-orm/better-sqlite3";
7
7
  import { serve } from "@hono/node-server";
8
8
  import Database from "better-sqlite3";
@@ -529,7 +529,7 @@ async function createNodeRequestHandler(options) {
529
529
  async function start(env = process.env, app) {
530
530
  const handler = await createNodeRequestHandler({
531
531
  env,
532
- app: async () => app ?? (await import("./app-DZDlmh7C.js")).createApp()
532
+ app: async () => app ?? (await import("./app-BxCOR3Uc.js")).createApp()
533
533
  });
534
534
  const hostname = resolveHost(env);
535
535
  const port = resolvePort(env);
@@ -574,4 +574,4 @@ async function start(env = process.env, app) {
574
574
  });
575
575
  }
576
576
  //#endregion
577
- export { BUILTIN_COLOR_THEMES, BUILTIN_FONT_THEMES, buildThemeStyle, createApp, createExportService, createNodeBindings, createNodeCliRuntime, createNodeRequestHandler, createNodeRequestRuntime, createStorageDriver, getCjkSerifCssVariables, getFontThemeCssVariables, migrate, resolveConfig, start };
577
+ export { BUILTIN_COLOR_THEMES, BUILTIN_FONT_THEMES, buildThemeStyle, createApp, createExportService, createNodeBindings, createNodeCliRuntime, createNodeRequestHandler, createNodeRequestRuntime, createStorageDriver, getCjkFontCssVariables, getFontThemeCssVariables, migrate, resolveCjkFontProfile, resolveConfig, start };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.6.14",
3
+ "version": "0.6.15",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -37,15 +37,16 @@
37
37
  "@lingui/core": "^5.9.3",
38
38
  "@lingui/react": "^5.9.3",
39
39
  "@tailwindcss/typography": "^0.5.19",
40
- "@tiptap/core": "^3.22.1",
41
- "@tiptap/extension-code-block": "^3.22.1",
42
- "@tiptap/extension-image": "^3.22.1",
43
- "@tiptap/extension-placeholder": "^3.22.1",
44
- "@tiptap/extension-table": "^3.22.1",
45
- "@tiptap/markdown": "^3.22.1",
46
- "@tiptap/pm": "^3.22.1",
47
- "@tiptap/starter-kit": "^3.22.1",
48
- "@tiptap/suggestion": "^3.22.1",
40
+ "@tiptap/core": "^3.27.3",
41
+ "@tiptap/extension-code-block": "^3.27.3",
42
+ "@tiptap/extension-image": "^3.27.3",
43
+ "@tiptap/extension-link": "^3.27.3",
44
+ "@tiptap/extension-placeholder": "^3.27.3",
45
+ "@tiptap/extension-table": "^3.27.3",
46
+ "@tiptap/markdown": "^3.27.3",
47
+ "@tiptap/pm": "^3.27.3",
48
+ "@tiptap/starter-kit": "^3.27.3",
49
+ "@tiptap/suggestion": "^3.27.3",
49
50
  "basecoat-css": "^0.3.11",
50
51
  "better-auth": "^1.5.5",
51
52
  "better-sqlite3": "^12.8.0",
@@ -0,0 +1,137 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { run } from "../../../bin/commands/posts/rebuild-html.js";
3
+ import { POST_BODY_HTML_VERSION } from "../../lib/post-body-html.js";
4
+
5
+ const originalEnv = {
6
+ INTERNAL_ADMIN_TOKEN: process.env.INTERNAL_ADMIN_TOKEN,
7
+ SITE_ORIGIN: process.env.SITE_ORIGIN,
8
+ SITE_PATH_PREFIX: process.env.SITE_PATH_PREFIX,
9
+ };
10
+
11
+ afterEach(() => {
12
+ for (const [key, value] of Object.entries(originalEnv)) {
13
+ if (value === undefined) {
14
+ delete process.env[key];
15
+ } else {
16
+ process.env[key] = value;
17
+ }
18
+ }
19
+
20
+ vi.restoreAllMocks();
21
+ vi.unstubAllGlobals();
22
+ });
23
+
24
+ function batchResponse(overrides: Record<string, unknown> = {}) {
25
+ return new Response(
26
+ JSON.stringify({
27
+ processed: 1,
28
+ wouldRebuild: 1,
29
+ rebuilt: 1,
30
+ wouldUpgradeFootnotes: 0,
31
+ upgradedFootnotes: 0,
32
+ skipped: 0,
33
+ conflicted: 0,
34
+ failed: 0,
35
+ failures: [],
36
+ nextCursor: null,
37
+ done: true,
38
+ targetVersion: POST_BODY_HTML_VERSION,
39
+ ...overrides,
40
+ }),
41
+ { status: 200, headers: { "Content-Type": "application/json" } },
42
+ );
43
+ }
44
+
45
+ describe("jant posts rebuild-html", () => {
46
+ it("paginates the current site and preserves dry-run mode", async () => {
47
+ process.env.INTERNAL_ADMIN_TOKEN = "internal-secret";
48
+ const fetchMock = vi
49
+ .fn()
50
+ .mockResolvedValueOnce(
51
+ batchResponse({
52
+ processed: 2,
53
+ wouldRebuild: 2,
54
+ rebuilt: 0,
55
+ nextCursor: "pst_cursor",
56
+ done: false,
57
+ }),
58
+ )
59
+ .mockResolvedValueOnce(
60
+ batchResponse({ processed: 1, rebuilt: 0, wouldRebuild: 1 }),
61
+ );
62
+ vi.stubGlobal("fetch", fetchMock);
63
+ vi.spyOn(console, "log").mockImplementation(() => {});
64
+
65
+ await run([
66
+ "--url",
67
+ "https://example.com/blog",
68
+ "--limit",
69
+ "2",
70
+ "--dry-run",
71
+ ]);
72
+
73
+ expect(fetchMock).toHaveBeenNthCalledWith(
74
+ 1,
75
+ "https://example.com/blog/api/internal/posts/body-html/rebuild",
76
+ expect.objectContaining({
77
+ method: "POST",
78
+ headers: {
79
+ Authorization: "Bearer internal-secret",
80
+ "Content-Type": "application/json",
81
+ },
82
+ body: JSON.stringify({ limit: 2, dryRun: true }),
83
+ }),
84
+ );
85
+ expect(fetchMock).toHaveBeenNthCalledWith(
86
+ 2,
87
+ "https://example.com/blog/api/internal/posts/body-html/rebuild",
88
+ expect.objectContaining({
89
+ body: JSON.stringify({
90
+ limit: 2,
91
+ cursor: "pst_cursor",
92
+ dryRun: true,
93
+ }),
94
+ }),
95
+ );
96
+ });
97
+
98
+ it("targets one explicit managed site without fleet-wide discovery", async () => {
99
+ const fetchMock = vi.fn().mockResolvedValue(batchResponse());
100
+ vi.stubGlobal("fetch", fetchMock);
101
+ vi.spyOn(console, "log").mockImplementation(() => {});
102
+
103
+ await run([
104
+ "--url",
105
+ "https://core.example.com",
106
+ "--site",
107
+ "sit_managed",
108
+ "--token",
109
+ "secret",
110
+ "--once",
111
+ ]);
112
+
113
+ expect(fetchMock).toHaveBeenCalledWith(
114
+ "https://core.example.com/api/internal/sites/sit_managed/posts/body-html/rebuild",
115
+ expect.objectContaining({
116
+ body: JSON.stringify({ limit: 50 }),
117
+ }),
118
+ );
119
+ });
120
+
121
+ it("aborts when pagination does not advance", async () => {
122
+ const fetchMock = vi
123
+ .fn()
124
+ .mockResolvedValueOnce(
125
+ batchResponse({ nextCursor: "pst_same", done: false }),
126
+ )
127
+ .mockResolvedValueOnce(
128
+ batchResponse({ nextCursor: "pst_same", done: false }),
129
+ );
130
+ vi.stubGlobal("fetch", fetchMock);
131
+ vi.spyOn(console, "log").mockImplementation(() => {});
132
+
133
+ await expect(
134
+ run(["--url", "https://example.com", "--token", "secret"]),
135
+ ).rejects.toThrow("without advancing the cursor");
136
+ });
137
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DEFAULT_NAVIGATION_SEED_ITEMS } from "../../dev/scripts/dev-auth-db.mjs";
3
+ import {
4
+ DEFAULT_NAVIGATION_PROFILE,
5
+ SYSTEM_NAV_KEYS,
6
+ } from "../types/constants.js";
7
+
8
+ describe("dev navigation bootstrap", () => {
9
+ it("derives the translated-label seed rows from the shared profile", () => {
10
+ expect(DEFAULT_NAVIGATION_SEED_ITEMS).toEqual(
11
+ DEFAULT_NAVIGATION_PROFILE.systemKeys.map((systemKey) => ({
12
+ systemKey,
13
+ label: "",
14
+ url: SYSTEM_NAV_KEYS[systemKey].url,
15
+ placement: SYSTEM_NAV_KEYS[systemKey].defaultPlacement,
16
+ })),
17
+ );
18
+ expect(
19
+ DEFAULT_NAVIGATION_SEED_ITEMS.map((item) => item.systemKey),
20
+ ).not.toContain("latest");
21
+ });
22
+ });
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import { spawn, spawnSync } from "node:child_process";
9
- import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
9
+ import {
10
+ mkdir,
11
+ mkdtemp,
12
+ readFile,
13
+ rm,
14
+ stat,
15
+ writeFile,
16
+ } from "node:fs/promises";
10
17
  import { tmpdir } from "node:os";
11
18
  import { dirname, join } from "node:path";
12
19
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -71,6 +78,7 @@ function buildFixtureServices(): ServicesArg {
71
78
  threadId: "pst_root",
72
79
  createdAt: 1773020000,
73
80
  publishedAt: 1773020000,
81
+ featuredAt: 1773030000,
74
82
  });
75
83
  const collection = makeCollection({ id: "col-1", slug: "ideas" });
76
84
  const media = makeMedia({ id: "med_hero", filename: "hero.webp" });
@@ -95,15 +103,29 @@ function buildFixtureServices(): ServicesArg {
95
103
  type: "collection" as const,
96
104
  collection: {
97
105
  ...collection,
98
- postCount: 1,
106
+ threadCount: 1,
99
107
  recentActivityAt: collection.updatedAt,
100
108
  },
101
109
  },
102
110
  ],
103
111
  directoryItems: [],
104
112
  }),
105
- getCollectionsByPostIds: async () => new Map(),
106
- getCollectionEntriesByPostIds: async () => new Map(),
113
+ getCollectionsByPostIds: async () =>
114
+ new Map([["pst_root", [collection]]]),
115
+ getCollectionEntriesByThreadIds: async () =>
116
+ new Map([
117
+ [
118
+ "pst_root",
119
+ [
120
+ {
121
+ collectionId: "col-1",
122
+ createdAt: 1773020000,
123
+ position: 0,
124
+ pinnedAt: null,
125
+ },
126
+ ],
127
+ ],
128
+ ]),
107
129
  },
108
130
  media: {
109
131
  getByPostIds: async () => new Map([["pst_root", [media]]]),
@@ -166,13 +188,50 @@ describe("Hugo smoke build", () => {
166
188
  for (const rel of [
167
189
  "public/index.html",
168
190
  "public/featured/index.html",
191
+ "public/featured/index.xml",
169
192
  "public/archive/index.html",
170
193
  "public/collections/index.html",
194
+ "public/ideas/index.html",
195
+ "public/ideas/index.xml",
171
196
  "public/hello-world/index.html",
172
197
  // Alias page for the reply slug (root aliases include /hello-reply/).
173
198
  "public/hello-reply/index.html",
174
199
  ]) {
175
200
  expect(await fileExists(join(tempDir, rel)), `missing ${rel}`).toBe(true);
176
201
  }
202
+
203
+ const collectionHtml = await readFile(
204
+ join(tempDir, "public/ideas/index.html"),
205
+ "utf-8",
206
+ );
207
+ expect(collectionHtml).toContain("Hello World");
208
+ expect(collectionHtml).toContain("Follow-up");
209
+ expect(collectionHtml).toMatch(
210
+ /class="thread thread-full thread-has-replies"/,
211
+ );
212
+
213
+ const collectionFeed = await readFile(
214
+ join(tempDir, "public/ideas/index.xml"),
215
+ "utf-8",
216
+ );
217
+ expect(collectionFeed).toContain("Hello World");
218
+ expect(collectionFeed).toContain("Follow-up");
219
+ expect(collectionFeed.match(/<entry>/g)).toHaveLength(1);
220
+
221
+ const featuredHtml = await readFile(
222
+ join(tempDir, "public/featured/index.html"),
223
+ "utf-8",
224
+ );
225
+ expect(featuredHtml).toContain("Hello World");
226
+ expect(featuredHtml).toContain("Follow-up");
227
+ expect(featuredHtml).toContain("thread-item-featured");
228
+
229
+ const featuredFeed = await readFile(
230
+ join(tempDir, "public/featured/index.xml"),
231
+ "utf-8",
232
+ );
233
+ expect(featuredFeed).toContain("Hello World");
234
+ expect(featuredFeed).toContain("Follow-up");
235
+ expect(featuredFeed.match(/<entry>/g)).toHaveLength(1);
177
236
  }, 60_000);
178
237
  });
@@ -31,7 +31,7 @@ function buildRoundtripServices(opts: {
31
31
  posts: Post[];
32
32
  collections: Collection[];
33
33
  mediaByPost: Map<string, Media[]>;
34
- collectionEntriesByPost: Map<
34
+ collectionEntriesByThread: Map<
35
35
  string,
36
36
  {
37
37
  collectionId: string;
@@ -65,14 +65,15 @@ function buildRoundtripServices(opts: {
65
65
  type: "collection" as const,
66
66
  collection: {
67
67
  ...collection,
68
- postCount: 0,
68
+ threadCount: 0,
69
69
  recentActivityAt: collection.updatedAt,
70
70
  },
71
71
  })),
72
72
  directoryItems: [],
73
73
  }),
74
74
  getCollectionsByPostIds: async () => new Map(),
75
- getCollectionEntriesByPostIds: async () => opts.collectionEntriesByPost,
75
+ getCollectionEntriesByThreadIds: async () =>
76
+ opts.collectionEntriesByThread,
76
77
  },
77
78
  media: {
78
79
  getByPostIds: async () => opts.mediaByPost,
@@ -170,7 +171,7 @@ describe("export → import round-trip", () => {
170
171
  ["post-root", [rootMedia]],
171
172
  ["post-reply", [replyMedia]],
172
173
  ]),
173
- collectionEntriesByPost: entries,
174
+ collectionEntriesByThread: entries,
174
175
  aliasMap: new Map([["post-root", ["/historic-slug/"]]]),
175
176
  });
176
177
  const exportService = createExportService(services, makeSiteConfig());
@@ -220,6 +221,7 @@ describe("export → import round-trip", () => {
220
221
  // Reply bundle carries build.render: never and its own media entries.
221
222
  const replyFm = rootBundle.children[0].frontMatter;
222
223
  expect(replyFm.build).toEqual({ render: "never", list: "local" });
224
+ expect(replyFm.collections).toBeUndefined();
223
225
  expect(replyFm.media?.[0].src).toBe("/media/med-reply.webp");
224
226
  expect(replyFm.media?.[0].alt).toBe("Detail");
225
227
 
@@ -26,7 +26,7 @@ interface FixtureOptions {
26
26
  posts: Post[];
27
27
  collections?: Collection[];
28
28
  collectionsByPost?: Map<string, Collection[]>;
29
- collectionEntriesByPost?: Map<
29
+ collectionEntriesByThread?: Map<
30
30
  string,
31
31
  {
32
32
  collectionId: string;
@@ -47,7 +47,7 @@ function buildServices(opts: FixtureOptions): ServicesArg {
47
47
  posts,
48
48
  collections = [],
49
49
  collectionsByPost = new Map(),
50
- collectionEntriesByPost = new Map(),
50
+ collectionEntriesByThread = new Map(),
51
51
  mediaByPost = new Map(),
52
52
  slugMap = new Map(posts.map((p) => [p.id, p.slug])),
53
53
  aliasMap = new Map(),
@@ -75,14 +75,14 @@ function buildServices(opts: FixtureOptions): ServicesArg {
75
75
  type: "collection" as const,
76
76
  collection: {
77
77
  ...collection,
78
- postCount: 0,
78
+ threadCount: 0,
79
79
  recentActivityAt: collection.updatedAt,
80
80
  },
81
81
  })),
82
82
  directoryItems: [],
83
83
  }),
84
84
  getCollectionsByPostIds: async () => collectionsByPost,
85
- getCollectionEntriesByPostIds: async () => collectionEntriesByPost,
85
+ getCollectionEntriesByThreadIds: async () => collectionEntriesByThread,
86
86
  },
87
87
  media: {
88
88
  getByPostIds: async () => mediaByPost,
@@ -123,7 +123,7 @@ describe("createExportService (Hugo)", () => {
123
123
  // Starts with YAML delimiter
124
124
  expect(text.startsWith("---\n")).toBe(true);
125
125
 
126
- // Stable key order: id, title, date, slug, type, featured_at, pinned_at
126
+ // Stable key order: Post fields, Featured projection, then pin metadata.
127
127
  const headerLines = text
128
128
  .split("\n")
129
129
  .slice(1, text.split("\n").indexOf("---", 1))
@@ -139,6 +139,8 @@ describe("createExportService (Hugo)", () => {
139
139
  "visibility",
140
140
  "summary_text",
141
141
  "featured_at",
142
+ "featured_post_ids",
143
+ "featured_sort_at",
142
144
  "pinned_at",
143
145
  ];
144
146
  let cursor = -1;
@@ -156,6 +158,10 @@ describe("createExportService (Hugo)", () => {
156
158
  expect(frontMatter.featured_at).toBe(
157
159
  new Date(1773100000 * 1000).toISOString(),
158
160
  );
161
+ expect(frontMatter.featured_post_ids).toEqual(["post-root"]);
162
+ expect(frontMatter.featured_sort_at).toBe(
163
+ new Date((root.publishedAt ?? root.createdAt) * 1000).toISOString(),
164
+ );
159
165
  expect(frontMatter.pinned_at).toBe(
160
166
  new Date(1773200000 * 1000).toISOString(),
161
167
  );
@@ -204,6 +210,47 @@ describe("createExportService (Hugo)", () => {
204
210
  expect(replyFm.id).toBe("post-reply");
205
211
  });
206
212
 
213
+ it("projects Child Featured selection and publication order onto the Root bundle", async () => {
214
+ const root = makePost({
215
+ id: "post-root",
216
+ slug: "thread-root",
217
+ threadId: "post-root",
218
+ publishedAt: 1000,
219
+ createdAt: 1000,
220
+ });
221
+ const featuredReply = makePost({
222
+ id: "post-featured-reply",
223
+ slug: "featured-reply",
224
+ replyToId: root.id,
225
+ threadId: root.id,
226
+ featuredAt: 9000,
227
+ publishedAt: 3000,
228
+ createdAt: 3000,
229
+ });
230
+ const finalReply = makePost({
231
+ id: "post-final-reply",
232
+ slug: "final-reply",
233
+ replyToId: featuredReply.id,
234
+ threadId: root.id,
235
+ publishedAt: 4000,
236
+ createdAt: 4000,
237
+ });
238
+
239
+ const service = createExportService(
240
+ buildServices({ posts: [root, featuredReply, finalReply] }),
241
+ makeSiteConfig(),
242
+ );
243
+ const files = filesToMap(await service.generateHugoFiles());
244
+ const rootText = files.get("content/thread-root/_index.md") as string;
245
+ const { frontMatter } = await parseFrontMatter(rootText);
246
+
247
+ expect(frontMatter.featured_at).toBeUndefined();
248
+ expect(frontMatter.featured_post_ids).toEqual([featuredReply.id]);
249
+ expect(frontMatter.featured_sort_at).toBe(
250
+ new Date(3000 * 1000).toISOString(),
251
+ );
252
+ });
253
+
207
254
  it("merges historical root aliases + reply slugs onto the root", async () => {
208
255
  const root = makePost({ id: "r", slug: "new-slug", threadId: "r" });
209
256
  const reply = makePost({
@@ -354,6 +401,56 @@ describe("createExportService (Hugo)", () => {
354
401
  }
355
402
  });
356
403
 
404
+ it("disables all Hugo feed outputs and hides only system RSS navigation", async () => {
405
+ const collection = makeCollection({ id: "col-1", slug: "ideas" });
406
+ const service = createExportService(
407
+ buildServices({ posts: [], collections: [collection] }),
408
+ makeSiteConfig({
409
+ publicApiEnabled: false,
410
+ rssFeedsEnabled: false,
411
+ navItems: [
412
+ {
413
+ type: "system",
414
+ systemKey: "rss",
415
+ label: "System feed",
416
+ url: "/feed",
417
+ position: 0,
418
+ placement: "header",
419
+ },
420
+ {
421
+ type: "link",
422
+ systemKey: null,
423
+ label: "Custom feed",
424
+ url: "/feed",
425
+ position: 1,
426
+ placement: "header",
427
+ },
428
+ ],
429
+ }),
430
+ );
431
+ const files = filesToMap(await service.generateHugoFiles());
432
+ const toml = files.get("hugo.toml") as string;
433
+ const data = files.get("data/jant.toml") as string;
434
+
435
+ expect(toml).toMatch(/\[outputs\][\s\S]*home = \[\s*"html"\s*\]/);
436
+ expect(toml).toContain("public_api_enabled = false");
437
+ expect(toml).toContain("rss_feeds_enabled = false");
438
+ expect(data).toContain("public_api_enabled = false");
439
+ expect(data).toContain("rss_feeds_enabled = false");
440
+ expect(data).not.toContain('label = "System feed"');
441
+ expect(data).toContain('label = "Custom feed"');
442
+
443
+ for (const path of [
444
+ "content/featured/_index.md",
445
+ "content/archive/_index.md",
446
+ "content/ideas/_index.md",
447
+ ]) {
448
+ const raw = files.get(path) as string;
449
+ const { frontMatter } = await parseFrontMatter(raw);
450
+ expect(frontMatter.outputs).toEqual(["html"]);
451
+ }
452
+ });
453
+
357
454
  it("resolves the nav RSS link to /featured/index.xml when mainRssFeed=featured", async () => {
358
455
  const service = createExportService(
359
456
  buildServices({ posts: [] }),
@@ -445,7 +542,7 @@ describe("createExportService (Hugo)", () => {
445
542
  type: "collection" as const,
446
543
  collection: {
447
544
  ...collection,
448
- postCount: 0,
545
+ threadCount: 0,
449
546
  recentActivityAt: collection.updatedAt,
450
547
  },
451
548
  },
@@ -510,6 +607,7 @@ describe("createExportService (Hugo)", () => {
510
607
  "themes/jant/layouts/partials/pagination.html",
511
608
  "themes/jant/layouts/partials/post-card.html",
512
609
  "themes/jant/layouts/partials/reply.html",
610
+ "themes/jant/layouts/partials/featured-thread.html",
513
611
  "themes/jant/layouts/partials/feed-post-content.xml",
514
612
  ];
515
613
  for (const path of expectedLayouts) {
@@ -520,6 +618,31 @@ describe("createExportService (Hugo)", () => {
520
618
  expect(files.has("themes/jant/static/tokens.css")).toBe(true);
521
619
  expect(files.has("themes/jant/static/theme.css")).toBe(true);
522
620
  expect(files.has("themes/jant/static/custom.css")).toBe(true);
621
+
622
+ const collectionList = files.get(
623
+ "themes/jant/layouts/_default/list.html",
624
+ ) as string;
625
+ expect(collectionList).toContain(
626
+ 'class="thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}"',
627
+ );
628
+ });
629
+
630
+ it("renders semantic, namespaced footnotes in exported site HTML fields", async () => {
631
+ const service = createExportService(
632
+ buildServices({ posts: [] }),
633
+ makeSiteConfig({
634
+ siteFooter: "Footer[^1]\n\n[^1]: Export definition",
635
+ }),
636
+ );
637
+ const files = filesToMap(await service.generateHugoFiles());
638
+ const { parse } = await import("smol-toml");
639
+ const data = parse(files.get("data/jant.toml") as string);
640
+ const footerHtml = data.site_footer_html as string;
641
+
642
+ expect(footerHtml).toContain('role="doc-noteref"');
643
+ expect(footerHtml).toContain('role="doc-endnotes"');
644
+ expect(footerHtml).toContain("fn-3gu8g1hux2k2m-1");
645
+ expect(footerHtml).not.toContain("site-footer");
523
646
  });
524
647
 
525
648
  it("writes a .gitignore covering Hugo build artifacts", async () => {
@@ -550,9 +673,15 @@ describe("createExportService (Hugo)", () => {
550
673
  expect(Object.keys(decoded)).toContain("hugo.toml");
551
674
  });
552
675
 
553
- it("round-trips collection memberships on each post's front matter", async () => {
676
+ it("writes Thread collection memberships only on root front matter", async () => {
554
677
  const collection = makeCollection({ id: "col-1", slug: "ideas" });
555
678
  const root = makePost({ id: "post-root", slug: "entry-a" });
679
+ const reply = makePost({
680
+ id: "post-reply",
681
+ slug: "entry-a-reply",
682
+ replyToId: "post-root",
683
+ threadId: "post-root",
684
+ });
556
685
  const entries = new Map<
557
686
  string,
558
687
  {
@@ -572,9 +701,9 @@ describe("createExportService (Hugo)", () => {
572
701
  ]);
573
702
  const service = createExportService(
574
703
  buildServices({
575
- posts: [root],
704
+ posts: [root, reply],
576
705
  collections: [collection],
577
- collectionEntriesByPost: entries,
706
+ collectionEntriesByThread: entries,
578
707
  }),
579
708
  makeSiteConfig(),
580
709
  );
@@ -591,6 +720,10 @@ describe("createExportService (Hugo)", () => {
591
720
  pinned_at: new Date(1773050000 * 1000).toISOString(),
592
721
  },
593
722
  ]);
723
+ const { frontMatter: replyFrontMatter } = await parseFrontMatter(
724
+ files.get("content/entry-a/entry-a-reply/index.md") as string,
725
+ );
726
+ expect(replyFrontMatter.collections).toBeUndefined();
594
727
  });
595
728
 
596
729
  it("emits media[] front matter for each attachment on a post", async () => {
@@ -43,6 +43,8 @@ interface TestAppOptions {
43
43
  telegramBotTokens?: string;
44
44
  /** Optional `TELEGRAM_WEBHOOK_SECRET` env binding for Telegram webhook tests */
45
45
  telegramWebhookSecret?: string;
46
+ /** RSS publication delay; defaults to zero in general route tests. */
47
+ rssPublishDelaySeconds?: number;
46
48
  }
47
49
 
48
50
  /**
@@ -84,6 +86,7 @@ export function createTestApp(options: TestAppOptions = {}) {
84
86
  HOSTED_CONTROL_PLANE_SSO_SECRET: options.hostedControlPlaneSsoSecret,
85
87
  TELEGRAM_BOT_TOKENS: options.telegramBotTokens,
86
88
  TELEGRAM_WEBHOOK_SECRET: options.telegramWebhookSecret,
89
+ RSS_PUBLISH_DELAY_SECONDS: options.rssPublishDelaySeconds ?? 0,
87
90
  NODE_DATABASE: {
88
91
  db,
89
92
  dialect: "sqlite",