@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,163 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { eq } from "drizzle-orm";
3
+ import { createTestApp } from "../../../../__tests__/helpers/app.js";
4
+ import { posts, sites } from "../../../../db/schema.js";
5
+ import { POST_BODY_HTML_VERSION } from "../../../../lib/post-body-html.js";
6
+ import { createPostService } from "../../../../services/post.js";
7
+ import { internalPostBodyHtmlRoutes } from "../post-body-html.js";
8
+ import { internalSitesRoutes } from "../sites.js";
9
+
10
+ describe("Internal post body HTML rebuild routes", () => {
11
+ it("hides the current-site endpoint when no internal token is configured", async () => {
12
+ const { app } = createTestApp({ authenticated: false });
13
+ app.route("/api/internal/posts/body-html", internalPostBodyHtmlRoutes);
14
+
15
+ const response = await app.request(
16
+ "/api/internal/posts/body-html/rebuild",
17
+ { method: "POST" },
18
+ );
19
+
20
+ expect(response.status).toBe(404);
21
+ });
22
+
23
+ it("dry-runs and rebuilds the current site", async () => {
24
+ const { app, services, db } = createTestApp({
25
+ authenticated: false,
26
+ internalAdminToken: "internal-secret",
27
+ });
28
+ app.use("*", async (c, next) => {
29
+ c.set("appConfig", undefined as never);
30
+ await next();
31
+ });
32
+ app.route("/api/internal/posts/body-html", internalPostBodyHtmlRoutes);
33
+ const post = await services.posts.create({
34
+ format: "note",
35
+ bodyMarkdown: "Body[^1]\n\n[^1]: Definition",
36
+ });
37
+ await db
38
+ .update(posts)
39
+ .set({
40
+ bodyHtml: '<span class="sidenote">legacy</span>',
41
+ bodyHtmlVersion: 1,
42
+ })
43
+ .where(eq(posts.id, post.id));
44
+
45
+ const dryRun = await app.request("/api/internal/posts/body-html/rebuild", {
46
+ method: "POST",
47
+ headers: {
48
+ Authorization: "Bearer internal-secret",
49
+ "Content-Type": "application/json",
50
+ },
51
+ body: JSON.stringify({ dryRun: true, limit: 25 }),
52
+ });
53
+ expect(dryRun.status).toBe(200);
54
+ await expect(dryRun.json()).resolves.toMatchObject({
55
+ processed: 1,
56
+ wouldRebuild: 1,
57
+ rebuilt: 0,
58
+ targetVersion: POST_BODY_HTML_VERSION,
59
+ });
60
+
61
+ const rebuild = await app.request("/api/internal/posts/body-html/rebuild", {
62
+ method: "POST",
63
+ headers: {
64
+ Authorization: "Bearer internal-secret",
65
+ "Content-Type": "application/json",
66
+ },
67
+ body: JSON.stringify({ limit: 25 }),
68
+ });
69
+ expect(rebuild.status).toBe(200);
70
+ await expect(rebuild.json()).resolves.toMatchObject({
71
+ rebuilt: 1,
72
+ failed: 0,
73
+ });
74
+ });
75
+
76
+ it("rebuilds only the explicitly selected managed site", async () => {
77
+ const { app, services, db } = createTestApp({
78
+ authenticated: false,
79
+ internalAdminToken: "internal-secret",
80
+ siteResolutionMode: "host-based",
81
+ });
82
+ app.use("*", async (c, next) => {
83
+ c.set("appConfig", undefined as never);
84
+ await next();
85
+ });
86
+ app.route("/api/internal/sites", internalSitesRoutes);
87
+
88
+ const timestamp = Math.floor(Date.now() / 1000);
89
+ const secondSiteId = "sit_managed00000000000000000000";
90
+ await db.insert(sites).values({
91
+ id: secondSiteId,
92
+ key: "managed",
93
+ status: "active",
94
+ createdAt: timestamp,
95
+ updatedAt: timestamp,
96
+ });
97
+ const defaultPost = await services.posts.create({
98
+ format: "note",
99
+ bodyMarkdown: "Default",
100
+ });
101
+ const managedService = createPostService(
102
+ db,
103
+ { slugIdLength: 5 },
104
+ secondSiteId,
105
+ );
106
+ const managedPost = await managedService.create({
107
+ format: "note",
108
+ bodyMarkdown: "Managed[^1]\n\n[^1]: Definition",
109
+ });
110
+ await db
111
+ .update(posts)
112
+ .set({ bodyHtmlVersion: 1 })
113
+ .where(eq(posts.id, defaultPost.id));
114
+ await db
115
+ .update(posts)
116
+ .set({ bodyHtmlVersion: 1 })
117
+ .where(eq(posts.id, managedPost.id));
118
+
119
+ const response = await app.request(
120
+ `/api/internal/sites/${secondSiteId}/posts/body-html/rebuild`,
121
+ {
122
+ method: "POST",
123
+ headers: {
124
+ Authorization: "Bearer internal-secret",
125
+ "Content-Type": "application/json",
126
+ },
127
+ body: JSON.stringify({}),
128
+ },
129
+ );
130
+ expect(response.status).toBe(200);
131
+ await expect(response.json()).resolves.toMatchObject({
132
+ processed: 1,
133
+ rebuilt: 1,
134
+ });
135
+
136
+ const rows = await db
137
+ .select({ id: posts.id, version: posts.bodyHtmlVersion })
138
+ .from(posts);
139
+ expect(rows.find((row) => row.id === managedPost.id)?.version).toBe(
140
+ POST_BODY_HTML_VERSION,
141
+ );
142
+ expect(rows.find((row) => row.id === defaultPost.id)?.version).toBe(1);
143
+ });
144
+
145
+ it("rejects an unknown explicit site instead of reporting an empty success", async () => {
146
+ const { app } = createTestApp({
147
+ authenticated: false,
148
+ internalAdminToken: "internal-secret",
149
+ siteResolutionMode: "host-based",
150
+ });
151
+ app.route("/api/internal/sites", internalSitesRoutes);
152
+
153
+ const response = await app.request(
154
+ "/api/internal/sites/sit_missing/posts/body-html/rebuild",
155
+ {
156
+ method: "POST",
157
+ headers: { Authorization: "Bearer internal-secret" },
158
+ },
159
+ );
160
+
161
+ expect(response.status).toBe(404);
162
+ });
163
+ });
@@ -120,7 +120,7 @@ describe("Internal site admin routes", () => {
120
120
  primaryHost: "locale-demo.example.com",
121
121
  siteName: "Locale Demo",
122
122
  siteLanguage: "en-US",
123
- timeZone: "Asia/Shanghai",
123
+ timeZone: "Etc/GMT-8",
124
124
  }),
125
125
  });
126
126
 
@@ -139,10 +139,43 @@ describe("Internal site admin routes", () => {
139
139
  { key: "ONBOARDING_STATUS", value: "completed" },
140
140
  { key: "SITE_LANGUAGE", value: "en" },
141
141
  { key: "SITE_NAME", value: "Locale Demo" },
142
- { key: "TIME_ZONE", value: "Asia/Shanghai" },
142
+ { key: "TIME_ZONE", value: "Etc/GMT-8" },
143
143
  ]);
144
144
  });
145
145
 
146
+ it("falls back to UTC instead of rejecting invalid browser timezone metadata", async () => {
147
+ const { app, sqlite } = createTestApp({
148
+ authenticated: false,
149
+ internalAdminToken: "internal-secret",
150
+ siteResolutionMode: "host-based",
151
+ });
152
+ app.route("/api/internal/sites", internalSitesRoutes);
153
+
154
+ const res = await app.request("/api/internal/sites", {
155
+ method: "POST",
156
+ headers: {
157
+ Authorization: "Bearer internal-secret",
158
+ "Content-Type": "application/json",
159
+ },
160
+ body: JSON.stringify({
161
+ key: "timezone-fallback",
162
+ primaryHost: "timezone-fallback.example.com",
163
+ siteName: "Timezone Fallback",
164
+ timeZone: "Unknown/Zone",
165
+ }),
166
+ });
167
+
168
+ expect(res.status).toBe(201);
169
+ const body = (await res.json()) as { siteId: string };
170
+ const timeZone = sqlite
171
+ .prepare(
172
+ 'SELECT "value" FROM "site_setting" WHERE "site_id" = ? AND "key" = \'TIME_ZONE\'',
173
+ )
174
+ .get(body.siteId);
175
+
176
+ expect(timeZone).toBeUndefined();
177
+ });
178
+
146
179
  it("reports an unused key as available", async () => {
147
180
  const { app } = createTestApp({
148
181
  authenticated: false,
@@ -0,0 +1,43 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { requireInternalAdminApi } from "../../../middleware/auth.js";
4
+ import { parseValidated } from "../../../lib/schemas.js";
5
+ import { rebuildPostBodyHtmlWithRuntimeSettings } from "../../../services/post.js";
6
+ import type { Bindings } from "../../../types.js";
7
+ import type { AppVariables } from "../../../types/app-context.js";
8
+
9
+ type Env = { Bindings: Bindings; Variables: AppVariables };
10
+
11
+ export const RebuildPostBodyHtmlSchema = z.object({
12
+ limit: z.number().int().positive().max(100).optional(),
13
+ cursor: z.string().trim().min(1).optional(),
14
+ dryRun: z.boolean().optional(),
15
+ });
16
+
17
+ export const internalPostBodyHtmlRoutes = new Hono<Env>();
18
+
19
+ /**
20
+ * Rebuild the current site's materialized post body HTML projection.
21
+ *
22
+ * Callers follow `nextCursor` until `done`. The service owns rendering,
23
+ * compare-and-swap writes, and timestamp preservation; this route only
24
+ * validates the authenticated maintenance request.
25
+ */
26
+ internalPostBodyHtmlRoutes.post(
27
+ "/rebuild",
28
+ requireInternalAdminApi(),
29
+ async (c) => {
30
+ const contentType = c.req.header("Content-Type") || "";
31
+ const rawBody = contentType.includes("application/json")
32
+ ? await c.req.json().catch(() => ({}))
33
+ : {};
34
+ const body = parseValidated(RebuildPostBodyHtmlSchema, rawBody);
35
+
36
+ const result = await rebuildPostBodyHtmlWithRuntimeSettings(
37
+ c.var.services,
38
+ c.env,
39
+ body,
40
+ );
41
+ return c.json(result);
42
+ },
43
+ );
@@ -3,12 +3,14 @@ import { z } from "zod";
3
3
  import { requireInternalAdminApi } from "../../../middleware/auth.js";
4
4
  import { ConflictError } from "../../../lib/errors.js";
5
5
  import { parseValidated } from "../../../lib/schemas.js";
6
+ import { rebuildPostBodyHtmlWithRuntimeSettings } from "../../../services/post.js";
6
7
  import {
7
8
  getConfiguredStorageDriver,
8
9
  getSiteResolutionMode,
9
10
  } from "../../../lib/env.js";
10
11
  import type { Bindings } from "../../../types.js";
11
12
  import type { AppVariables } from "../../../types/app-context.js";
13
+ import { RebuildPostBodyHtmlSchema } from "./post-body-html.js";
12
14
 
13
15
  type Env = { Bindings: Bindings; Variables: AppVariables };
14
16
 
@@ -199,6 +201,28 @@ internalSitesRoutes.post(
199
201
  },
200
202
  );
201
203
 
204
+ // Rebuild one managed site's materialized body HTML projection. Hosted fleet
205
+ // orchestration stays in the control plane; core never silently widens a
206
+ // content-facing operation to every tenant.
207
+ internalSitesRoutes.post(
208
+ "/:siteId/posts/body-html/rebuild",
209
+ requireInternalAdminApi(),
210
+ async (c) => {
211
+ assertHostBasedMode(c.env);
212
+
213
+ const contentType = c.req.header("Content-Type") || "";
214
+ const rawBody = contentType.includes("application/json")
215
+ ? await c.req.json().catch(() => ({}))
216
+ : {};
217
+ const body = parseValidated(RebuildPostBodyHtmlSchema, rawBody);
218
+ const services = c.var.servicesForSite(c.req.param("siteId"));
219
+
220
+ return c.json(
221
+ await rebuildPostBodyHtmlWithRuntimeSettings(services, c.env, body),
222
+ );
223
+ },
224
+ );
225
+
202
226
  internalSitesRoutes.get(
203
227
  "/:siteId/export",
204
228
  requireInternalAdminApi(),
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
  import type { Bindings } from "../../types.js";
9
9
  import type { AppVariables } from "../../types/app-context.js";
10
10
  import { requireAuthApi } from "../../middleware/auth.js";
11
+ import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
11
12
  import {
12
13
  CreateNavItemSchema,
13
14
  NavItemIdSchema,
@@ -31,6 +32,11 @@ const MoveSchema = z.object({
31
32
  before: NavItemIdSchema.nullable().optional(),
32
33
  });
33
34
 
35
+ const PageCandidatesQuerySchema = z.object({
36
+ q: z.string().trim().max(200).optional(),
37
+ limit: z.coerce.number().int().min(1).max(50).optional(),
38
+ });
39
+
34
40
  async function withSiteHeaderHtml<T extends object>(
35
41
  c: Context<Env>,
36
42
  body: T,
@@ -48,11 +54,21 @@ async function withSiteHeaderHtml<T extends object>(
48
54
  }
49
55
 
50
56
  // List nav items
51
- navItemsApiRoutes.get("/", async (c) => {
57
+ navItemsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
52
58
  const items = await c.var.services.navItems.list();
53
59
  return c.json({ navItems: items });
54
60
  });
55
61
 
62
+ // Search published titled notes that can be added as pages (requires auth)
63
+ navItemsApiRoutes.get("/pages", requireAuthApi(), async (c) => {
64
+ const query = parseValidated(PageCandidatesQuerySchema, c.req.query());
65
+ const pages = await c.var.services.navItems.listPageCandidates({
66
+ query: query.q,
67
+ limit: query.limit,
68
+ });
69
+ return c.json({ pages });
70
+ });
71
+
56
72
  // Move nav item (requires auth) — must be before /:id
57
73
  navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
58
74
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.navItem);
@@ -94,6 +110,13 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
94
110
  url: getCollectionPagePath(collection.slug),
95
111
  placement: body.placement,
96
112
  });
113
+ } else if (body.type === "page") {
114
+ item = await c.var.services.navItems.create({
115
+ type: "page",
116
+ postId: body.postId,
117
+ label: body.label,
118
+ placement: body.placement,
119
+ });
97
120
  } else {
98
121
  item = await c.var.services.navItems.create({
99
122
  type: "link",
@@ -131,12 +131,15 @@ postsApiRoutes.get("/:id/content", requireAuthApi(), async (c) => {
131
131
  postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
132
132
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
133
133
 
134
- // Fetch post, media, and collections in parallel (all keyed by the same id)
135
- const [post, mediaList, postCollections] = await Promise.all([
136
- c.var.services.posts.getById(id),
137
- c.var.services.media.getByPostId(id),
138
- c.var.services.collections.getCollectionsByPostId(id),
139
- ]);
134
+ // Fetch post, media, collections and thread position in parallel (all keyed
135
+ // by the same id)
136
+ const [post, mediaList, threadCollections, threadPosition] =
137
+ await Promise.all([
138
+ c.var.services.posts.getById(id),
139
+ c.var.services.media.getByPostId(id),
140
+ c.var.services.collections.getCollectionsByPostId(id),
141
+ c.var.services.posts.getThreadPosition(id),
142
+ ]);
140
143
  const foundPost = assertFound(post, "Post");
141
144
  const {
142
145
  r2PublicUrl,
@@ -145,10 +148,10 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
145
148
  localPublicUrl,
146
149
  sitePathPrefix,
147
150
  } = c.var.appConfig;
148
- const collectionIds = postCollections.map((col) => col.id);
151
+ const collectionIds = threadCollections.map((col) => col.id);
149
152
 
150
- return c.json(
151
- toApiPost(foundPost, {
153
+ return c.json({
154
+ ...toApiPost(foundPost, {
152
155
  collectionIds,
153
156
  attachments: mediaList.map((m) =>
154
157
  toApiAttachment(
@@ -161,7 +164,8 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
161
164
  ),
162
165
  ),
163
166
  }),
164
- );
167
+ threadPosition,
168
+ });
165
169
  });
166
170
 
167
171
  // Create post (requires auth)
@@ -88,6 +88,41 @@ describe("Public Archive API Routes", () => {
88
88
  expect(nextBody.posts[0].id).not.toBe(body.posts[0].id);
89
89
  });
90
90
 
91
+ it("paginates chronologically without promoting pinned posts", async () => {
92
+ const { app, services } = createTestApp({ authenticated: false });
93
+ app.route("/api/public/archive", publicArchiveApiRoutes);
94
+
95
+ const olderPinned = await services.posts.create({
96
+ format: "note",
97
+ title: "Older pinned post",
98
+ bodyMarkdown: "older",
99
+ pinned: true,
100
+ publishedAt: Date.UTC(2025, 0, 1) / 1000,
101
+ });
102
+ const newerUnpinned = await services.posts.create({
103
+ format: "note",
104
+ title: "Newer unpinned post",
105
+ bodyMarkdown: "newer",
106
+ publishedAt: Date.UTC(2026, 0, 1) / 1000,
107
+ });
108
+
109
+ const first = await app.request("/api/public/archive?limit=1");
110
+ expect(first.status).toBe(200);
111
+ const firstBody = await first.json();
112
+ expect(firstBody.posts.map((post: { id: string }) => post.id)).toEqual([
113
+ newerUnpinned.id,
114
+ ]);
115
+
116
+ const second = await app.request(
117
+ `/api/public/archive?limit=1&cursor=${firstBody.nextCursor}`,
118
+ );
119
+ expect(second.status).toBe(200);
120
+ const secondBody = await second.json();
121
+ expect(secondBody.posts.map((post: { id: string }) => post.id)).toEqual([
122
+ olderPinned.id,
123
+ ]);
124
+ });
125
+
91
126
  it("filters by year using publishedAt", async () => {
92
127
  const { app, services } = createTestApp({ authenticated: false });
93
128
  app.route("/api/public/archive", publicArchiveApiRoutes);