@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -81,6 +81,8 @@ function insertTestPost(
81
81
  format?: "note" | "link" | "quote";
82
82
  status?: "draft" | "published";
83
83
  visibility?: "public" | "latest_hidden" | "private";
84
+ language?: string | null;
85
+ translationGroupId?: string | null;
84
86
  },
85
87
  ) {
86
88
  const ts = now();
@@ -88,9 +90,10 @@ function insertTestPost(
88
90
  .prepare(
89
91
  `INSERT INTO post (
90
92
  id, site_id, format, status, visibility, title, thread_id,
93
+ language, translation_group_id,
91
94
  published_at, last_activity_at, created_at, updated_at
92
95
  )
93
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
94
97
  )
95
98
  .run(
96
99
  input.id,
@@ -100,6 +103,8 @@ function insertTestPost(
100
103
  input.visibility ?? "latest_hidden",
101
104
  input.title,
102
105
  input.id,
106
+ input.language ?? null,
107
+ input.translationGroupId ?? null,
103
108
  ts,
104
109
  ts,
105
110
  ts,
@@ -252,7 +257,9 @@ describe("NavItemService", () => {
252
257
  expect(item).toMatchObject({
253
258
  type: "page",
254
259
  postId: TEST_POST_ID,
255
- label: "About me",
260
+ // Nothing stored: the item follows the page's title from here on.
261
+ label: "",
262
+ targetTitle: "About me",
256
263
  url: "/about-me",
257
264
  placement: "header",
258
265
  });
@@ -280,7 +287,7 @@ describe("NavItemService", () => {
280
287
  ).rejects.toThrow("Page must be a published, non-private titled note");
281
288
  });
282
289
 
283
- it("truncates a page's default navigation label", async () => {
290
+ it("truncates a page's custom navigation label", async () => {
284
291
  insertTestPost(sqlite, {
285
292
  id: TEST_POST_ID,
286
293
  slug: "long-title",
@@ -290,9 +297,146 @@ describe("NavItemService", () => {
290
297
  const item = await navItemService.create({
291
298
  type: "page",
292
299
  postId: TEST_POST_ID,
300
+ label: "B".repeat(150),
301
+ });
302
+
303
+ expect(item.label).toBe("B".repeat(100));
304
+ });
305
+
306
+ it("leaves a page item's label empty so it follows the page title", async () => {
307
+ insertTestPost(sqlite, {
308
+ id: TEST_POST_ID,
309
+ slug: "about-me",
310
+ title: "About me",
311
+ });
312
+ const item = await navItemService.create({
313
+ type: "page",
314
+ postId: TEST_POST_ID,
315
+ });
316
+
317
+ sqlite
318
+ .prepare(`UPDATE post SET title = ? WHERE id = ?`)
319
+ .run("Colophon", TEST_POST_ID);
320
+
321
+ const [reread] = await navItemService.list();
322
+ expect(reread).toMatchObject({
323
+ id: item.id,
324
+ label: "",
325
+ targetTitle: "Colophon",
326
+ });
327
+ });
328
+ });
329
+
330
+ describe("list in a language view", () => {
331
+ const TRANSLATION_GROUP = "tgr_test0000000000000000000001";
332
+
333
+ function addAboutPageNav() {
334
+ insertTestPost(sqlite, {
335
+ id: TEST_POST_ID,
336
+ slug: "about",
337
+ title: "关于",
338
+ language: "zh-Hans",
339
+ translationGroupId: TRANSLATION_GROUP,
340
+ });
341
+ return navItemService.create({ type: "page", postId: TEST_POST_ID });
342
+ }
343
+
344
+ it("points a page item at the version written in the view's language", async () => {
345
+ const item = await addAboutPageNav();
346
+ insertTestPost(sqlite, {
347
+ id: TEST_POST_ID_2,
348
+ slug: "about-en",
349
+ title: "About",
350
+ language: "en",
351
+ translationGroupId: TRANSLATION_GROUP,
352
+ });
353
+
354
+ const [resolved] = await navItemService.list({ language: "en" });
355
+
356
+ expect(resolved).toMatchObject({
357
+ id: item.id,
358
+ // The address and the title move together: a link that lands on
359
+ // English content is labelled in English.
360
+ url: "/about-en",
361
+ targetTitle: "About",
362
+ // Still the configured target — only the destination follows the view.
363
+ postId: TEST_POST_ID,
364
+ });
365
+ });
366
+
367
+ it("leaves the item alone when that language has no version", async () => {
368
+ const item = await addAboutPageNav();
369
+
370
+ const [resolved] = await navItemService.list({ language: "ja" });
371
+
372
+ expect(resolved).toMatchObject({
373
+ id: item.id,
374
+ url: "/about",
375
+ targetTitle: "关于",
376
+ });
377
+ });
378
+
379
+ it("ignores a translation that is not publicly readable", async () => {
380
+ await addAboutPageNav();
381
+ insertTestPost(sqlite, {
382
+ id: TEST_POST_ID_2,
383
+ slug: "about-en",
384
+ title: "About",
385
+ status: "draft",
386
+ language: "en",
387
+ translationGroupId: TRANSLATION_GROUP,
388
+ });
389
+ insertTestPost(sqlite, {
390
+ id: TEST_POST_ID_3,
391
+ slug: "about-ja",
392
+ title: "About (ja)",
393
+ visibility: "private",
394
+ language: "ja",
395
+ translationGroupId: TRANSLATION_GROUP,
396
+ });
397
+
398
+ const [draftView] = await navItemService.list({ language: "en" });
399
+ const [privateView] = await navItemService.list({ language: "ja" });
400
+
401
+ expect(draftView?.url).toBe("/about");
402
+ expect(privateView?.url).toBe("/about");
403
+ });
404
+
405
+ it("keeps a label the author typed, whatever the view", async () => {
406
+ insertTestPost(sqlite, {
407
+ id: TEST_POST_ID,
408
+ slug: "about",
409
+ title: "关于",
410
+ language: "zh-Hans",
411
+ translationGroupId: TRANSLATION_GROUP,
412
+ });
413
+ insertTestPost(sqlite, {
414
+ id: TEST_POST_ID_2,
415
+ slug: "about-en",
416
+ title: "About",
417
+ language: "en",
418
+ translationGroupId: TRANSLATION_GROUP,
419
+ });
420
+ await navItemService.create({
421
+ type: "page",
422
+ postId: TEST_POST_ID,
423
+ label: "自我介绍",
424
+ });
425
+
426
+ const [resolved] = await navItemService.list({ language: "en" });
427
+
428
+ expect(resolved).toMatchObject({
429
+ label: "自我介绍",
430
+ url: "/about-en",
293
431
  });
432
+ });
433
+
434
+ it("does not follow translations without a language", async () => {
435
+ const item = await addAboutPageNav();
436
+
437
+ const [resolved] = await navItemService.list();
294
438
 
295
- expect(item.label).toBe("A".repeat(100));
439
+ expect(resolved).toMatchObject({ id: item.id, url: "/about" });
296
440
  });
297
441
  });
298
442
 
@@ -302,15 +446,15 @@ describe("NavItemService", () => {
302
446
 
303
447
  expect(created.map((item) => item.systemKey)).toEqual([
304
448
  "featured",
305
- "collections",
306
449
  "archive",
307
- "rss",
450
+ "collections",
451
+ "subscribe",
308
452
  "settings",
309
453
  ]);
310
454
  expect(created.map((item) => item.placement)).toEqual([
311
455
  "header",
312
456
  "header",
313
- "header",
457
+ "more",
314
458
  "more",
315
459
  "more",
316
460
  ]);
@@ -333,9 +477,9 @@ describe("NavItemService", () => {
333
477
 
334
478
  expect(created.map((item) => item.systemKey)).toEqual([
335
479
  "featured",
336
- "collections",
337
480
  "archive",
338
- "rss",
481
+ "collections",
482
+ "subscribe",
339
483
  "settings",
340
484
  ]);
341
485
  expect(items.find((item) => item.id === latest.id)).toEqual(
@@ -367,7 +511,7 @@ describe("NavItemService", () => {
367
511
  expect(created.map((item) => item.systemKey)).toEqual([
368
512
  "featured",
369
513
  "collections",
370
- "rss",
514
+ "subscribe",
371
515
  "settings",
372
516
  ]);
373
517
  expect(items.slice(0, 2).map((item) => item.id)).toEqual([
@@ -513,6 +657,65 @@ describe("NavItemService", () => {
513
657
  });
514
658
  });
515
659
 
660
+ describe("resolveNavTarget", () => {
661
+ it("resolves a page address to the item it can become", async () => {
662
+ insertTestPost(sqlite, {
663
+ id: TEST_POST_ID,
664
+ slug: "about",
665
+ title: "About this site",
666
+ visibility: "latest_hidden",
667
+ });
668
+
669
+ expect(await navItemService.resolveNavTarget("/about")).toEqual({
670
+ status: "page",
671
+ page: {
672
+ id: TEST_POST_ID,
673
+ title: "About this site",
674
+ slug: "about",
675
+ updatedAt: expect.any(Number),
676
+ },
677
+ });
678
+ });
679
+
680
+ it("reports an address navigation can only hold as a link", async () => {
681
+ // A custom archive URL is a real page with no post behind it, so no page
682
+ // item can track it.
683
+ insertTestPath(sqlite, {
684
+ id: "pth_archive000000000000000000",
685
+ path: "reading-2025",
686
+ kind: "archive",
687
+ archiveQuery: "year=2025",
688
+ });
689
+
690
+ expect(await navItemService.resolveNavTarget("/reading-2025")).toEqual({
691
+ status: "link_only",
692
+ });
693
+ });
694
+
695
+ it("resolves an alias to the post's canonical slug", async () => {
696
+ insertTestPost(sqlite, {
697
+ id: TEST_POST_ID,
698
+ slug: "about",
699
+ title: "About this site",
700
+ });
701
+ insertTestPath(sqlite, {
702
+ id: "pth_alias0000000000000000000000",
703
+ path: "about-us",
704
+ kind: "alias",
705
+ postId: TEST_POST_ID,
706
+ });
707
+
708
+ // The menu should show where the page lives, not the old address the
709
+ // author happened to have in their history.
710
+ expect(await navItemService.resolveNavTarget("/about-us")).toEqual(
711
+ expect.objectContaining({
712
+ status: "page",
713
+ page: expect.objectContaining({ slug: "about" }),
714
+ }),
715
+ );
716
+ });
717
+ });
718
+
516
719
  describe("listSuggestedLinks", () => {
517
720
  it("suggests a published /about page as a page nav item", async () => {
518
721
  insertTestPost(sqlite, {
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Path resolution.
3
+ *
4
+ * `resolve` answers a request — a redirect there has to stay a redirect, so
5
+ * the router can emit it. `resolveTarget` answers a person holding an address,
6
+ * where the only useful answer is the thing at the end of the hops.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach } from "vitest";
10
+ import { sql } from "drizzle-orm";
11
+ import {
12
+ createTestDatabase,
13
+ DEFAULT_TEST_SITE_ID,
14
+ } from "../../__tests__/helpers/db.js";
15
+ import { createPathService } from "../path.js";
16
+ import { createPostService } from "../post.js";
17
+ import type { Database } from "../../db/index.js";
18
+
19
+ describe("PathService.resolveTarget", () => {
20
+ let db: Database;
21
+ let paths: ReturnType<typeof createPathService>;
22
+ let postService: ReturnType<typeof createPostService>;
23
+
24
+ beforeEach(() => {
25
+ const testDb = createTestDatabase();
26
+ db = testDb.db as unknown as Database;
27
+ paths = createPathService(db, DEFAULT_TEST_SITE_ID);
28
+ postService = createPostService(
29
+ db,
30
+ { slugIdLength: 5 },
31
+ DEFAULT_TEST_SITE_ID,
32
+ paths,
33
+ );
34
+ });
35
+
36
+ async function createPost(slug: string) {
37
+ return postService.create({
38
+ format: "note",
39
+ title: slug,
40
+ slug,
41
+ body: JSON.stringify({
42
+ type: "doc",
43
+ content: [{ type: "paragraph" }],
44
+ }),
45
+ });
46
+ }
47
+
48
+ it("resolves a plain slug to its post", async () => {
49
+ const post = await createPost("hello");
50
+
51
+ expect(await paths.resolveTarget("hello")).toEqual(
52
+ expect.objectContaining({ targetType: "post", postId: post.id }),
53
+ );
54
+ });
55
+
56
+ it("follows a redirect to the post it points at", async () => {
57
+ const post = await createPost("new-name");
58
+ await paths.create({
59
+ path: "old-name",
60
+ kind: "redirect",
61
+ redirectToPath: "new-name",
62
+ redirectType: 301,
63
+ });
64
+
65
+ expect(await paths.resolve("old-name")).toEqual(
66
+ expect.objectContaining({ targetType: "redirect" }),
67
+ );
68
+ expect(await paths.resolveTarget("old-name")).toEqual(
69
+ expect.objectContaining({ targetType: "post", postId: post.id }),
70
+ );
71
+ });
72
+
73
+ it("follows a chain of redirects", async () => {
74
+ const post = await createPost("third");
75
+ await paths.create({
76
+ path: "first",
77
+ kind: "redirect",
78
+ redirectToPath: "second",
79
+ redirectType: 301,
80
+ });
81
+ await paths.create({
82
+ path: "second",
83
+ kind: "redirect",
84
+ redirectToPath: "third",
85
+ redirectType: 301,
86
+ });
87
+
88
+ expect(await paths.resolveTarget("first")).toEqual(
89
+ expect.objectContaining({ targetType: "post", postId: post.id }),
90
+ );
91
+ });
92
+
93
+ it("gives up on a redirect loop rather than spinning", async () => {
94
+ await paths.create({
95
+ path: "ping",
96
+ kind: "redirect",
97
+ redirectToPath: "pong",
98
+ redirectType: 302,
99
+ });
100
+ await paths.create({
101
+ path: "pong",
102
+ kind: "redirect",
103
+ redirectToPath: "ping",
104
+ redirectType: 302,
105
+ });
106
+
107
+ expect(await paths.resolveTarget("ping")).toBeNull();
108
+ });
109
+
110
+ it("returns null for an address nothing is registered at", async () => {
111
+ expect(await paths.resolveTarget("nothing-here")).toBeNull();
112
+ });
113
+ });
114
+
115
+ /**
116
+ * `aliases[0]` becomes a Post's permalink, and therefore its Atom `<id>` and
117
+ * its sitemap `<loc>`. Feed readers and the Discover crawler both key entries
118
+ * on that id, so which alias comes first is an identity decision, not a
119
+ * presentation one — it must come from the query, never from the storage
120
+ * engine's row order.
121
+ */
122
+ describe("PathService.getPostAliases", () => {
123
+ let db: Database;
124
+ let paths: ReturnType<typeof createPathService>;
125
+ let postService: ReturnType<typeof createPostService>;
126
+
127
+ beforeEach(() => {
128
+ const testDb = createTestDatabase();
129
+ db = testDb.db as unknown as Database;
130
+ paths = createPathService(db, DEFAULT_TEST_SITE_ID);
131
+ postService = createPostService(
132
+ db,
133
+ { slugIdLength: 5 },
134
+ DEFAULT_TEST_SITE_ID,
135
+ paths,
136
+ );
137
+ });
138
+
139
+ async function createAlias(postId: string, path: string, createdAt: number) {
140
+ const record = await paths.create({ path, kind: "alias", postId });
141
+ await db.run(
142
+ sql`UPDATE path_registry SET created_at = ${createdAt} WHERE id = ${record.id}`,
143
+ );
144
+ return record;
145
+ }
146
+
147
+ it("returns aliases oldest first, whatever order they were inserted in", async () => {
148
+ const post = await postService.create({
149
+ format: "note",
150
+ title: "Aliased",
151
+ slug: "aliased",
152
+ body: JSON.stringify({ type: "doc", content: [{ type: "paragraph" }] }),
153
+ });
154
+
155
+ // Inserted newest-first, so insertion order is the opposite of the answer.
156
+ await createAlias(post.id, "second-address", 2_000);
157
+ await createAlias(post.id, "first-address", 1_000);
158
+
159
+ const aliases = await paths.getPostAliases([post.id]);
160
+
161
+ expect(aliases.get(post.id)).toEqual(["/first-address", "/second-address"]);
162
+ });
163
+
164
+ it("keeps a second custom URL from taking over the permalink", async () => {
165
+ const post = await postService.create({
166
+ format: "note",
167
+ title: "Renamed",
168
+ slug: "renamed",
169
+ body: JSON.stringify({ type: "doc", content: [{ type: "paragraph" }] }),
170
+ });
171
+
172
+ await createAlias(post.id, "original", 1_000);
173
+ const before = await paths.getPostAliases([post.id]);
174
+ await createAlias(post.id, "added-later", 2_000);
175
+ const after = await paths.getPostAliases([post.id]);
176
+
177
+ expect(before.get(post.id)?.[0]).toBe("/original");
178
+ expect(after.get(post.id)?.[0]).toBe("/original");
179
+ });
180
+ });