@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -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-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { pageRoutes } from "../page.js";
4
+
5
+ function createPageTestApp(options: { authenticated?: boolean } = {}) {
6
+ const testApp = createTestApp(options);
7
+ testApp.app.route("/", pageRoutes);
8
+ return testApp;
9
+ }
10
+
11
+ async function createDraft(
12
+ services: ReturnType<typeof createTestApp>["services"],
13
+ title = "A private draft",
14
+ ) {
15
+ return services.posts.create({
16
+ format: "note",
17
+ title,
18
+ bodyMarkdown: `${title} body`,
19
+ status: "draft",
20
+ });
21
+ }
22
+
23
+ describe("draft preview route", () => {
24
+ it("redirects anonymous visitors to sign in and preserves the preview path", async () => {
25
+ const { app, services } = createPageTestApp();
26
+ const draft = await createDraft(services);
27
+
28
+ const res = await app.request(`/preview/${draft.slug}`, {
29
+ redirect: "manual",
30
+ });
31
+
32
+ expect(res.status).toBe(302);
33
+ expect(res.headers.get("Location")).toBe(
34
+ `/signin?redirect=${encodeURIComponent(`/preview/${draft.slug}`)}`,
35
+ );
36
+ });
37
+
38
+ it("renders the public post view with explicit private preview signals", async () => {
39
+ const { app, services } = createPageTestApp({ authenticated: true });
40
+ const draft = await createDraft(services);
41
+
42
+ const res = await app.request(`/preview/${draft.slug}`);
43
+
44
+ expect(res.status).toBe(200);
45
+ expect(res.headers.get("Cache-Control")).toBe("private, no-store");
46
+ expect(res.headers.get("X-Robots-Tag")).toBe("noindex, nofollow");
47
+
48
+ const html = await res.text();
49
+ expect(html).toContain(`data-post-id="${draft.id}"`);
50
+ expect(html).toContain("A private draft body");
51
+ expect(html).toContain("data-preview-status");
52
+ expect(html).toContain("Draft preview");
53
+ expect(html).toContain("This post isn’t published.");
54
+ expect(html).toContain("Edit draft");
55
+ expect(html).toContain(`/preview/${draft.slug}?edit=1`);
56
+ expect(html).toContain('<meta name="robots" content="noindex, nofollow"');
57
+ expect(html).not.toContain('rel="canonical"');
58
+ expect(html).not.toContain("application/ld+json");
59
+ expect(html).not.toContain("article:published_time");
60
+ expect(html).toContain('id="compose-dialog"');
61
+ });
62
+
63
+ it("renders every draft in a saved thread", async () => {
64
+ const { app, services } = createPageTestApp({ authenticated: true });
65
+ const root = await createDraft(services, "Thread root");
66
+ const reply = await services.posts.create({
67
+ format: "quote",
68
+ quoteText: "Unpublished reply quote",
69
+ status: "draft",
70
+ replyToId: root.id,
71
+ });
72
+
73
+ const res = await app.request(`/preview/${root.slug}`);
74
+
75
+ expect(res.status).toBe(200);
76
+ const html = await res.text();
77
+ expect(html).toContain("Thread root body");
78
+ expect(html).toContain("Unpublished reply quote");
79
+ expect(html).toContain(`/preview/${reply.slug}`);
80
+ });
81
+
82
+ it("redirects a published post preview to its public permalink", async () => {
83
+ const { app, services } = createPageTestApp({ authenticated: true });
84
+ const post = await services.posts.create({
85
+ format: "note",
86
+ bodyMarkdown: "Already public",
87
+ status: "published",
88
+ });
89
+
90
+ const res = await app.request(`/preview/${post.slug}`, {
91
+ redirect: "manual",
92
+ });
93
+
94
+ expect(res.status).toBe(302);
95
+ expect(res.headers.get("Location")).toBe(`/${post.slug}`);
96
+ });
97
+
98
+ it("returns 404 for the preview root and unknown draft slugs", async () => {
99
+ const { app } = createPageTestApp({ authenticated: true });
100
+
101
+ expect((await app.request("/preview")).status).toBe(404);
102
+ expect((await app.request("/preview/missing-draft")).status).toBe(404);
103
+ });
104
+ });
105
+
106
+ describe("reserved path and redirect precedence", () => {
107
+ it("ignores a legacy redirect that collides with the preview route", async () => {
108
+ const { app, services } = createPageTestApp({ authenticated: true });
109
+ const draft = await createDraft(services);
110
+ await services.paths.create({
111
+ path: `preview/${draft.slug}`,
112
+ kind: "redirect",
113
+ redirectToPath: "elsewhere",
114
+ redirectType: 301,
115
+ });
116
+
117
+ const res = await app.request(`/preview/${draft.slug}`, {
118
+ redirect: "manual",
119
+ });
120
+
121
+ expect(res.status).toBe(200);
122
+ expect(await res.text()).toContain(`data-post-id="${draft.id}"`);
123
+ });
124
+
125
+ it("makes a legacy canonical preview path inert", async () => {
126
+ const { app, services } = createPageTestApp({ authenticated: true });
127
+ const post = await services.posts.create({
128
+ format: "note",
129
+ bodyMarkdown: "Old preview post",
130
+ status: "published",
131
+ });
132
+ await services.paths.updatePostSlug(post.id, "preview");
133
+
134
+ const res = await app.request("/preview", { redirect: "manual" });
135
+
136
+ expect(res.status).toBe(404);
137
+ });
138
+
139
+ it("keeps ordinary redirects working and ahead of derived collection feeds", async () => {
140
+ const { app, services } = createPageTestApp();
141
+ const collection = await services.collections.create({
142
+ slug: "reading",
143
+ title: "Reading",
144
+ });
145
+ const post = await services.posts.create({
146
+ format: "note",
147
+ bodyMarkdown: "Book log",
148
+ status: "published",
149
+ });
150
+ await services.collections.addThread(collection.id, post.id);
151
+ await services.paths.create({
152
+ path: "reading/feed",
153
+ kind: "redirect",
154
+ redirectToPath: "archive",
155
+ redirectType: 302,
156
+ });
157
+ await services.paths.create({
158
+ path: "old-link",
159
+ kind: "redirect",
160
+ redirectToPath: "reading",
161
+ redirectType: 301,
162
+ });
163
+
164
+ const feedRes = await app.request("/reading/feed", {
165
+ redirect: "manual",
166
+ });
167
+ expect(feedRes.status).toBe(302);
168
+ expect(feedRes.headers.get("Location")).toBe("/archive");
169
+
170
+ const ordinaryRes = await app.request("/old-link", {
171
+ redirect: "manual",
172
+ });
173
+ expect(ordinaryRes.status).toBe(301);
174
+ expect(ordinaryRes.headers.get("Location")).toBe("/reading");
175
+ });
176
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { searchRoutes } from "../search.js";
4
+
5
+ describe("Search Page Routes", () => {
6
+ it("keeps public HTML search available when anonymous JSON APIs are off", async () => {
7
+ const { app, services } = createTestApp({
8
+ authenticated: false,
9
+ fts: true,
10
+ });
11
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
12
+ app.route("/search", searchRoutes);
13
+
14
+ const response = await app.request("/search?q=marker");
15
+
16
+ expect(response.status).toBe(200);
17
+ });
18
+
19
+ it("hides visible Thread collection tags on a matching child post", async () => {
20
+ const { app, services } = createTestApp({ fts: true });
21
+ app.route("/search", searchRoutes);
22
+
23
+ const collection = await services.collections.create({
24
+ slug: "search-thread",
25
+ title: "Search Thread Collection",
26
+ });
27
+ const root = await services.posts.create({
28
+ format: "note",
29
+ bodyMarkdown: "Thread root",
30
+ });
31
+ await services.posts.create({
32
+ format: "note",
33
+ bodyMarkdown: "Unique child search marker",
34
+ replyToId: root.id,
35
+ });
36
+ await services.collections.addThread(collection.id, root.id);
37
+
38
+ const response = await app.request("/search?q=marker");
39
+
40
+ expect(response.status).toBe(200);
41
+ const html = await response.text();
42
+ expect(html).toContain("Unique child search");
43
+ expect(html).toContain("<mark>marker</mark>");
44
+ expect(html).not.toContain("Search Thread Collection");
45
+ });
46
+
47
+ it("shows Thread collection tags on a matching root post", async () => {
48
+ const { app, services } = createTestApp({ fts: true });
49
+ app.route("/search", searchRoutes);
50
+
51
+ const collection = await services.collections.create({
52
+ slug: "search-root",
53
+ title: "Root Search Collection",
54
+ });
55
+ const root = await services.posts.create({
56
+ format: "note",
57
+ bodyMarkdown: "Unique root search marker",
58
+ });
59
+ await services.collections.addThread(collection.id, root.id);
60
+
61
+ const response = await app.request("/search?q=marker");
62
+
63
+ expect(response.status).toBe(200);
64
+ const html = await response.text();
65
+ expect(html).toContain("Unique root search");
66
+ expect(html).toContain("<mark>marker</mark>");
67
+ expect(html).toContain("Root Search Collection");
68
+ });
69
+ });
@@ -21,16 +21,22 @@ import type {
21
21
  import type { AppVariables } from "../../types/app-context.js";
22
22
  import type {
23
23
  ArchiveFilters,
24
+ ArchiveSort,
24
25
  ArchiveView,
25
26
  ArchiveVisibility,
26
27
  } from "../../types/props.js";
27
28
  import { FORMATS, MEDIA_KINDS } from "../../types.js";
28
29
  import { ArchivePage } from "../../ui/pages/ArchivePage.js";
29
30
  import { defaultFeedRenderer } from "../../lib/feed.js";
31
+ import {
32
+ getFeedEntryUpdatedAt,
33
+ getRssPublishedBefore,
34
+ RSS_FEED_CACHE_CONTROL,
35
+ } from "../../lib/feed-policy.js";
30
36
  import { getNavigationData } from "../../lib/navigation.js";
31
37
  import { buildPageTitle } from "../../lib/page-title.js";
32
38
  import { renderPublicPage } from "../../lib/render.js";
33
- import { formatYearMonth, toISOString } from "../../lib/time.js";
39
+ import { formatYearMonth } from "../../lib/time.js";
34
40
  import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
35
41
  import {
36
42
  createMediaContext,
@@ -61,6 +67,7 @@ interface ParsedArchiveParams {
61
67
  visibility?: ArchiveVisibility;
62
68
  visibilityAll: boolean;
63
69
  view?: ArchiveView;
70
+ sort: ArchiveSort;
64
71
  currentPage: number;
65
72
  }
66
73
 
@@ -139,6 +146,11 @@ function parseArchiveParams(
139
146
  ? viewParam
140
147
  : undefined;
141
148
 
149
+ // sort selects the time axis for ordering, month grouping, and the year
150
+ // filter alike — they must stay on the same column or the month headers
151
+ // stop agreeing with the order inside them.
152
+ const sort: ArchiveSort = q("sort") === "updated" ? "updated" : "published";
153
+
142
154
  // Page always comes from the actual request URL (pagination links use ?page=N)
143
155
  const pageParam = c.req.query("page");
144
156
  const currentPage = Math.max(1, parseInt(pageParam || "1", 10) || 1);
@@ -154,6 +166,7 @@ function parseArchiveParams(
154
166
  visibility,
155
167
  visibilityAll,
156
168
  view,
169
+ sort,
157
170
  currentPage,
158
171
  };
159
172
  }
@@ -182,12 +195,15 @@ function buildArchivePostFilters(
182
195
  : (params.visibility ?? undefined)
183
196
  : undefined;
184
197
 
185
- let publishedAfter: number | undefined;
186
- let publishedBefore: number | undefined;
187
- if (params.validYear) {
188
- publishedAfter = Date.UTC(params.validYear, 0, 1) / 1000;
189
- publishedBefore = Date.UTC(params.validYear + 1, 0, 1) / 1000;
190
- }
198
+ // The year filter follows the active axis, so every month bucket shown
199
+ // under `year=N` really belongs to that year.
200
+ const yearRange = params.validYear
201
+ ? {
202
+ after: Date.UTC(params.validYear, 0, 1) / 1000,
203
+ before: Date.UTC(params.validYear + 1, 0, 1) / 1000,
204
+ }
205
+ : undefined;
206
+ const sortsByActivity = params.sort === "updated";
191
207
 
192
208
  return {
193
209
  format: params.format,
@@ -201,18 +217,28 @@ function buildArchivePostFilters(
201
217
  ? { visibility: effectiveVisibility }
202
218
  : {}),
203
219
  collectionId,
204
- publishedAfter,
205
- publishedBefore,
220
+ ...(yearRange
221
+ ? sortsByActivity
222
+ ? { axisAfter: yearRange.after, axisBefore: yearRange.before }
223
+ : { publishedAfter: yearRange.after, publishedBefore: yearRange.before }
224
+ : {}),
206
225
  mediaKinds: params.mediaKinds,
207
226
  hasMedia: params.hasMedia,
208
227
  hasTitle: params.hasTitle,
209
228
  hasReplies: params.hasReplies,
229
+ // "thread_updated", not "activity": the archive is the canonical all-posts
230
+ // view, and the quiet-reply switch only promises not to move a Thread on
231
+ // Latest. Here the honest answer is when the Thread last changed.
232
+ sortBy: sortsByActivity ? "thread_updated" : "published",
233
+ ignorePinnedSort: true,
210
234
  };
211
235
  }
212
236
 
213
237
  /**
214
238
  * Build a query string from parsed archive params (for feed self-URL and
215
- * archive page feed link). Omits page-only params (view, page).
239
+ * archive page feed link). Omits view and page — those shape the rendered
240
+ * page, not the result set — but carries `sort`, so the feed button on an
241
+ * updated-sorted page hands back the matching feed.
216
242
  */
217
243
  function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
218
244
  const qs = new URLSearchParams();
@@ -230,6 +256,7 @@ function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
230
256
  if (params.hasReplies !== undefined) {
231
257
  qs.set("replies", params.hasReplies ? "any" : "none");
232
258
  }
259
+ if (params.sort === "updated") qs.set("sort", "updated");
233
260
  const str = qs.toString();
234
261
  return str ? `?${str}` : "";
235
262
  }
@@ -331,6 +358,7 @@ export async function renderArchivePage(
331
358
  services.posts.getDistinctYears({
332
359
  status: "published",
333
360
  excludeReplies: true,
361
+ sortBy: filters.sortBy,
334
362
  }),
335
363
  services.collections.list(),
336
364
  ]);
@@ -358,8 +386,12 @@ export async function renderArchivePage(
358
386
 
359
387
  const grouped = new Map<string, PostWithMedia[]>();
360
388
  for (const post of posts) {
361
- const publishedAt = post.publishedAt ?? post.updatedAt;
362
- const key = formatYearMonth(publishedAt, appConfig.timeZone);
389
+ // Bucket on the same axis the query sorted by, so groups stay ordered.
390
+ const groupedAt =
391
+ params.sort === "updated"
392
+ ? post.threadUpdatedAt
393
+ : (post.publishedAt ?? post.updatedAt);
394
+ const key = formatYearMonth(groupedAt, appConfig.timeZone);
363
395
  if (!grouped.has(key)) {
364
396
  grouped.set(key, []);
365
397
  }
@@ -431,6 +463,7 @@ export async function renderArchivePage(
431
463
  hasReplies: params.hasReplies,
432
464
  visibility: effectiveVisibility,
433
465
  view: params.view,
466
+ sort: params.sort === "updated" ? "updated" : undefined,
434
467
  };
435
468
 
436
469
  const feedQuery = buildArchiveFeedQuery(params);
@@ -441,7 +474,12 @@ export async function renderArchivePage(
441
474
  }));
442
475
 
443
476
  return renderPublicPage(c, {
444
- title: buildPageTitle("Archive", navData.siteName),
477
+ // Distinguishes a bookmarked or shared ?sort=updated view in the tab bar.
478
+ title: buildPageTitle(
479
+ "Archive",
480
+ params.sort === "updated" ? "Recently updated" : undefined,
481
+ navData.siteName,
482
+ ),
445
483
  navData,
446
484
  content: (
447
485
  <ArchivePage
@@ -456,7 +494,9 @@ export async function renderArchivePage(
456
494
  isAuthenticated={navData.isAuthenticated}
457
495
  sitePathPrefix={navData.sitePathPrefix}
458
496
  timeZone={appConfig.timeZone}
459
- feedHref={`/archive/feed${feedQuery}`}
497
+ feedHref={
498
+ appConfig.rssFeedsEnabled ? `/archive/feed${feedQuery}` : undefined
499
+ }
460
500
  />
461
501
  ),
462
502
  });
@@ -615,10 +655,27 @@ async function buildArchiveFeedData(
615
655
  const collection = params.collectionSlug
616
656
  ? await services.collections.getBySlug(params.collectionSlug)
617
657
  : undefined;
658
+ const rssPublishedBefore = getRssPublishedBefore(
659
+ appConfig.rssPublishDelaySeconds,
660
+ );
661
+ const yearPublishedBefore = params.validYear
662
+ ? Date.UTC(params.validYear + 1, 0, 1) / 1000
663
+ : undefined;
618
664
 
619
665
  // Feed mirrors the unauthenticated archive page: published + non-private,
620
666
  // including Hidden-from-Latest. /archive is the canonical "all posts" view,
621
667
  // so its feed must match.
668
+ //
669
+ // Ordered by publication by default, like the page it belongs to. Ordering
670
+ // by activity would make the feed's contents shift under a fixed
671
+ // `rssFeedLimit` — a new reply pulls an old Thread back into the window and
672
+ // pushes something else out — and it buys nothing in return, because readers
673
+ // key entries by id and will not re-surface one that merely moved.
674
+ // /latest/feed is the activity feed; this one is the chronological record.
675
+ //
676
+ // `?sort=updated` opts into the same axis the page uses, for a subscriber
677
+ // who wants that trade-off deliberately.
678
+ const sortsByActivity = params.sort === "updated";
622
679
  const filters: PostFilters = {
623
680
  format: params.format,
624
681
  status: "published",
@@ -630,13 +687,22 @@ async function buildArchiveFeedData(
630
687
  hasMedia: params.hasMedia,
631
688
  hasTitle: params.hasTitle,
632
689
  hasReplies: params.hasReplies,
690
+ sortBy: sortsByActivity ? "thread_updated" : "published",
633
691
  ignorePinnedSort: true,
692
+ // The year filter follows the active axis; the RSS delay always stays on
693
+ // publication, so a just-published post is never announced early.
634
694
  ...(params.validYear
635
- ? {
636
- publishedAfter: Date.UTC(params.validYear, 0, 1) / 1000,
637
- publishedBefore: Date.UTC(params.validYear + 1, 0, 1) / 1000,
638
- }
695
+ ? sortsByActivity
696
+ ? {
697
+ axisAfter: Date.UTC(params.validYear, 0, 1) / 1000,
698
+ axisBefore: yearPublishedBefore,
699
+ }
700
+ : { publishedAfter: Date.UTC(params.validYear, 0, 1) / 1000 }
639
701
  : {}),
702
+ publishedBefore:
703
+ yearPublishedBefore === undefined || sortsByActivity
704
+ ? rssPublishedBefore
705
+ : Math.min(yearPublishedBefore, rssPublishedBefore),
640
706
  limit: appConfig.rssFeedLimit,
641
707
  };
642
708
 
@@ -648,7 +714,9 @@ async function buildArchiveFeedData(
648
714
  // Batch load media, aliases, and thread replies
649
715
  const postIds = posts.map((p) => p.id);
650
716
  const [threadMap, rawMediaMap, aliasesMap] = await Promise.all([
651
- services.posts.getPublishedThreads(rootIds),
717
+ services.posts.getPublishedThreads(rootIds, {
718
+ publishedBefore: rssPublishedBefore,
719
+ }),
652
720
  services.media.getByPostIds(postIds),
653
721
  services.paths.getPostAliases(postIds),
654
722
  ]);
@@ -702,9 +770,9 @@ async function buildArchiveFeedData(
702
770
  aliasMap,
703
771
  ).map((postView, index) => {
704
772
  const post = posts[index] as (typeof posts)[number];
773
+ const thread = threadMap.get(post.id);
705
774
 
706
775
  // Build thread replies
707
- const thread = threadMap.get(post.id);
708
776
  const replies =
709
777
  thread && thread.length > 1
710
778
  ? toPostViews(
@@ -720,7 +788,7 @@ async function buildArchiveFeedData(
720
788
 
721
789
  return {
722
790
  ...postView,
723
- feedUpdatedAt: toISOString(post.lastActivityAt),
791
+ feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
724
792
  threadReplies: replies,
725
793
  };
726
794
  });
@@ -748,7 +816,7 @@ archiveRoutes.get("/feed", async (c) => {
748
816
  return new Response(defaultFeedRenderer(feedData), {
749
817
  headers: {
750
818
  "Content-Type": "application/atom+xml; charset=utf-8",
751
- "Cache-Control": "public, max-age=180",
819
+ "Cache-Control": RSS_FEED_CACHE_CONTROL,
752
820
  },
753
821
  });
754
822
  });
@@ -19,9 +19,13 @@ import {
19
19
  } from "../../lib/collection-sort.js";
20
20
  import { assembleCollectionTimeline } from "../../lib/timeline.js";
21
21
  import { defaultFeedRenderer } from "../../lib/feed.js";
22
+ import {
23
+ getFeedEntryUpdatedAt,
24
+ getRssPublishedBefore,
25
+ RSS_FEED_CACHE_CONTROL,
26
+ } from "../../lib/feed-policy.js";
22
27
  import { toPlainText as markdownToPlainText } from "../../lib/markdown.js";
23
28
  import { buildMediaMap } from "../../lib/media-helpers.js";
24
- import { toISOString } from "../../lib/time.js";
25
29
  import { createMediaContext, toPostViews } from "../../lib/view.js";
26
30
  import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
27
31
  import {
@@ -147,16 +151,17 @@ export async function renderCollectionPage(
147
151
  );
148
152
  const isAggregate = selection.collections.length > 1;
149
153
 
150
- const ratedPostCount = await c.var.services.posts.countUpTo(
151
- {
154
+ const ratedThreadCount =
155
+ await c.var.services.posts.countCollectionThreadRootsUpToForCollections(
152
156
  collectionIds,
153
- status: "published",
154
- excludePrivate: !navData.isAuthenticated,
155
- hasRating: true,
156
- },
157
- 2,
158
- );
159
- const showRatingSort = supportsCollectionRatingSort(ratedPostCount);
157
+ {
158
+ status: "published",
159
+ excludePrivate: !navData.isAuthenticated,
160
+ hasRating: true,
161
+ },
162
+ 2,
163
+ );
164
+ const showRatingSort = supportsCollectionRatingSort(ratedThreadCount);
160
165
  const requestedDefaultSort = isAggregate
161
166
  ? "newest"
162
167
  : primaryCollection.sortOrder;
@@ -219,7 +224,17 @@ export async function renderCollectionPage(
219
224
  defaultSort={defaultSort}
220
225
  showRatingSort={showRatingSort}
221
226
  isAuthenticated={navData.isAuthenticated}
227
+ isInNavigation={navData.links.some(
228
+ (item) =>
229
+ item.type === "collection" &&
230
+ item.collectionId === primaryCollection.id,
231
+ )}
222
232
  sitePathPrefix={navData.sitePathPrefix}
233
+ feedHref={
234
+ c.var.appConfig.rssFeedsEnabled
235
+ ? `${canonicalPagePath}/feed`
236
+ : undefined
237
+ }
223
238
  />
224
239
  ),
225
240
  });
@@ -254,6 +269,9 @@ export async function renderCollectionFeed(
254
269
  const siteUrl = appConfig.siteUrl;
255
270
  const siteLanguage = appConfig.siteLanguage;
256
271
  const feedLimit = appConfig.rssFeedLimit;
272
+ const publishedBefore = getRssPublishedBefore(
273
+ appConfig.rssPublishDelaySeconds,
274
+ );
257
275
  const primaryCollection = selection.collections[0];
258
276
  if (!primaryCollection) return null;
259
277
 
@@ -264,6 +282,7 @@ export async function renderCollectionFeed(
264
282
  status: "published",
265
283
  excludePrivate: true,
266
284
  ignoreCollectionPinnedSort: true,
285
+ publishedBefore,
267
286
  limit: feedLimit,
268
287
  },
269
288
  );
@@ -274,7 +293,7 @@ export async function renderCollectionFeed(
274
293
 
275
294
  const postIds = posts.map((post) => post.id);
276
295
  const [threadMap, rawMediaMap, aliasesMap] = await Promise.all([
277
- c.var.services.posts.getPublishedThreads(rootIds),
296
+ c.var.services.posts.getPublishedThreads(rootIds, { publishedBefore }),
278
297
  c.var.services.media.getByPostIds(postIds),
279
298
  c.var.services.paths.getPostAliases(postIds),
280
299
  ]);
@@ -345,15 +364,9 @@ export async function renderCollectionFeed(
345
364
  )
346
365
  : undefined;
347
366
 
348
- // feedUpdatedAt = max(lastActivityAt, collectedAt)
349
- const lastActivity = toISOString(post.lastActivityAt);
350
- const collectedIso = collectedAt ? toISOString(collectedAt) : null;
351
- const feedUpdatedAt =
352
- collectedIso && collectedIso > lastActivity ? collectedIso : lastActivity;
353
-
354
367
  return {
355
368
  ...postView,
356
- feedUpdatedAt,
369
+ feedUpdatedAt: getFeedEntryUpdatedAt(post, thread, [collectedAt]),
357
370
  threadReplies: replies,
358
371
  };
359
372
  });
@@ -382,7 +395,7 @@ export async function renderCollectionFeed(
382
395
  return new Response(xml, {
383
396
  headers: {
384
397
  "Content-Type": "application/atom+xml; charset=utf-8",
385
- "Cache-Control": "public, max-age=180",
398
+ "Cache-Control": RSS_FEED_CACHE_CONTROL,
386
399
  },
387
400
  });
388
401
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Collections Listing Page Route
3
3
  *
4
- * Lists all collections with their post counts.
4
+ * Lists all collections with their Thread counts.
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
@@ -70,6 +70,11 @@ collectionsPageRoutes.get("/", async (c) => {
70
70
  <CollectionsPage
71
71
  items={directoryData.items}
72
72
  isAuthenticated={navData.isAuthenticated ?? false}
73
+ navigationCollectionIds={navData.links.flatMap((item) =>
74
+ item.type === "collection" && item.collectionId
75
+ ? [item.collectionId]
76
+ : [],
77
+ )}
73
78
  sitePathPrefix={navData.sitePathPrefix}
74
79
  siteOrigin={c.var.appConfig.siteOrigin}
75
80
  />