@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
@@ -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",
@@ -132,7 +132,7 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
132
132
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
133
133
 
134
134
  // Fetch post, media, and collections in parallel (all keyed by the same id)
135
- const [post, mediaList, postCollections] = await Promise.all([
135
+ const [post, mediaList, threadCollections] = await Promise.all([
136
136
  c.var.services.posts.getById(id),
137
137
  c.var.services.media.getByPostId(id),
138
138
  c.var.services.collections.getCollectionsByPostId(id),
@@ -145,7 +145,7 @@ postsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
145
145
  localPublicUrl,
146
146
  sitePathPrefix,
147
147
  } = c.var.appConfig;
148
- const collectionIds = postCollections.map((col) => col.id);
148
+ const collectionIds = threadCollections.map((col) => col.id);
149
149
 
150
150
  return c.json(
151
151
  toApiPost(foundPost, {
@@ -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);
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import { eq } from "drizzle-orm";
2
3
  import { createTestApp } from "../../../../__tests__/helpers/app.js";
4
+ import { posts } from "../../../../db/schema.js";
3
5
  import { publicPostsApiRoutes } from "../posts.js";
4
6
 
5
7
  describe("Public Posts API Routes", () => {
@@ -100,6 +102,32 @@ describe("Public Posts API Routes", () => {
100
102
  expect(body.posts[0]).not.toHaveProperty("bodyText");
101
103
  });
102
104
 
105
+ it("returns canonical v3 HTML when the stored projection is stale", async () => {
106
+ const { app, services, db } = createTestApp({ authenticated: false });
107
+ app.route("/api/public/posts", publicPostsApiRoutes);
108
+ const post = await services.posts.create({
109
+ format: "note",
110
+ title: "Footnote API",
111
+ bodyMarkdown: "API body[^1]\n\n[^1]: API definition",
112
+ });
113
+ await db
114
+ .update(posts)
115
+ .set({
116
+ bodyHtml: '<span class="sidenote">legacy</span>',
117
+ bodyHtmlVersion: 1,
118
+ })
119
+ .where(eq(posts.id, post.id));
120
+
121
+ const response = await app.request("/api/public/posts");
122
+ const body = await response.json();
123
+
124
+ expect(response.status).toBe(200);
125
+ expect(body.posts[0].bodyHtml).toContain('role="doc-noteref"');
126
+ expect(body.posts[0].bodyHtml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
127
+ expect(body.posts[0].bodyHtml).not.toContain(post.id);
128
+ expect(body.posts[0].bodyHtml).not.toContain("legacy");
129
+ });
130
+
103
131
  it("filters posts by a single collection slug", async () => {
104
132
  const { app, services } = createTestApp({ authenticated: false });
105
133
  app.route("/api/public/posts", publicPostsApiRoutes);
@@ -194,12 +222,20 @@ describe("Public Posts API Routes", () => {
194
222
  title: "Older",
195
223
  bodyMarkdown: "first",
196
224
  collectionIds: [collection.id],
225
+ publishedAt: 1000,
197
226
  });
198
227
  const newer = await services.posts.create({
199
228
  format: "note",
200
229
  title: "Newer",
201
230
  bodyMarkdown: "second",
202
231
  collectionIds: [collection.id],
232
+ publishedAt: 2000,
233
+ });
234
+ await services.posts.create({
235
+ format: "note",
236
+ bodyMarkdown: "Later reply to the older Thread",
237
+ replyToId: older.id,
238
+ publishedAt: 3000,
203
239
  });
204
240
 
205
241
  const res = await app.request(
@@ -213,6 +249,145 @@ describe("Public Posts API Routes", () => {
213
249
  expect(body.posts[1].id).toBe(newer.id);
214
250
  });
215
251
 
252
+ it("sorts collection results by Thread activity from Child Posts", async () => {
253
+ const { app, services } = createTestApp({ authenticated: false });
254
+ app.route("/api/public/posts", publicPostsApiRoutes);
255
+
256
+ const collection = await services.collections.create({
257
+ slug: "active-threads",
258
+ title: "Active Threads",
259
+ });
260
+ const olderRoot = await services.posts.create({
261
+ format: "note",
262
+ title: "Older root",
263
+ bodyMarkdown: "older",
264
+ collectionIds: [collection.id],
265
+ publishedAt: 1000,
266
+ });
267
+ const newerRoot = await services.posts.create({
268
+ format: "note",
269
+ title: "Newer root",
270
+ bodyMarkdown: "newer",
271
+ collectionIds: [collection.id],
272
+ publishedAt: 2000,
273
+ });
274
+ await services.posts.create({
275
+ format: "note",
276
+ bodyMarkdown: "new activity",
277
+ replyToId: olderRoot.id,
278
+ publishedAt: 3000,
279
+ });
280
+
281
+ const res = await app.request(
282
+ "/api/public/posts?collection=active-threads&sort=newest",
283
+ );
284
+ expect(res.status).toBe(200);
285
+
286
+ const body = await res.json();
287
+ expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
288
+ olderRoot.id,
289
+ newerRoot.id,
290
+ ]);
291
+ });
292
+
293
+ it("sorts and cursor-paginates collection Threads by Child ratings", async () => {
294
+ const { app, services } = createTestApp({ authenticated: false });
295
+ app.route("/api/public/posts", publicPostsApiRoutes);
296
+
297
+ const collection = await services.collections.create({
298
+ slug: "rated-threads",
299
+ title: "Rated Threads",
300
+ sortOrder: "rating_desc",
301
+ });
302
+ const createRatedThread = async (
303
+ title: string,
304
+ publishedAt: number,
305
+ rating: number,
306
+ ) => {
307
+ const root = await services.posts.create({
308
+ format: "note",
309
+ title,
310
+ bodyMarkdown: `${title} root`,
311
+ collectionIds: [collection.id],
312
+ publishedAt,
313
+ });
314
+ await services.posts.create({
315
+ format: "note",
316
+ bodyMarkdown: `${title} rated Child`,
317
+ replyToId: root.id,
318
+ publishedAt: publishedAt + 100,
319
+ rating,
320
+ });
321
+ return root;
322
+ };
323
+
324
+ const highest = await createRatedThread("Highest", 1000, 5);
325
+ const middle = await createRatedThread("Middle", 2000, 3);
326
+ const lowest = await createRatedThread("Lowest", 3000, 1);
327
+
328
+ const firstRes = await app.request(
329
+ "/api/public/posts?collection=rated-threads&limit=1",
330
+ );
331
+ expect(firstRes.status).toBe(200);
332
+ const first = await firstRes.json();
333
+ expect(first.posts[0].id).toBe(highest.id);
334
+ expect(first.nextCursor).toBe(highest.id);
335
+
336
+ const secondRes = await app.request(
337
+ `/api/public/posts?collection=rated-threads&limit=1&cursor=${first.nextCursor}`,
338
+ );
339
+ expect(secondRes.status).toBe(200);
340
+ const second = await secondRes.json();
341
+ expect(second.posts[0].id).toBe(middle.id);
342
+ expect(second.nextCursor).toBe(middle.id);
343
+
344
+ const thirdRes = await app.request(
345
+ `/api/public/posts?collection=rated-threads&limit=1&cursor=${second.nextCursor}`,
346
+ );
347
+ expect(thirdRes.status).toBe(200);
348
+ const third = await thirdRes.json();
349
+ expect(third.posts[0].id).toBe(lowest.id);
350
+ });
351
+
352
+ it("keeps root format and Latest visibility filters for Collections", async () => {
353
+ const { app, services } = createTestApp({ authenticated: false });
354
+ app.route("/api/public/posts", publicPostsApiRoutes);
355
+
356
+ const collection = await services.collections.create({
357
+ slug: "mixed",
358
+ title: "Mixed",
359
+ });
360
+ const note = await services.posts.create({
361
+ format: "note",
362
+ title: "Visible note",
363
+ bodyMarkdown: "note",
364
+ collectionIds: [collection.id],
365
+ });
366
+ await services.posts.create({
367
+ format: "link",
368
+ title: "Visible link",
369
+ url: "https://example.com",
370
+ collectionIds: [collection.id],
371
+ });
372
+ await services.posts.create({
373
+ format: "note",
374
+ title: "Hidden note",
375
+ bodyMarkdown: "hidden",
376
+ visibility: "latest_hidden",
377
+ collectionIds: [collection.id],
378
+ });
379
+
380
+ const res = await app.request(
381
+ "/api/public/posts?collection=mixed&format=note",
382
+ );
383
+ expect(res.status).toBe(200);
384
+
385
+ const body = await res.json();
386
+ expect(body.posts.map((post: { id: string }) => post.id)).toEqual([
387
+ note.id,
388
+ ]);
389
+ });
390
+
216
391
  it("allows overriding collection sort order via sort parameter", async () => {
217
392
  const { app, services } = createTestApp({ authenticated: false });
218
393
  app.route("/api/public/posts", publicPostsApiRoutes);
@@ -282,6 +457,39 @@ describe("Public Posts API Routes", () => {
282
457
  expect(body).not.toHaveProperty("body");
283
458
  });
284
459
 
460
+ it("returns the shared Thread Collections for a child post", async () => {
461
+ const { app, services } = createTestApp({ authenticated: false });
462
+ app.route("/api/public/posts", publicPostsApiRoutes);
463
+
464
+ const collection = await services.collections.create({
465
+ slug: "shared",
466
+ title: "Shared",
467
+ });
468
+ const root = await services.posts.create({
469
+ format: "note",
470
+ bodyMarkdown: "root",
471
+ collectionIds: [collection.id],
472
+ });
473
+ const child = await services.posts.create({
474
+ format: "note",
475
+ bodyMarkdown: "child",
476
+ replyToId: root.id,
477
+ });
478
+
479
+ const res = await app.request(`/api/public/posts/${child.slug}`);
480
+ expect(res.status).toBe(200);
481
+ await expect(res.json()).resolves.toMatchObject({
482
+ id: child.id,
483
+ collections: [
484
+ {
485
+ id: collection.id,
486
+ slug: "shared",
487
+ title: "Shared",
488
+ },
489
+ ],
490
+ });
491
+ });
492
+
285
493
  it("returns markdown instead of rendered fields when content=markdown", async () => {
286
494
  const { app, services } = createTestApp({ authenticated: false });
287
495
  app.route("/api/public/posts", publicPostsApiRoutes);
@@ -5,11 +5,14 @@ import type { AppVariables } from "../../../types/app-context.js";
5
5
  import { MEDIA_KINDS } from "../../../types.js";
6
6
  import { FormatSchema, parseValidated } from "../../../lib/schemas.js";
7
7
  import { toPublicPost } from "./posts.js";
8
+ import { requirePublicApiEnabled } from "../../../middleware/public-content-access.js";
8
9
 
9
10
  type Env = { Bindings: Bindings; Variables: AppVariables };
10
11
 
11
12
  export const publicArchiveApiRoutes = new Hono<Env>();
12
13
 
14
+ publicArchiveApiRoutes.use("*", requirePublicApiEnabled());
15
+
13
16
  const MediaKindSchema = z.enum(MEDIA_KINDS);
14
17
  const MEDIA_KIND_LIST = MEDIA_KINDS.join(", ");
15
18
  const INVALID_MEDIA_MESSAGE =
@@ -108,6 +111,7 @@ publicArchiveApiRoutes.get("/", async (c) => {
108
111
  hasMedia: mediaPresence ?? hasMedia,
109
112
  hasTitle: title ?? hasTitle,
110
113
  hasReplies: replies,
114
+ ignorePinnedSort: true,
111
115
  });
112
116
 
113
117
  const postIds = posts.map((post) => post.id);