@jant/core 0.6.16 → 0.7.1

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 (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,413 @@
1
+ /**
2
+ * View language resolution.
3
+ *
4
+ * These are the rules the language routes are built on: which first path
5
+ * segment counts as a language, what the primary language's own prefix means,
6
+ * and how links stay inside the view they were rendered in.
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+ import type { Context } from "hono";
11
+ import type { AppConfig } from "../../types/config.js";
12
+ import {
13
+ buildLanguageSwitcher,
14
+ buildSurfaceAlternates,
15
+ getViewLang,
16
+ getViewLanguages,
17
+ isPerLanguageSurface,
18
+ isPrefixedLanguageView,
19
+ languageScopeBasePath,
20
+ languageUrlPrefixes,
21
+ resolveLanguageView,
22
+ toLanguagePath,
23
+ toViewPath,
24
+ viewBasePath,
25
+ viewRelativePath,
26
+ } from "../view-language.js";
27
+
28
+ interface FakeContextOptions {
29
+ path?: string;
30
+ query?: string;
31
+ viewLang?: string;
32
+ siteLanguage?: string;
33
+ additionalLanguages?: string[];
34
+ multilingualEnabled?: boolean;
35
+ sitePathPrefix?: string;
36
+ siteUrl?: string;
37
+ }
38
+
39
+ /** Minimal stand-in for the request context these helpers actually read. */
40
+ function fakeContext(options: FakeContextOptions = {}) {
41
+ const {
42
+ path = "/",
43
+ query = "",
44
+ viewLang,
45
+ siteLanguage = "zh-Hans",
46
+ additionalLanguages = ["en"],
47
+ multilingualEnabled = true,
48
+ sitePathPrefix = "",
49
+ siteUrl = "https://example.com",
50
+ } = options;
51
+
52
+ const appConfig = {
53
+ siteLanguage,
54
+ additionalLanguages,
55
+ multilingualEnabled,
56
+ sitePathPrefix,
57
+ siteUrl,
58
+ } as unknown as AppConfig;
59
+
60
+ return {
61
+ req: { path, url: `https://example.com${path}${query}` },
62
+ var: { appConfig, viewLang },
63
+ } as unknown as Parameters<typeof resolveLanguageView>[0];
64
+ }
65
+
66
+ describe("resolveLanguageView", () => {
67
+ it("serves a configured language prefix as a view", () => {
68
+ const c = fakeContext({ path: "/en/archive" });
69
+
70
+ expect(resolveLanguageView(c)).toEqual({ kind: "view", lang: "en" });
71
+ });
72
+
73
+ it("matches the prefix case-insensitively", () => {
74
+ const c = fakeContext({
75
+ path: "/zh-hant",
76
+ additionalLanguages: ["zh-Hant"],
77
+ });
78
+
79
+ expect(resolveLanguageView(c)).toEqual({ kind: "view", lang: "zh-Hant" });
80
+ });
81
+
82
+ it("passes a segment that is not a configured language", () => {
83
+ expect(resolveLanguageView(fakeContext({ path: "/fr" }))).toEqual({
84
+ kind: "pass",
85
+ });
86
+ expect(resolveLanguageView(fakeContext({ path: "/hello" }))).toEqual({
87
+ kind: "pass",
88
+ });
89
+ });
90
+
91
+ it("passes everything on a site with no second language", () => {
92
+ // Without this, a post at /en on a single-language site would 404.
93
+ const c = fakeContext({ path: "/en", additionalLanguages: [] });
94
+
95
+ expect(resolveLanguageView(c)).toEqual({ kind: "pass" });
96
+ });
97
+
98
+ it("ignores the primary language repeated in the additional list", () => {
99
+ const c = fakeContext({
100
+ path: "/zh-hans",
101
+ additionalLanguages: ["zh-Hans"],
102
+ });
103
+
104
+ expect(resolveLanguageView(c)).toEqual({ kind: "pass" });
105
+ });
106
+
107
+ describe("the primary language's own prefix", () => {
108
+ it("redirects to the root it aliases", () => {
109
+ const c = fakeContext({ path: "/zh-hans" });
110
+
111
+ expect(resolveLanguageView(c)).toEqual({ kind: "redirect", to: "/" });
112
+ });
113
+
114
+ it("strips the prefix from sub-paths, keeping the query", () => {
115
+ const c = fakeContext({ path: "/zh-hans/archive", query: "?year=2024" });
116
+
117
+ expect(resolveLanguageView(c)).toEqual({
118
+ kind: "redirect",
119
+ to: "/archive?year=2024",
120
+ });
121
+ });
122
+
123
+ it("keeps the deployment path prefix", () => {
124
+ const c = fakeContext({
125
+ path: "/zh-hans/archive",
126
+ sitePathPrefix: "/blog",
127
+ });
128
+
129
+ expect(resolveLanguageView(c)).toEqual({
130
+ kind: "redirect",
131
+ to: "/blog/archive",
132
+ });
133
+ });
134
+ });
135
+
136
+ describe("after multilingual is switched off", () => {
137
+ it("redirects a previously configured prefix", () => {
138
+ // The configuration is retained so old links and feed subscriptions
139
+ // keep working rather than 404ing.
140
+ const c = fakeContext({ path: "/en/feed", multilingualEnabled: false });
141
+
142
+ expect(resolveLanguageView(c)).toEqual({ kind: "redirect", to: "/feed" });
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("languageUrlPrefixes", () => {
148
+ it("lists every prefix a path can arrive under, primary included", () => {
149
+ // The primary language has no view of its own, but `/zh-hans/hello`
150
+ // redirects to `/hello` — so it still names the same page.
151
+ expect(
152
+ languageUrlPrefixes({
153
+ siteLanguage: "zh-Hans",
154
+ additionalLanguages: ["en", "ja"],
155
+ }),
156
+ ).toEqual(["zh-hans", "en", "ja"]);
157
+ });
158
+
159
+ it("is empty on a site with one language", () => {
160
+ // Nothing serves a view there, so `/en/hello` is just a post's address.
161
+ expect(
162
+ languageUrlPrefixes({
163
+ siteLanguage: "en",
164
+ additionalLanguages: [],
165
+ }),
166
+ ).toEqual([]);
167
+ expect(
168
+ languageUrlPrefixes({
169
+ siteLanguage: "en",
170
+ additionalLanguages: ["en"],
171
+ }),
172
+ ).toEqual([]);
173
+ });
174
+ });
175
+
176
+ describe("getViewLang", () => {
177
+ it("returns the prefix language inside a language view", () => {
178
+ expect(getViewLang(fakeContext({ viewLang: "en" }))).toBe("en");
179
+ });
180
+
181
+ it("returns the primary language at the root of a multilingual site", () => {
182
+ // The root is the primary language's view — otherwise `/` would show every
183
+ // language while `/en` showed one.
184
+ expect(getViewLang(fakeContext())).toBe("zh-Hans");
185
+ });
186
+
187
+ it("returns null on a single-language site", () => {
188
+ expect(getViewLang(fakeContext({ multilingualEnabled: false }))).toBeNull();
189
+ expect(getViewLang(fakeContext({ additionalLanguages: [] }))).toBeNull();
190
+ });
191
+ });
192
+
193
+ describe("isPrefixedLanguageView", () => {
194
+ it("is true only under a language prefix", () => {
195
+ expect(isPrefixedLanguageView(fakeContext({ viewLang: "en" }))).toBe(true);
196
+ expect(isPrefixedLanguageView(fakeContext())).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe("path helpers", () => {
201
+ it("prefixes in-view links and leaves root links alone", () => {
202
+ const inView = fakeContext({ viewLang: "en" });
203
+ const atRoot = fakeContext();
204
+
205
+ expect(toViewPath(inView, "/")).toBe("/en");
206
+ expect(toViewPath(inView, "/archive")).toBe("/en/archive");
207
+ expect(toViewPath(atRoot, "/")).toBe("/");
208
+ expect(toViewPath(atRoot, "/archive")).toBe("/archive");
209
+ });
210
+
211
+ it("composes with the deployment path prefix", () => {
212
+ const c = fakeContext({ viewLang: "en", sitePathPrefix: "/blog" });
213
+
214
+ expect(toViewPath(c, "/")).toBe("/blog/en");
215
+ expect(toViewPath(c, "/archive")).toBe("/blog/en/archive");
216
+ expect(viewBasePath(c)).toBe("/en");
217
+ });
218
+
219
+ it("strips the prefix when resolving against site content", () => {
220
+ expect(
221
+ viewRelativePath(fakeContext({ path: "/en/hello", viewLang: "en" })),
222
+ ).toBe("/hello");
223
+ expect(viewRelativePath(fakeContext({ path: "/en", viewLang: "en" }))).toBe(
224
+ "/",
225
+ );
226
+ expect(viewRelativePath(fakeContext({ path: "/hello" }))).toBe("/hello");
227
+ });
228
+
229
+ it("builds paths into another language's view", () => {
230
+ const c = fakeContext({ viewLang: "en" });
231
+
232
+ expect(toLanguagePath(c, "zh-Hans", "/archive")).toBe("/archive");
233
+ expect(toLanguagePath(c, "en", "/archive")).toBe("/en/archive");
234
+ expect(toLanguagePath(c, null, "/")).toBe("/");
235
+ });
236
+ });
237
+
238
+ describe("getViewLanguages", () => {
239
+ it("lists the primary language first", () => {
240
+ expect(
241
+ getViewLanguages(fakeContext({ additionalLanguages: ["en", "ja"] })),
242
+ ).toEqual(["zh-Hans", "en", "ja"]);
243
+ });
244
+
245
+ it("is empty when the site serves one language", () => {
246
+ expect(
247
+ getViewLanguages(fakeContext({ multilingualEnabled: false })),
248
+ ).toEqual([]);
249
+ expect(getViewLanguages(fakeContext({ additionalLanguages: [] }))).toEqual(
250
+ [],
251
+ );
252
+ });
253
+ });
254
+
255
+ describe("languageScopeBasePath", () => {
256
+ it("scopes an active non-primary language to its prefix", () => {
257
+ expect(languageScopeBasePath(fakeContext(), "en")).toBe("/en");
258
+ });
259
+
260
+ it("keeps the primary language at the root", () => {
261
+ expect(languageScopeBasePath(fakeContext(), "zh-Hans")).toBe("");
262
+ });
263
+
264
+ it("stays at the root without a language", () => {
265
+ expect(languageScopeBasePath(fakeContext(), null)).toBe("");
266
+ });
267
+
268
+ it("stays at the root while multilingual is off", () => {
269
+ // A post keeps its language column after the feature is switched off,
270
+ // but there is no /en view to scope its chrome to any more.
271
+ expect(
272
+ languageScopeBasePath(fakeContext({ multilingualEnabled: false }), "en"),
273
+ ).toBe("");
274
+ });
275
+
276
+ it("ignores a language the site no longer publishes", () => {
277
+ expect(languageScopeBasePath(fakeContext(), "ja")).toBe("");
278
+ });
279
+ });
280
+
281
+ describe("buildSurfaceAlternates", () => {
282
+ it("points at the same surface in every language", () => {
283
+ const c = fakeContext({ path: "/en/archive", viewLang: "en" });
284
+
285
+ expect(buildSurfaceAlternates(c)).toEqual([
286
+ { hreflang: "zh-Hans", href: "https://example.com/archive" },
287
+ { hreflang: "en", href: "https://example.com/en/archive" },
288
+ { hreflang: "x-default", href: "https://example.com/archive" },
289
+ ]);
290
+ });
291
+
292
+ it("keeps the query string, so a filtered view alternates correctly", () => {
293
+ const c = fakeContext({ path: "/archive", query: "?year=2024" });
294
+
295
+ expect(buildSurfaceAlternates(c)[1]).toEqual({
296
+ hreflang: "en",
297
+ href: "https://example.com/en/archive?year=2024",
298
+ });
299
+ });
300
+
301
+ it("is empty without an absolute site URL, which hreflang requires", () => {
302
+ expect(buildSurfaceAlternates(fakeContext({ siteUrl: "" }))).toEqual([]);
303
+ });
304
+
305
+ it("is empty on a single-language site", () => {
306
+ expect(
307
+ buildSurfaceAlternates(fakeContext({ multilingualEnabled: false })),
308
+ ).toEqual([]);
309
+ });
310
+ });
311
+
312
+ describe("buildLanguageSwitcher", () => {
313
+ it("offers the current surface in each language", () => {
314
+ const c = fakeContext({ path: "/en/archive", viewLang: "en" });
315
+
316
+ expect(buildLanguageSwitcher(c)).toEqual([
317
+ {
318
+ lang: "zh-Hans",
319
+ label: "简体中文",
320
+ href: "/archive",
321
+ isCurrent: false,
322
+ isPrimary: true,
323
+ },
324
+ {
325
+ lang: "en",
326
+ label: "English",
327
+ href: "/en/archive",
328
+ isCurrent: true,
329
+ isPrimary: false,
330
+ },
331
+ ]);
332
+ });
333
+
334
+ it("takes an explicit destination per language", () => {
335
+ // A post links to its translation, not to its own path in another view.
336
+ const c = fakeContext({ path: "/my-post" });
337
+
338
+ expect(
339
+ buildLanguageSwitcher(c, {
340
+ hrefByLanguage: new Map([["en", "/the-translation"]]),
341
+ fallbackPath: "/",
342
+ currentLang: "zh-Hans",
343
+ }),
344
+ ).toEqual([
345
+ {
346
+ lang: "zh-Hans",
347
+ label: "简体中文",
348
+ href: "/",
349
+ isCurrent: true,
350
+ isPrimary: true,
351
+ },
352
+ {
353
+ lang: "en",
354
+ label: "English",
355
+ href: "/the-translation",
356
+ isCurrent: false,
357
+ isPrimary: false,
358
+ },
359
+ ]);
360
+ });
361
+
362
+ it("falls back to a language's home when there is no translation", () => {
363
+ const c = fakeContext({ path: "/my-post" });
364
+
365
+ const options = buildLanguageSwitcher(c, { fallbackPath: "/" });
366
+ expect(options.map((option) => option.href)).toEqual(["/", "/en"]);
367
+ });
368
+
369
+ it("is empty on a single-language site", () => {
370
+ expect(
371
+ buildLanguageSwitcher(fakeContext({ additionalLanguages: [] })),
372
+ ).toEqual([]);
373
+ });
374
+
375
+ it("aims at each language's home from a page no language view serves", () => {
376
+ // /ja/settings/language would be a 404 — the switcher must never mint one.
377
+ const c = fakeContext({ path: "/settings/language" });
378
+
379
+ expect(buildLanguageSwitcher(c).map((option) => option.href)).toEqual([
380
+ "/",
381
+ "/en",
382
+ ]);
383
+ });
384
+ });
385
+
386
+ describe("isPerLanguageSurface", () => {
387
+ it("recognizes the surfaces the language routes serve", () => {
388
+ expect(isPerLanguageSurface("/")).toBe(true);
389
+ expect(isPerLanguageSurface("/archive")).toBe(true);
390
+ expect(isPerLanguageSurface("/collections/reading")).toBe(true);
391
+ });
392
+
393
+ it("rejects everything without a per-language counterpart", () => {
394
+ expect(isPerLanguageSurface("/settings")).toBe(false);
395
+ expect(isPerLanguageSurface("/settings/language")).toBe(false);
396
+ expect(isPerLanguageSurface("/my-post")).toBe(false);
397
+ });
398
+
399
+ it("counts a collection selection and its feed, and nothing deeper", () => {
400
+ // `langGet()` serves /collections/:selection and its feed, and nothing
401
+ // else under /collections.
402
+ expect(isPerLanguageSurface("/collections/a+b")).toBe(true);
403
+ expect(isPerLanguageSurface("/collections/a+b/feed")).toBe(true);
404
+ expect(isPerLanguageSurface("/collections/a+b/feed/atom")).toBe(false);
405
+ expect(isPerLanguageSurface("/collections/reading/edit")).toBe(false);
406
+ });
407
+
408
+ it("ignores a query string, so a built link can be passed as-is", () => {
409
+ expect(isPerLanguageSurface("/archive?media=any")).toBe(true);
410
+ expect(isPerLanguageSurface("/search?q=hello")).toBe(true);
411
+ expect(isPerLanguageSurface("/settings?tab=general")).toBe(false);
412
+ });
413
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { Bindings } from "../../types.js";
3
3
  import { POST_BODY_HTML_VERSION } from "../post-body-html.js";
4
+ import { withConditionalResponse } from "../http-cache.js";
4
5
  import {
5
6
  normalizeWorkerCacheKeyUrl,
6
7
  withWorkerResponseCache,
@@ -326,3 +327,92 @@ describe("withWorkerResponseCache", () => {
326
327
  expect(nextCallCount).toBe(1);
327
328
  });
328
329
  });
330
+
331
+ /**
332
+ * Revalidation composed with the cache, the way `createApp()` composes them:
333
+ * conditional handling on the outside, so it sees a cache hit and a cache miss
334
+ * alike.
335
+ */
336
+ describe("withWorkerResponseCache under conditional requests", () => {
337
+ const FEED_URL = "https://example.com/feed";
338
+ const ETAG = '"feed-v1"';
339
+
340
+ function feedResponse() {
341
+ return new Response("<feed/>", {
342
+ headers: {
343
+ "Cache-Control": "public, max-age=60",
344
+ "Content-Type": "application/atom+xml; charset=utf-8",
345
+ ETag: ETAG,
346
+ },
347
+ });
348
+ }
349
+
350
+ async function poll(
351
+ cache: ReturnType<typeof createMemoryCache>,
352
+ ifNoneMatch: string | undefined,
353
+ next: () => Promise<Response>,
354
+ ) {
355
+ const request = new Request(
356
+ FEED_URL,
357
+ ifNoneMatch ? { headers: { "If-None-Match": ifNoneMatch } } : undefined,
358
+ );
359
+
360
+ return withConditionalResponse(
361
+ request,
362
+ await withWorkerResponseCache({
363
+ bindings: createCloudflareBindings(),
364
+ cache,
365
+ request,
366
+ next,
367
+ }),
368
+ );
369
+ }
370
+
371
+ it("stores the full document even when the poll that filled the cache got a 304", async () => {
372
+ const cache = createMemoryCache();
373
+ let renders = 0;
374
+ const render = async () => {
375
+ renders += 1;
376
+ return feedResponse();
377
+ };
378
+
379
+ // A reader that already holds this version, arriving on a cache miss.
380
+ const conditional = await poll(cache, ETAG, render);
381
+ expect(conditional.status).toBe(304);
382
+ expect(await conditional.text()).toBe("");
383
+ expect(renders).toBe(1);
384
+
385
+ // The next reader gets the document from the cache, not another render.
386
+ const unconditional = await poll(cache, undefined, render);
387
+ expect(unconditional.status).toBe(200);
388
+ expect(await unconditional.text()).toBe("<feed/>");
389
+ expect(renders).toBe(1);
390
+ });
391
+
392
+ it("revalidates a cache hit without reaching a route", async () => {
393
+ const cache = createMemoryCache();
394
+ let renders = 0;
395
+ const render = async () => {
396
+ renders += 1;
397
+ return feedResponse();
398
+ };
399
+
400
+ await poll(cache, undefined, render);
401
+ const revalidated = await poll(cache, ETAG, render);
402
+
403
+ expect(revalidated.status).toBe(304);
404
+ expect(revalidated.headers.get("ETag")).toBe(ETAG);
405
+ expect(revalidated.headers.get("Cache-Control")).toBe("public, max-age=60");
406
+ expect(renders).toBe(1);
407
+ });
408
+
409
+ it("serves the document again once the cached version no longer matches", async () => {
410
+ const cache = createMemoryCache();
411
+
412
+ await poll(cache, undefined, async () => feedResponse());
413
+ const stale = await poll(cache, '"feed-v0"', async () => feedResponse());
414
+
415
+ expect(stale.status).toBe(200);
416
+ expect(await stale.text()).toBe("<feed/>");
417
+ });
418
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Addresses the author pasted.
3
+ *
4
+ * Pickers ask "which page?" and the author often answers with a URL rather
5
+ * than a title — they have the page open in another tab. Turning that answer
6
+ * into something the path registry can resolve is the same job everywhere, and
7
+ * it is fiddly enough (site prefix, language prefix, which hosts are ours) to
8
+ * be worth doing in one place.
9
+ */
10
+
11
+ import type { Context } from "hono";
12
+ import { z } from "zod";
13
+ import type { Bindings } from "../types/bindings.js";
14
+ import type { AppVariables } from "../types/app-context.js";
15
+ import { toInternalPath } from "./url.js";
16
+ import { languageUrlPrefixes } from "./view-language.js";
17
+
18
+ type AddressContext = Context<{ Bindings: Bindings; Variables: AppVariables }>;
19
+
20
+ /** Query shape of every "what is at this address?" endpoint. */
21
+ export const AddressQuerySchema = z.object({
22
+ url: z.string().trim().min(1).max(2048),
23
+ });
24
+
25
+ /**
26
+ * Convert an address into the internal path this site resolves it against.
27
+ *
28
+ * Accepts a path or a full URL on any host the site answers on — the
29
+ * configured one *and* the host of the current request, because a hosted site
30
+ * is routinely reached on a domain its canonical URL knows nothing about.
31
+ *
32
+ * @param c - Request context, after `withConfig()`
33
+ * @param input - What the author typed or pasted
34
+ * @returns Internal path rooted at `/`, or `null` when the address is not ours
35
+ * @example
36
+ * requestInternalPath(c, "https://example.com/en/about"); // "/about"
37
+ * requestInternalPath(c, "https://elsewhere.com/about"); // null
38
+ */
39
+ export function requestInternalPath(
40
+ c: AddressContext,
41
+ input: string,
42
+ ): string | null {
43
+ const { siteOrigin, sitePathPrefix } = c.var.appConfig;
44
+
45
+ let requestOrigin = "";
46
+ try {
47
+ requestOrigin = new URL(c.req.url).origin;
48
+ } catch {
49
+ // A request URL that will not parse is not one we can match against.
50
+ }
51
+
52
+ return toInternalPath(input, {
53
+ siteOrigins: [siteOrigin, requestOrigin].filter(Boolean),
54
+ sitePathPrefix,
55
+ languagePrefixes: languageUrlPrefixes(c.var.appConfig),
56
+ });
57
+ }
@@ -1,5 +1,6 @@
1
1
  import { isTextAttachment } from "../services/media.js";
2
2
  import type { Media, Post } from "../types.js";
3
+ import { getPostDisplayTitle } from "./post-meta.js";
3
4
  import { getImageUrl, getMediaUrl, getPublicUrlForProvider } from "./image.js";
4
5
  import { toPublicPath } from "./url.js";
5
6
 
@@ -7,6 +8,12 @@ export type ApiPostResponse = Omit<Post, "title" | "url"> & {
7
8
  attachments?: ReturnType<typeof toApiAttachment>[];
8
9
  collectionIds?: string[];
9
10
  title?: string | null;
11
+ /**
12
+ * Short plain-text name for this Post, derived when it has no title of its
13
+ * own. Use it wherever the Post is referenced from somewhere else; a slug is
14
+ * a URL, not a name.
15
+ */
16
+ displayTitle: string;
10
17
  url?: string | null;
11
18
  sourceName?: string | null;
12
19
  sourceUrl?: string | null;
@@ -82,11 +89,15 @@ export function toApiPost(
82
89
  } = {},
83
90
  ): ApiPostResponse {
84
91
  const { title, url, ...rest } = post;
92
+ // A short name for this Post wherever it is referenced from somewhere else —
93
+ // notes are usually untitled, so the client cannot just read `title`.
94
+ const displayTitle = getPostDisplayTitle(post);
85
95
 
86
96
  if (post.format === "quote") {
87
97
  return {
88
98
  ...rest,
89
99
  ...extras,
100
+ displayTitle,
90
101
  sourceName: title ?? null,
91
102
  sourceUrl: url ?? null,
92
103
  };
@@ -95,6 +106,7 @@ export function toApiPost(
95
106
  return {
96
107
  ...rest,
97
108
  ...extras,
109
+ displayTitle,
98
110
  title: title ?? null,
99
111
  url: url ?? null,
100
112
  };