@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
@@ -76,12 +76,20 @@ import PARTIAL_POST_CARD from "./export-theme/layouts/partials/post-card.html?ra
76
76
  import PARTIAL_MEDIA_GALLERY from "./export-theme/layouts/partials/media-gallery.html?raw";
77
77
  import PARTIAL_REPLY from "./export-theme/layouts/partials/reply.html?raw";
78
78
  import PARTIAL_THREAD_PREVIEW from "./export-theme/layouts/partials/thread-preview.html?raw";
79
+ import PARTIAL_FEATURED_THREAD from "./export-theme/layouts/partials/featured-thread.html?raw";
79
80
  import LAYOUT_RSS from "./export-theme/layouts/_default/rss.xml?raw";
80
81
  import PARTIAL_FEED_POST_CONTENT from "./export-theme/layouts/partials/feed-post-content.xml?raw";
81
82
 
82
83
  import type { StorageDriver } from "../lib/storage.js";
83
84
  import { base64ToUint8Array } from "../lib/favicon.js";
84
- import type { Post, Collection, Media, NavItem } from "../types.js";
85
+ import {
86
+ SYSTEM_NAV_KEYS,
87
+ type Collection,
88
+ type Media,
89
+ type NavItem,
90
+ type Post,
91
+ type SystemNavKey,
92
+ } from "../types.js";
85
93
 
86
94
  /** A file entry in the exported Hugo site. */
87
95
  export interface ExportFile {
@@ -102,6 +110,10 @@ export interface SiteConfig {
102
110
  siteDescription: string;
103
111
  siteLanguage: string;
104
112
  showJantBrandingOnHome: boolean;
113
+ /** Whether anonymous JSON reads are enabled on the source Jant site. */
114
+ publicApiEnabled: boolean;
115
+ /** Whether the exported Hugo site should emit Atom feeds. */
116
+ rssFeedsEnabled: boolean;
105
117
  /** "latest" or "featured" — drives the default RSS nav link in the exported site. */
106
118
  mainRssFeed: string;
107
119
  siteFooter: string;
@@ -171,7 +183,7 @@ interface ExportCollectionDirectorySourceItem {
171
183
  slug: string;
172
184
  title: string;
173
185
  description?: string | null;
174
- postCount?: number;
186
+ threadCount?: number;
175
187
  recentActivityAt?: number;
176
188
  };
177
189
  }
@@ -184,14 +196,14 @@ interface SiteIconAssets {
184
196
  }
185
197
 
186
198
  interface ExportedCollectionMetrics {
187
- postCount: number;
199
+ threadCount: number;
188
200
  recentActivityAt: number;
189
201
  }
190
202
 
191
203
  /**
192
204
  * A single `collections:` front-matter entry, already resolved to its
193
205
  * Hugo-visible slug. Assembled from
194
- * `collectionService.getCollectionEntriesByPostIds` + `collectionSlugMap`.
206
+ * `collectionService.getCollectionEntriesByThreadIds` + `collectionSlugMap`.
195
207
  */
196
208
  interface ExportedCollectionEntry {
197
209
  slug: string;
@@ -262,15 +274,15 @@ export function createExportService(
262
274
  const rootPostIds = roots.map((p) => p.id);
263
275
 
264
276
  const [
265
- collectionsByPost,
266
- collectionEntriesByPost,
277
+ collectionsByRoot,
278
+ collectionEntriesByThread,
267
279
  rawMediaByPost,
268
280
  slugMap,
269
281
  aliasMap,
270
282
  collectionSlugMap,
271
283
  ] = await Promise.all([
272
- services.collections.getCollectionsByPostIds(allPostIds),
273
- services.collections.getCollectionEntriesByPostIds(allPostIds),
284
+ services.collections.getCollectionsByPostIds(rootPostIds),
285
+ services.collections.getCollectionEntriesByThreadIds(rootPostIds),
274
286
  services.media.getByPostIds(allPostIds),
275
287
  services.paths.getPostSlugMap(allPostIds),
276
288
  services.paths.getPostAliases(rootPostIds),
@@ -289,7 +301,7 @@ export function createExportService(
289
301
  const collectionMetrics = buildExportedCollectionMetrics(
290
302
  allCollections,
291
303
  allPosts,
292
- collectionsByPost,
304
+ collectionsByRoot,
293
305
  );
294
306
  const exportedCollectionDirectoryItems =
295
307
  buildExportedCollectionDirectoryItems(
@@ -325,9 +337,9 @@ export function createExportService(
325
337
  const threadReplies = repliesByThread.get(root.id) ?? [];
326
338
  const rootAliases = [...(aliasMap.get(root.id) ?? [])];
327
339
 
328
- const rootCollectionEntries = buildExportedCollectionEntriesForPost(
340
+ const rootCollectionEntries = buildExportedCollectionEntriesForThread(
329
341
  root.id,
330
- collectionEntriesByPost,
342
+ collectionEntriesByThread,
331
343
  collectionSlugMap,
332
344
  collectionTitleMap,
333
345
  );
@@ -339,9 +351,6 @@ export function createExportService(
339
351
  rootAliases,
340
352
  rootCollectionEntries,
341
353
  slugMap,
342
- collectionEntriesByPost,
343
- collectionSlugMap,
344
- collectionTitleMap,
345
354
  rawMediaByPost,
346
355
  siteConfig,
347
356
  deps.storage ?? null,
@@ -353,10 +362,16 @@ export function createExportService(
353
362
  // Collection landing pages (`content/{slug}/_index.md`).
354
363
  for (const collection of allCollections) {
355
364
  const slug = collectionSlugMap.get(collection.id) ?? collection.slug;
356
- const entryCount = collectionMetrics.get(collection.id)?.postCount ?? 0;
365
+ const entryCount =
366
+ collectionMetrics.get(collection.id)?.threadCount ?? 0;
357
367
  exportFiles.push({
358
368
  path: `content/${slug}/_index.md`,
359
- content: await buildCollectionSection(collection, slug, entryCount),
369
+ content: await buildCollectionSection(
370
+ collection,
371
+ slug,
372
+ entryCount,
373
+ siteConfig.rssFeedsEnabled,
374
+ ),
360
375
  });
361
376
  }
362
377
 
@@ -375,7 +390,7 @@ export function createExportService(
375
390
  });
376
391
  exportFiles.push({
377
392
  path: "content/archive/_index.md",
378
- content: await buildArchiveSection(),
393
+ content: await buildArchiveSection(siteConfig.rssFeedsEnabled),
379
394
  });
380
395
 
381
396
  const usedSlugs = new Set<string>();
@@ -384,7 +399,7 @@ export function createExportService(
384
399
  if (!usedSlugs.has("featured")) {
385
400
  exportFiles.push({
386
401
  path: "content/featured/_index.md",
387
- content: await buildFeaturedSection(),
402
+ content: await buildFeaturedSection(siteConfig.rssFeedsEnabled),
388
403
  });
389
404
  }
390
405
 
@@ -477,6 +492,10 @@ export function createExportService(
477
492
  path: "themes/jant/layouts/partials/thread-preview.html",
478
493
  content: PARTIAL_THREAD_PREVIEW,
479
494
  });
495
+ exportFiles.push({
496
+ path: "themes/jant/layouts/partials/featured-thread.html",
497
+ content: PARTIAL_FEATURED_THREAD,
498
+ });
480
499
  exportFiles.push({
481
500
  path: "themes/jant/layouts/_default/rss.xml",
482
501
  content: LAYOUT_RSS,
@@ -625,9 +644,9 @@ async function buildSiteIconAssets(
625
644
  // Thread bundle generation
626
645
  // ---------------------------------------------------------------------------
627
646
 
628
- function buildExportedCollectionEntriesForPost(
629
- postId: string,
630
- collectionEntriesByPost: Map<
647
+ function buildExportedCollectionEntriesForThread(
648
+ threadId: string,
649
+ collectionEntriesByThread: Map<
631
650
  string,
632
651
  {
633
652
  collectionId: string;
@@ -639,7 +658,7 @@ function buildExportedCollectionEntriesForPost(
639
658
  collectionSlugMap: Map<string, string>,
640
659
  collectionTitleMap: Map<string, string>,
641
660
  ): ExportedCollectionEntry[] {
642
- const entries = collectionEntriesByPost.get(postId) ?? [];
661
+ const entries = collectionEntriesByThread.get(threadId) ?? [];
643
662
  const resolved: ExportedCollectionEntry[] = [];
644
663
  for (const entry of entries) {
645
664
  const slug = collectionSlugMap.get(entry.collectionId);
@@ -805,23 +824,21 @@ async function buildThreadBundle(
805
824
  rootAliases: string[],
806
825
  rootCollectionEntries: ExportedCollectionEntry[],
807
826
  slugMap: Map<string, string>,
808
- collectionEntriesByPost: Map<
809
- string,
810
- {
811
- collectionId: string;
812
- createdAt: number;
813
- position: number;
814
- pinnedAt: number | null;
815
- }[]
816
- >,
817
- collectionSlugMap: Map<string, string>,
818
- collectionTitleMap: Map<string, string>,
819
827
  mediaByPost: Map<string, Media[]>,
820
828
  siteConfig: SiteConfig,
821
829
  storage: StorageDriver | null,
822
830
  bundleMedia: boolean,
823
831
  ): Promise<ExportFile[]> {
824
832
  const files: ExportFile[] = [];
833
+ const featuredPosts = [root, ...threadReplies].filter(
834
+ (post) => post.status === "published" && post.featuredAt !== null,
835
+ );
836
+ const featuredPostIds = featuredPosts.map((post) => post.id);
837
+ const featuredSortAt = featuredPosts.reduce<number | null>(
838
+ (latest, post) =>
839
+ Math.max(latest ?? -1, post.publishedAt ?? post.createdAt),
840
+ null,
841
+ );
825
842
 
826
843
  // Root aliases = historical root slugs + every reply slug (so
827
844
  // /{reply-slug}/ gets a Hugo alias page that redirects/anchors to
@@ -876,6 +893,9 @@ async function buildThreadBundle(
876
893
  rating: root.rating ?? undefined,
877
894
  featured_at:
878
895
  root.featuredAt !== null ? toISOString(root.featuredAt) : undefined,
896
+ featured_post_ids: featuredPostIds.length > 0 ? featuredPostIds : undefined,
897
+ featured_sort_at:
898
+ featuredSortAt !== null ? toISOString(featuredSortAt) : undefined,
879
899
  pinned_at: root.pinnedAt !== null ? toISOString(root.pinnedAt) : undefined,
880
900
  root_aliases: rootAliases.length > 0 ? rootAliases : undefined,
881
901
  collections:
@@ -926,13 +946,6 @@ async function buildThreadBundle(
926
946
  buildMediaEmission(m, siteConfig, bundleMedia),
927
947
  );
928
948
  const replyMediaList = replyEmissions.map((e) => e.entry);
929
- const replyCollectionEntries = buildExportedCollectionEntriesForPost(
930
- reply.id,
931
- collectionEntriesByPost,
932
- collectionSlugMap,
933
- collectionTitleMap,
934
- );
935
-
936
949
  const replyFrontMatter: HugoFrontMatter = {
937
950
  id: reply.id,
938
951
  title: reply.format !== "quote" ? (reply.title ?? undefined) : undefined,
@@ -965,10 +978,6 @@ async function buildThreadBundle(
965
978
  reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
966
979
  pinned_at:
967
980
  reply.pinnedAt !== null ? toISOString(reply.pinnedAt) : undefined,
968
- collections:
969
- replyCollectionEntries.length > 0
970
- ? collectionEntriesToRefs(replyCollectionEntries)
971
- : undefined,
972
981
  media: replyMediaList.length > 0 ? replyMediaList : undefined,
973
982
  };
974
983
 
@@ -1046,22 +1055,22 @@ async function buildCollectionsSection(): Promise<string> {
1046
1055
  return `${await formatFrontMatter(frontMatter)}\n`;
1047
1056
  }
1048
1057
 
1049
- async function buildArchiveSection(): Promise<string> {
1058
+ async function buildArchiveSection(rssFeedsEnabled: boolean): Promise<string> {
1050
1059
  const frontMatter: HugoFrontMatter = {
1051
1060
  title: "Archive",
1052
1061
  type: "archive",
1053
1062
  // Opt into Atom output at /archive/index.xml.
1054
- outputs: ["html", "rss"],
1063
+ outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"],
1055
1064
  };
1056
1065
  return `${await formatFrontMatter(frontMatter)}\n`;
1057
1066
  }
1058
1067
 
1059
- async function buildFeaturedSection(): Promise<string> {
1068
+ async function buildFeaturedSection(rssFeedsEnabled: boolean): Promise<string> {
1060
1069
  const frontMatter: HugoFrontMatter = {
1061
1070
  title: "Featured",
1062
1071
  type: "featured",
1063
1072
  // Opt into Atom output at /featured/index.xml.
1064
- outputs: ["html", "rss"],
1073
+ outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"],
1065
1074
  };
1066
1075
  return `${await formatFrontMatter(frontMatter)}\n`;
1067
1076
  }
@@ -1070,6 +1079,7 @@ async function buildCollectionSection(
1070
1079
  collection: Collection,
1071
1080
  slug: string,
1072
1081
  entryCount: number,
1082
+ rssFeedsEnabled: boolean,
1073
1083
  ): Promise<string> {
1074
1084
  const frontMatter: HugoFrontMatter = {
1075
1085
  title: collection.title,
@@ -1079,7 +1089,7 @@ async function buildCollectionSection(
1079
1089
  sort_order: collection.sortOrder,
1080
1090
  entry_count: entryCount,
1081
1091
  // Opt into Atom output at /{slug}/index.xml.
1082
- outputs: ["html", "rss"],
1092
+ outputs: rssFeedsEnabled ? ["html", "rss"] : ["html"],
1083
1093
  };
1084
1094
  return `${await formatFrontMatter(frontMatter)}\n`;
1085
1095
  }
@@ -1248,7 +1258,11 @@ function buildExportedCollectionDirectoryItems(
1248
1258
  sequence: sequenceLabels[index] ?? "",
1249
1259
  label: item.label,
1250
1260
  url: item.url,
1251
- descriptionHtml: description ? renderMarkdown(description) : null,
1261
+ descriptionHtml: description
1262
+ ? renderMarkdown(description, {
1263
+ namespace: `collection-directory-link-${sequenceLabels[index] ?? index}`,
1264
+ })
1265
+ : null,
1252
1266
  });
1253
1267
  return;
1254
1268
  }
@@ -1273,12 +1287,12 @@ function buildExportedCollectionDirectoryItems(
1273
1287
  slug,
1274
1288
  title: collection.title || slug,
1275
1289
  descriptionHtml: collectionDescription
1276
- ? renderMarkdown(collectionDescription)
1290
+ ? renderMarkdown(collectionDescription, { namespace: collection.id })
1277
1291
  : null,
1278
1292
  entryCount:
1279
- metrics?.postCount ??
1280
- (typeof collection.postCount === "number"
1281
- ? collection.postCount
1293
+ metrics?.threadCount ??
1294
+ (typeof collection.threadCount === "number"
1295
+ ? collection.threadCount
1282
1296
  : undefined),
1283
1297
  recentActivityLabel: formatCollectionActivityLabel(activityTimestamp),
1284
1298
  recentActivityIso: formatCollectionActivityIso(activityTimestamp),
@@ -1291,13 +1305,13 @@ function buildExportedCollectionDirectoryItems(
1291
1305
  function buildExportedCollectionMetrics(
1292
1306
  collections: readonly Collection[],
1293
1307
  posts: readonly Post[],
1294
- collectionsByPost: ReadonlyMap<string, readonly Collection[]>,
1308
+ collectionsByThread: ReadonlyMap<string, readonly Collection[]>,
1295
1309
  ): Map<string, ExportedCollectionMetrics> {
1296
1310
  const metrics = new Map<string, ExportedCollectionMetrics>();
1297
1311
 
1298
1312
  for (const collection of collections) {
1299
1313
  metrics.set(collection.id, {
1300
- postCount: 0,
1314
+ threadCount: 0,
1301
1315
  recentActivityAt: collection.updatedAt,
1302
1316
  });
1303
1317
  }
@@ -1317,15 +1331,15 @@ function buildExportedCollectionMetrics(
1317
1331
  post.publishedAt ??
1318
1332
  post.updatedAt ??
1319
1333
  post.createdAt;
1320
- const postCollections = collectionsByPost.get(post.id) ?? [];
1334
+ const threadCollections = collectionsByThread.get(post.id) ?? [];
1321
1335
 
1322
- for (const collection of postCollections) {
1336
+ for (const collection of threadCollections) {
1323
1337
  const current = metrics.get(collection.id);
1324
1338
  if (!current) {
1325
1339
  continue;
1326
1340
  }
1327
1341
 
1328
- if (current.postCount === 0) {
1342
+ if (current.threadCount === 0) {
1329
1343
  current.recentActivityAt = activityAt;
1330
1344
  } else {
1331
1345
  current.recentActivityAt = Math.max(
@@ -1333,7 +1347,7 @@ function buildExportedCollectionMetrics(
1333
1347
  activityAt,
1334
1348
  );
1335
1349
  }
1336
- current.postCount += 1;
1350
+ current.threadCount += 1;
1337
1351
  }
1338
1352
  }
1339
1353
 
@@ -1344,27 +1358,11 @@ function buildExportedCollectionMetrics(
1344
1358
  // Nav item resolution
1345
1359
  // ---------------------------------------------------------------------------
1346
1360
 
1347
- /**
1348
- * System nav items on the main site store an empty `label` in the DB and
1349
- * resolve their display text at render time through i18n
1350
- * (`getNavItemDisplayLabel`). The Hugo export has no i18n runtime, so fall
1351
- * back to these English defaults when serializing to `data/jant.toml`.
1352
- * Users can still override by setting a custom label on the nav item.
1353
- */
1354
- const SYSTEM_NAV_FALLBACK_LABELS: Record<string, string> = {
1355
- latest: "Latest",
1356
- featured: "Featured",
1357
- collections: "Collections",
1358
- archive: "Archive",
1359
- rss: "RSS",
1360
- settings: "Settings",
1361
- };
1362
-
1363
1361
  function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
1364
1362
  if (item.label) return item.label;
1365
1363
  if (item.systemKey) {
1366
- const fallback = SYSTEM_NAV_FALLBACK_LABELS[item.systemKey];
1367
- if (fallback) return fallback;
1364
+ const definition = SYSTEM_NAV_KEYS[item.systemKey as SystemNavKey];
1365
+ if (definition) return definition.defaultLabel;
1368
1366
  }
1369
1367
  return item.label;
1370
1368
  }
@@ -1447,7 +1445,7 @@ function buildHugoToml(config: SiteConfig): string {
1447
1445
  // Override the built-in RSS output format to emit Atom instead of
1448
1446
  // RSS 2.0 so the wire format mirrors the main site's `lib/feed.ts`.
1449
1447
  "[outputs]",
1450
- ' home = ["html", "rss"]',
1448
+ ` home = ["html"${config.rssFeedsEnabled ? ', "rss"' : ""}]`,
1451
1449
  // Hugo's default for sections is ["html", "rss"] — without overriding
1452
1450
  // it here every root post (which is a section) would get its own
1453
1451
  // /{slug}/index.xml. Turn sections off by default and re-enable RSS
@@ -1472,6 +1470,8 @@ function buildHugoToml(config: SiteConfig): string {
1472
1470
  "[params]",
1473
1471
  ` description = "${escapeTomlString(config.siteDescription)}"`,
1474
1472
  ` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1473
+ ` public_api_enabled = ${config.publicApiEnabled}`,
1474
+ ` rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1475
1475
  ` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
1476
1476
  ` show_header_avatar = ${config.showHeaderAvatar}`,
1477
1477
  ` noindex = ${config.noindex}`,
@@ -1502,7 +1502,9 @@ function buildJantDataToml(
1502
1502
  iconAssets: SiteIconAssets,
1503
1503
  directoryItems: readonly ExportedCollectionDirectoryItem[],
1504
1504
  ): string {
1505
- const footerHtml = config.siteFooter ? renderMarkdown(config.siteFooter) : "";
1505
+ const footerHtml = config.siteFooter
1506
+ ? renderMarkdown(config.siteFooter, { namespace: "site-footer" })
1507
+ : "";
1506
1508
  const parts: string[] = [
1507
1509
  'format = "jant-site"',
1508
1510
  "version = 1",
@@ -1511,6 +1513,8 @@ function buildJantDataToml(
1511
1513
  `site_description = "${escapeTomlString(config.siteDescription)}"`,
1512
1514
  `site_language = "${escapeTomlString(config.siteLanguage)}"`,
1513
1515
  `main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1516
+ `public_api_enabled = ${config.publicApiEnabled}`,
1517
+ `rss_feeds_enabled = ${config.rssFeedsEnabled}`,
1514
1518
  `show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
1515
1519
  `show_header_avatar = ${config.showHeaderAvatar}`,
1516
1520
  `noindex = ${config.noindex}`,
@@ -1548,6 +1552,13 @@ function buildJantDataToml(
1548
1552
  // `settings` is authenticated-only and has no corresponding page in the
1549
1553
  // static Hugo site — drop it at export time so it never shows up in nav.
1550
1554
  if (item.systemKey === "settings") continue;
1555
+ if (
1556
+ !config.rssFeedsEnabled &&
1557
+ item.type === "system" &&
1558
+ item.systemKey === "rss"
1559
+ ) {
1560
+ continue;
1561
+ }
1551
1562
  parts.push("");
1552
1563
  parts.push("[[nav]]");
1553
1564
  parts.push(`type = "${escapeTomlString(item.type)}"`);
@@ -132,12 +132,12 @@ const UploadMediaToolSchema = z.object({
132
132
  posterBase64: z.string().min(1).optional(),
133
133
  });
134
134
 
135
- const AddCollectionPostToolSchema = z.object({
135
+ const AddCollectionThreadToolSchema = z.object({
136
136
  collectionId: CollectionIdSchema,
137
- postId: PostIdSchema,
137
+ threadId: PostIdSchema,
138
138
  });
139
139
 
140
- const RemoveCollectionPostToolSchema = AddCollectionPostToolSchema;
140
+ const RemoveCollectionThreadToolSchema = AddCollectionThreadToolSchema;
141
141
 
142
142
  const mcpTools: McpToolDefinition[] = [
143
143
  {
@@ -174,7 +174,8 @@ const mcpTools: McpToolDefinition[] = [
174
174
  },
175
175
  {
176
176
  name: "jant_posts_get",
177
- description: "Get one post, including attachments and collection IDs.",
177
+ description:
178
+ "Get one post, including attachments and shared Thread collection IDs.",
178
179
  inputSchema: {
179
180
  type: "object",
180
181
  properties: {
@@ -687,55 +688,64 @@ const mcpTools: McpToolDefinition[] = [
687
688
  },
688
689
  },
689
690
  {
690
- name: "jant_collections_add_post",
691
- description: "Add a post to a collection.",
691
+ name: "jant_collections_add_thread",
692
+ description: "Add a thread to a collection.",
692
693
  inputSchema: {
693
694
  type: "object",
694
695
  properties: {
695
696
  collectionId: { type: "string", description: "Collection TypeID" },
696
- postId: { type: "string", description: "Post TypeID" },
697
+ threadId: {
698
+ type: "string",
699
+ description: "Root or child Post TypeID in the Thread",
700
+ },
697
701
  },
698
- required: ["collectionId", "postId"],
702
+ required: ["collectionId", "threadId"],
699
703
  additionalProperties: false,
700
704
  },
701
705
  async execute(args, context) {
702
- const input = AddCollectionPostToolSchema.parse(args ?? {});
703
- const [collection, post] = await Promise.all([
704
- context.services.collections.getById(input.collectionId),
705
- context.services.posts.getById(input.postId),
706
- ]);
706
+ const input = AddCollectionThreadToolSchema.parse(args ?? {});
707
+ const collection = await context.services.collections.getById(
708
+ input.collectionId,
709
+ );
707
710
 
708
711
  if (!collection) {
709
712
  throw new NotFoundError("Collection");
710
713
  }
711
- if (!post) {
712
- throw new NotFoundError("Post");
713
- }
714
714
 
715
- await context.services.collections.addPost(
715
+ await context.services.collections.addThread(
716
716
  input.collectionId,
717
- input.postId,
717
+ input.threadId,
718
718
  );
719
719
  return { success: true };
720
720
  },
721
721
  },
722
722
  {
723
- name: "jant_collections_remove_post",
724
- description: "Remove a post from a collection.",
723
+ name: "jant_collections_remove_thread",
724
+ description: "Remove a thread from a collection.",
725
725
  inputSchema: {
726
726
  type: "object",
727
727
  properties: {
728
728
  collectionId: { type: "string", description: "Collection TypeID" },
729
- postId: { type: "string", description: "Post TypeID" },
729
+ threadId: {
730
+ type: "string",
731
+ description: "Root or child Post TypeID in the Thread",
732
+ },
730
733
  },
731
- required: ["collectionId", "postId"],
734
+ required: ["collectionId", "threadId"],
732
735
  additionalProperties: false,
733
736
  },
734
737
  async execute(args, context) {
735
- const input = RemoveCollectionPostToolSchema.parse(args ?? {});
736
- await context.services.collections.removePost(
738
+ const input = RemoveCollectionThreadToolSchema.parse(args ?? {});
739
+ const collection = await context.services.collections.getById(
740
+ input.collectionId,
741
+ );
742
+ if (!collection) {
743
+ throw new NotFoundError("Collection");
744
+ }
745
+
746
+ await context.services.collections.removeThread(
737
747
  input.collectionId,
738
- input.postId,
748
+ input.threadId,
739
749
  );
740
750
  return { success: true };
741
751
  },
@@ -1287,14 +1297,14 @@ async function serializePost(
1287
1297
  post: Parameters<typeof toApiPost>[0],
1288
1298
  context: McpToolContext,
1289
1299
  ) {
1290
- const [mediaList, postCollections] = await Promise.all([
1300
+ const [mediaList, threadCollections] = await Promise.all([
1291
1301
  context.services.media.getByPostId(post.id),
1292
1302
  context.services.collections.getCollectionsByPostId(post.id),
1293
1303
  ]);
1294
1304
 
1295
1305
  return toApiPost(post, {
1296
1306
  attachments: serializeAttachments(mediaList, context.appConfig),
1297
- collectionIds: postCollections.map((collection) => collection.id),
1307
+ collectionIds: threadCollections.map((collection) => collection.id),
1298
1308
  });
1299
1309
  }
1300
1310
 
@@ -1005,7 +1005,7 @@ export function createMediaService(
1005
1005
 
1006
1006
  const markdown = await new Response(object.body).text();
1007
1007
  const tiptapJson = markdownToTiptapJson(markdown);
1008
- const html = renderTiptapJson(tiptapJson);
1008
+ const html = renderTiptapJson(tiptapJson, { namespace: record.id });
1009
1009
 
1010
1010
  return {
1011
1011
  id: record.id,