@jant/core 0.6.15 → 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 (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  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__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -5,9 +5,8 @@
5
5
  import { Hono, type Context } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../types/app-context.js";
8
- import { requireAuth } from "../../middleware/auth.js";
9
8
  import { CollectionPage } from "../../ui/pages/CollectionPage.js";
10
- import { CollectionEditorPage } from "../../ui/pages/CollectionEditorPage.js";
9
+ import type { CollectionPageProps } from "../../types.js";
11
10
  import { getNavigationData } from "../../lib/navigation.js";
12
11
  import { formatPageLabel, parsePageNumber } from "../../lib/pagination.js";
13
12
  import { buildPageTitle } from "../../lib/page-title.js";
@@ -20,14 +19,24 @@ import {
20
19
  import { assembleCollectionTimeline } from "../../lib/timeline.js";
21
20
  import { defaultFeedRenderer } from "../../lib/feed.js";
22
21
  import {
22
+ buildFeedDiscoveryFields,
23
23
  getFeedEntryUpdatedAt,
24
+ getFeedLimit,
24
25
  getRssPublishedBefore,
25
- RSS_FEED_CACHE_CONTROL,
26
+ renderFeed,
26
27
  } from "../../lib/feed-policy.js";
27
28
  import { toPlainText as markdownToPlainText } from "../../lib/markdown.js";
28
29
  import { buildMediaMap } from "../../lib/media-helpers.js";
29
30
  import { createMediaContext, toPostViews } from "../../lib/view.js";
30
- import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
31
+ import { toAbsoluteSiteUrl } from "../../lib/url.js";
32
+ import {
33
+ buildLanguageSwitcher,
34
+ buildSurfaceAlternates,
35
+ getViewLang,
36
+ toViewPath,
37
+ viewBasePath,
38
+ } from "../../lib/view-language.js";
39
+ import { getOrBuildEntry } from "../../i18n/supported-locales.js";
31
40
  import {
32
41
  getCollectionPagePath,
33
42
  getCollectionSelectionFeedPath,
@@ -58,52 +67,6 @@ function buildCollectionSelectionTitle(
58
67
  return collections.map((collection) => collection.title).join(" + ");
59
68
  }
60
69
 
61
- function resolveReturnHref(
62
- value: string | undefined,
63
- fallback: string,
64
- ): string {
65
- if (!value || !value.startsWith("/") || value.startsWith("//")) {
66
- return fallback;
67
- }
68
-
69
- return value;
70
- }
71
-
72
- collectionRoutes.use("/:slug/edit", requireAuth());
73
-
74
- collectionRoutes.get("/:slug/edit", async (c) => {
75
- const slug = c.req.param("slug");
76
- if (isAggregateCollectionSelection(slug)) return c.notFound();
77
-
78
- const [collection, navData] = await Promise.all([
79
- c.var.services.collections.getBySlug(slug),
80
- getNavigationData(c),
81
- ]);
82
- if (!collection) return c.notFound();
83
-
84
- const defaultReturnHref = toPublicPath(
85
- getCollectionPagePath(collection.slug),
86
- navData.sitePathPrefix,
87
- );
88
- const cancelHref = resolveReturnHref(
89
- c.req.query("returnTo"),
90
- defaultReturnHref,
91
- );
92
-
93
- return renderPublicPage(c, {
94
- title: buildPageTitle("Edit", collection.title, navData.siteName),
95
- navData,
96
- content: (
97
- <CollectionEditorPage
98
- mode="edit"
99
- collection={collection}
100
- cancelHref={cancelHref}
101
- sitePathPrefix={navData.sitePathPrefix}
102
- />
103
- ),
104
- });
105
- });
106
-
107
70
  /**
108
71
  * Render a collection selection page. Used by root-level single-collection
109
72
  * paths, collection aliases resolved through the path registry, and aggregate
@@ -133,10 +96,7 @@ export async function renderCollectionPage(
133
96
  // Only redirect for slug normalization when using the derived canonical path
134
97
  if (!pagePathOverride && slugExpression !== selection.slugExpression) {
135
98
  const search = new URL(c.req.url).search;
136
- return c.redirect(
137
- toPublicPath(`${canonicalPagePath}${search}`, navData.sitePathPrefix),
138
- 301,
139
- );
99
+ return c.redirect(`${toViewPath(c, canonicalPagePath)}${search}`, 301);
140
100
  }
141
101
 
142
102
  const sortQuery = c.req.query("sort");
@@ -192,6 +152,34 @@ export async function renderCollectionPage(
192
152
  i18n,
193
153
  );
194
154
 
155
+ // An empty page in one language is only a dead end if the reader cannot see
156
+ // that the collection has something in another. The extra count runs only
157
+ // when this view came up empty.
158
+ const viewLang = getViewLang(c);
159
+ let emptyInLanguage: CollectionPageProps["emptyInLanguage"];
160
+ if (totalThreadCount === 0 && viewLang) {
161
+ const acrossLanguages =
162
+ await c.var.services.posts.countCollectionThreadRootsForCollections(
163
+ collectionIds,
164
+ { status: "published", excludePrivate: !navData.isAuthenticated },
165
+ );
166
+ if (acrossLanguages > 0) {
167
+ const alternatives = buildLanguageSwitcher(c, {
168
+ fallbackPath: canonicalPagePath,
169
+ }).filter((option) => !option.isCurrent);
170
+ if (alternatives.length > 0) {
171
+ emptyInLanguage = {
172
+ languageLabel: getOrBuildEntry(viewLang).native,
173
+ alternatives,
174
+ };
175
+ }
176
+ }
177
+ }
178
+
179
+ const feedHref = c.var.appConfig.rssFeedsEnabled
180
+ ? `${canonicalPagePath}/feed`
181
+ : undefined;
182
+
195
183
  return renderPublicPage(c, {
196
184
  title:
197
185
  page > 1
@@ -202,6 +190,10 @@ export async function renderCollectionPage(
202
190
  : primaryCollection.description
203
191
  ? markdownToPlainText(primaryCollection.description)
204
192
  : undefined,
193
+ alternateLanguages: buildSurfaceAlternates(c),
194
+ pageFeed: feedHref
195
+ ? { href: toViewPath(c, feedHref), title: selectionTitle }
196
+ : undefined,
205
197
  navData,
206
198
  composeCollectionId: !isAggregate ? primaryCollection.id : undefined,
207
199
  content: (
@@ -214,11 +206,8 @@ export async function renderCollectionPage(
214
206
  pagePath={canonicalPagePath}
215
207
  baseUrl={
216
208
  currentSort === defaultSort
217
- ? toPublicPath(canonicalPagePath, navData.sitePathPrefix)
218
- : toPublicPath(
219
- `${canonicalPagePath}?sort=${currentSort}`,
220
- navData.sitePathPrefix,
221
- )
209
+ ? toViewPath(c, canonicalPagePath)
210
+ : toViewPath(c, `${canonicalPagePath}?sort=${currentSort}`)
222
211
  }
223
212
  currentSort={currentSort}
224
213
  defaultSort={defaultSort}
@@ -230,11 +219,9 @@ export async function renderCollectionPage(
230
219
  item.collectionId === primaryCollection.id,
231
220
  )}
232
221
  sitePathPrefix={navData.sitePathPrefix}
233
- feedHref={
234
- c.var.appConfig.rssFeedsEnabled
235
- ? `${canonicalPagePath}/feed`
236
- : undefined
237
- }
222
+ basePath={navData.basePath}
223
+ emptyInLanguage={emptyInLanguage}
224
+ feedHref={feedHref}
238
225
  />
239
226
  ),
240
227
  });
@@ -255,20 +242,15 @@ export async function renderCollectionFeed(
255
242
 
256
243
  if (!feedPathOverride && slugExpression !== selection.slugExpression) {
257
244
  const search = new URL(c.req.url).search;
258
- return c.redirect(
259
- toPublicPath(
260
- `${canonicalFeedPath}${search}`,
261
- c.var.appConfig.sitePathPrefix,
262
- ),
263
- 301,
264
- );
245
+ return c.redirect(`${toViewPath(c, canonicalFeedPath)}${search}`, 301);
265
246
  }
266
247
 
267
248
  const { appConfig } = c.var;
268
249
  const siteName = appConfig.siteName;
269
250
  const siteUrl = appConfig.siteUrl;
270
- const siteLanguage = appConfig.siteLanguage;
271
- const feedLimit = appConfig.rssFeedLimit;
251
+ // A language view's feed is that language's feed, so it declares it.
252
+ const siteLanguage = getViewLang(c) ?? appConfig.siteLanguage;
253
+ const feedLimit = getFeedLimit(c);
272
254
  const publishedBefore = getRssPublishedBefore(
273
255
  appConfig.rssPublishDelaySeconds,
274
256
  );
@@ -281,6 +263,7 @@ export async function renderCollectionFeed(
281
263
  {
282
264
  status: "published",
283
265
  excludePrivate: true,
266
+ lang: getViewLang(c) ?? undefined,
284
267
  ignoreCollectionPinnedSort: true,
285
268
  publishedBefore,
286
269
  limit: feedLimit,
@@ -377,14 +360,19 @@ export async function renderCollectionFeed(
377
360
  );
378
361
 
379
362
  const xml = defaultFeedRenderer({
380
- siteName: buildPageTitle(selectionTitle, siteName),
363
+ ...buildFeedDiscoveryFields(c),
364
+ siteName,
365
+ // Site name first, like every other feed: a reader's sidebar sorts by
366
+ // feed title, so leading with the collection would scatter one site's
367
+ // feeds across the alphabet.
368
+ title: buildPageTitle(siteName, selectionTitle),
381
369
  siteDescription:
382
370
  selection.collections.length === 1 && primaryCollection.description
383
371
  ? markdownToPlainText(primaryCollection.description)
384
372
  : "",
385
373
  siteUrl,
386
374
  selfUrl: toAbsoluteSiteUrl(
387
- canonicalFeedPath,
375
+ `${viewBasePath(c)}${canonicalFeedPath}`,
388
376
  siteUrl,
389
377
  appConfig.sitePathPrefix,
390
378
  ),
@@ -392,17 +380,23 @@ export async function renderCollectionFeed(
392
380
  posts: postViews,
393
381
  });
394
382
 
395
- return new Response(xml, {
396
- headers: {
397
- "Content-Type": "application/atom+xml; charset=utf-8",
398
- "Cache-Control": RSS_FEED_CACHE_CONTROL,
399
- },
400
- });
383
+ return renderFeed(xml);
401
384
  }
402
385
 
403
- collectionRoutes.get("/:slug", async (c) => {
386
+ /**
387
+ * Serve `/collections/{expression}` — the aggregate selection page.
388
+ *
389
+ * A single slug does not live here; it shares the root namespace with posts,
390
+ * so it is redirected to its canonical root-level address.
391
+ *
392
+ * @param c - Hono context
393
+ * @returns Collection page, a redirect, or 404
394
+ */
395
+ export async function renderCollectionSelectionRoute(
396
+ c: Context<Env>,
397
+ ): Promise<Response> {
404
398
  const slugExpression = c.req.param("slug");
405
- const sitePathPrefix = c.var.appConfig.sitePathPrefix;
399
+ if (!slugExpression) return c.notFound();
406
400
 
407
401
  if (!isAggregateCollectionSelection(slugExpression)) {
408
402
  const collection =
@@ -411,22 +405,26 @@ collectionRoutes.get("/:slug", async (c) => {
411
405
 
412
406
  const search = new URL(c.req.url).search;
413
407
  return c.redirect(
414
- toPublicPath(
415
- `${getCollectionPagePath(collection.slug)}${search}`,
416
- sitePathPrefix,
417
- ),
408
+ `${toViewPath(c, getCollectionPagePath(collection.slug))}${search}`,
418
409
  301,
419
410
  );
420
411
  }
421
412
 
422
413
  const result = await renderCollectionPage(c, slugExpression);
423
414
  return result ?? c.notFound();
424
- });
415
+ }
425
416
 
426
- // Collection RSS feed
427
- collectionRoutes.get("/:slug/feed", async (c) => {
417
+ /**
418
+ * Serve `/collections/{expression}/feed` — the aggregate selection's feed.
419
+ *
420
+ * @param c - Hono context
421
+ * @returns Atom feed, a redirect, or 404
422
+ */
423
+ export async function renderCollectionSelectionFeedRoute(
424
+ c: Context<Env>,
425
+ ): Promise<Response> {
428
426
  const slugExpression = c.req.param("slug");
429
- const sitePathPrefix = c.var.appConfig.sitePathPrefix;
427
+ if (!slugExpression) return c.notFound();
430
428
 
431
429
  if (!isAggregateCollectionSelection(slugExpression)) {
432
430
  const collection =
@@ -435,14 +433,14 @@ collectionRoutes.get("/:slug/feed", async (c) => {
435
433
 
436
434
  const search = new URL(c.req.url).search;
437
435
  return c.redirect(
438
- toPublicPath(
439
- `${getCollectionSelectionFeedPath(collection.slug)}${search}`,
440
- sitePathPrefix,
441
- ),
436
+ `${toViewPath(c, getCollectionSelectionFeedPath(collection.slug))}${search}`,
442
437
  301,
443
438
  );
444
439
  }
445
440
 
446
441
  const result = await renderCollectionFeed(c, slugExpression);
447
442
  return result ?? c.notFound();
448
- });
443
+ }
444
+
445
+ collectionRoutes.get("/:slug", renderCollectionSelectionRoute);
446
+ collectionRoutes.get("/:slug/feed", renderCollectionSelectionFeedRoute);
@@ -5,79 +5,67 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
+ import type { Context } from "hono";
8
9
  import type { Bindings } from "../../types.js";
9
10
  import type { AppVariables } from "../../types/app-context.js";
10
- import { requireAuth } from "../../middleware/auth.js";
11
- import { getCollectionsDirectoryPath } from "../../lib/collection-paths.js";
12
- import { getNavigationData } from "../../lib/navigation.js";
11
+ import {
12
+ collectNavigationCollectionIds,
13
+ getNavigationData,
14
+ } from "../../lib/navigation.js";
13
15
  import { buildPageTitle } from "../../lib/page-title.js";
14
16
  import { renderPublicPage } from "../../lib/render.js";
15
- import { toPublicPath } from "../../lib/url.js";
16
- import { CollectionEditorPage } from "../../ui/pages/CollectionEditorPage.js";
17
+ import {
18
+ buildSurfaceAlternates,
19
+ getViewLang,
20
+ } from "../../lib/view-language.js";
17
21
  import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
18
22
 
19
23
  type Env = { Bindings: Bindings; Variables: AppVariables };
20
24
 
21
25
  export const collectionsPageRoutes = new Hono<Env>();
22
26
 
23
- function resolveReturnHref(
24
- value: string | undefined,
25
- fallback: string,
26
- ): string {
27
- if (!value || !value.startsWith("/") || value.startsWith("//")) {
28
- return fallback;
29
- }
30
-
31
- return value;
32
- }
33
-
34
- collectionsPageRoutes.use("/new", requireAuth());
35
-
36
- collectionsPageRoutes.get("/new", async (c) => {
37
- const navData = await getNavigationData(c);
38
- const defaultReturnHref = toPublicPath(
39
- getCollectionsDirectoryPath(),
40
- navData.sitePathPrefix,
41
- );
42
- const cancelHref = resolveReturnHref(
43
- c.req.query("returnTo"),
44
- defaultReturnHref,
45
- );
46
-
47
- return renderPublicPage(c, {
48
- title: buildPageTitle("New Collection", navData.siteName),
49
- navData,
50
- content: (
51
- <CollectionEditorPage
52
- mode="create"
53
- cancelHref={cancelHref}
54
- sitePathPrefix={navData.sitePathPrefix}
55
- />
56
- ),
57
- });
58
- });
59
-
60
- collectionsPageRoutes.get("/", async (c) => {
27
+ /**
28
+ * Render the collections directory.
29
+ *
30
+ * The directory itself is site skeleton: every collection is listed in every
31
+ * language view, because a collection that is empty in one language is still
32
+ * part of how the site is organized. Only its links carry the view prefix.
33
+ *
34
+ * @param c - Hono context
35
+ * @returns Collections directory response
36
+ */
37
+ export async function renderCollectionsDirectory(
38
+ c: Context<Env>,
39
+ ): Promise<Response> {
40
+ // The numbers have to match the collection pages these rows link to, so they
41
+ // carry this reader's visibility and this view's language. Both come from the
42
+ // request rather than from `navData` — `navData.isAuthenticated` is
43
+ // `c.var.isAuthenticated` verbatim — so the two round trips stay parallel on
44
+ // the widest page under `/collections`.
61
45
  const [directoryData, navData] = await Promise.all([
62
- c.var.services.collections.listDirectoryData(),
46
+ c.var.services.collections.listDirectoryData({
47
+ isAuthenticated: c.var.isAuthenticated,
48
+ lang: getViewLang(c) ?? undefined,
49
+ }),
63
50
  getNavigationData(c),
64
51
  ]);
65
52
 
66
53
  return renderPublicPage(c, {
67
54
  title: buildPageTitle("Collections", navData.siteName),
55
+ alternateLanguages: buildSurfaceAlternates(c),
68
56
  navData,
69
57
  content: (
70
58
  <CollectionsPage
71
59
  items={directoryData.items}
72
60
  isAuthenticated={navData.isAuthenticated ?? false}
73
- navigationCollectionIds={navData.links.flatMap((item) =>
74
- item.type === "collection" && item.collectionId
75
- ? [item.collectionId]
76
- : [],
77
- )}
61
+ navigationCollectionIds={collectNavigationCollectionIds(navData.links)}
78
62
  sitePathPrefix={navData.sitePathPrefix}
63
+ basePath={navData.basePath}
79
64
  siteOrigin={c.var.appConfig.siteOrigin}
65
+ feedsEnabled={c.var.appConfig.rssFeedsEnabled}
80
66
  />
81
67
  ),
82
68
  });
83
- });
69
+ }
70
+
71
+ collectionsPageRoutes.get("/", renderCollectionsDirectory);
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
+ import type { Context } from "hono";
8
9
  import { msg } from "@lingui/core/macro";
9
10
  import type { Bindings } from "../../types.js";
10
11
  import type { AppVariables } from "../../types/app-context.js";
@@ -14,16 +15,23 @@ import { formatPageLabel, parsePageNumber } from "../../lib/pagination.js";
14
15
  import { buildPageTitle } from "../../lib/page-title.js";
15
16
  import { renderPublicPage } from "../../lib/render.js";
16
17
  import { assembleFeaturedTimeline } from "../../lib/timeline.js";
17
- import { toPublicPath } from "../../lib/url.js";
18
+ import { buildSurfaceAlternates, toViewPath } from "../../lib/view-language.js";
18
19
  import { defaultFeedRenderer } from "../../lib/feed.js";
19
- import { buildFeedData, renderFeed } from "../feed/feed.js";
20
+ import { renderFeed } from "../../lib/feed-policy.js";
21
+ import { buildFeedData } from "../feed/feed.js";
20
22
  import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
21
23
 
22
24
  type Env = { Bindings: Bindings; Variables: AppVariables };
23
25
 
24
26
  export const featuredRoutes = new Hono<Env>();
25
27
 
26
- featuredRoutes.get("/", async (c) => {
28
+ /**
29
+ * Render the featured timeline for the current view language.
30
+ *
31
+ * @param c - Hono context
32
+ * @returns Featured page response
33
+ */
34
+ export async function renderFeaturedPage(c: Context<Env>): Promise<Response> {
27
35
  const navData = await getNavigationData(c);
28
36
  const i18n = getI18n(c);
29
37
 
@@ -45,29 +53,40 @@ featuredRoutes.get("/", async (c) => {
45
53
  page > 1
46
54
  ? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName)
47
55
  : buildPageTitle(featuredTitle, navData.siteName),
56
+ alternateLanguages: buildSurfaceAlternates(c),
48
57
  navData,
49
58
  content: (
50
59
  <FeaturedPage
51
60
  items={items}
52
61
  currentPage={currentPage}
53
62
  totalPages={totalPages}
54
- baseUrl={toPublicPath("/featured", navData.sitePathPrefix)}
63
+ baseUrl={toViewPath(c, "/featured")}
55
64
  />
56
65
  ),
57
66
  });
58
- });
67
+ }
59
68
 
60
- // Atom — /featured/feed (canonical featured feed)
61
- featuredRoutes.get("/feed", async (c) => {
69
+ featuredRoutes.get("/", renderFeaturedPage);
70
+
71
+ /**
72
+ * Render the featured Atom feed for the current view language.
73
+ *
74
+ * @param c - Hono context
75
+ * @returns Atom feed response
76
+ */
77
+ export async function renderFeaturedFeed(c: Context<Env>): Promise<Response> {
62
78
  const feedData = await buildFeedData(c, {
63
79
  kind: "featured",
64
80
  selfPath: "/featured/feed",
65
81
  });
66
82
  return renderFeed(defaultFeedRenderer(feedData));
67
- });
83
+ }
68
84
 
69
- // Legacy atom.xml suffix → canonical /featured/feed
70
- featuredRoutes.get("/feed/atom.xml", (c) => {
71
- const sitePathPrefix = c.var.appConfig.sitePathPrefix;
72
- return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
73
- });
85
+ /** Legacy atom.xml suffix → canonical /featured/feed, inside the same view. */
86
+ export function redirectLegacyFeaturedFeed(c: Context<Env>): Response {
87
+ return c.redirect(toViewPath(c, "/featured/feed"), 308);
88
+ }
89
+
90
+ // Atom — /featured/feed (canonical featured feed)
91
+ featuredRoutes.get("/feed", renderFeaturedFeed);
92
+ featuredRoutes.get("/feed/atom.xml", redirectLegacyFeaturedFeed);
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { Hono } from "hono";
9
+ import type { Context } from "hono";
9
10
  import { msg } from "@lingui/core/macro";
10
11
  import type { Bindings } from "../../types.js";
11
12
  import type { AppVariables } from "../../types/app-context.js";
@@ -16,6 +17,11 @@ import { buildPageTitle } from "../../lib/page-title.js";
16
17
  import { renderPublicPage } from "../../lib/render.js";
17
18
  import { assembleTimeline } from "../../lib/timeline.js";
18
19
  import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
20
+ import {
21
+ buildSurfaceAlternates,
22
+ toViewPath,
23
+ viewBasePath,
24
+ } from "../../lib/view-language.js";
19
25
  import { buildWebSiteJsonLd } from "../../lib/structured-data.js";
20
26
  import { HomePage } from "../../ui/pages/HomePage.js";
21
27
 
@@ -23,7 +29,13 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
23
29
 
24
30
  export const homeRoutes = new Hono<Env>();
25
31
 
26
- homeRoutes.get("/", async (c) => {
32
+ /**
33
+ * Render the timeline home page for the current view language.
34
+ *
35
+ * @param c - Hono context
36
+ * @returns Home page response
37
+ */
38
+ export async function renderHomePage(c: Context<Env>): Promise<Response> {
27
39
  const i18n = getI18n(c);
28
40
  const page = parsePageNumber(c.req.query("page"));
29
41
  const paginatedPageTitle = formatPageLabel(page);
@@ -46,9 +58,13 @@ homeRoutes.get("/", async (c) => {
46
58
  page === 1 && siteUrl
47
59
  ? buildWebSiteJsonLd({
48
60
  name: navData.siteName,
49
- url: toAbsoluteSiteUrl("/", siteUrl, navData.sitePathPrefix),
61
+ url: toAbsoluteSiteUrl(
62
+ viewBasePath(c) || "/",
63
+ siteUrl,
64
+ navData.sitePathPrefix,
65
+ ),
50
66
  searchUrlTemplate: `${toAbsoluteSiteUrl(
51
- "/search",
67
+ `${viewBasePath(c)}/search`,
52
68
  siteUrl,
53
69
  navData.sitePathPrefix,
54
70
  )}?q={search_term_string}`,
@@ -68,18 +84,21 @@ homeRoutes.get("/", async (c) => {
68
84
  ? buildPageTitle(latestTitle, paginatedPageTitle, navData.siteName)
69
85
  : navData.siteName,
70
86
  jsonLd: websiteJsonLd,
87
+ alternateLanguages: buildSurfaceAlternates(c),
71
88
  navData,
72
89
  showHomeBranding:
73
90
  c.var.appConfig.showJantBrandingOnHome && currentPage === 1,
74
91
  content: (
75
92
  <HomePage
76
93
  items={items}
77
- baseUrl={toPublicPath("/", navData.sitePathPrefix)}
94
+ baseUrl={toViewPath(c, "/")}
78
95
  currentPage={currentPage}
79
96
  totalPages={totalPages}
80
97
  isAuthenticated={isAuthenticated}
81
- signinUrl={`${toPublicPath("/signin", navData.sitePathPrefix)}?redirect=${encodeURIComponent(toPublicPath("/", navData.sitePathPrefix))}`}
98
+ signinUrl={`${toPublicPath("/signin", navData.sitePathPrefix)}?redirect=${encodeURIComponent(toViewPath(c, "/"))}`}
82
99
  />
83
100
  ),
84
101
  });
85
- });
102
+ }
103
+
104
+ homeRoutes.get("/", renderHomePage);