@jant/core 0.6.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -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);