@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
@@ -6,7 +6,9 @@
6
6
 
7
7
  import type { Context } from "hono";
8
8
  import type { Collection, NavItem, NavItemView } from "../types.js";
9
+ import { isFeedNavKey } from "../types/constants.js";
9
10
  import { toNavItemViews } from "./view.js";
11
+ import { languageScopeBasePath, viewBasePath } from "./view-language.js";
10
12
  import { render as renderMarkdown, toPlainText } from "./markdown.js";
11
13
 
12
14
  /**
@@ -15,7 +17,15 @@ import { render as renderMarkdown, toPlainText } from "./markdown.js";
15
17
  export interface NavigationData {
16
18
  links: NavItemView[];
17
19
  currentPath: string;
20
+ /** Deployment path prefix, such as `/blog`. */
18
21
  sitePathPrefix: string;
22
+ /**
23
+ * Public path prefix for reader-facing surfaces. Equals `sitePathPrefix`
24
+ * outside a language view, and carries the language prefix inside one
25
+ * (`/blog/en`). Pass this — not `sitePathPrefix` — to anything building
26
+ * links to the home page, archive, search, feeds or collections.
27
+ */
28
+ basePath: string;
19
29
  siteName: string;
20
30
  /** Plain-text description for meta tags and RSS/Atom feeds */
21
31
  siteDescription: string;
@@ -23,11 +33,37 @@ export interface NavigationData {
23
33
  siteDescriptionHtml?: string;
24
34
  isAuthenticated: boolean;
25
35
  collections: Collection[];
26
- siteAvatarUrl?: string;
36
+ /** Header/drawer mark, already sized down — not the canonical asset. */
37
+ siteAvatarThumbUrl?: string;
27
38
  showHeaderAvatar?: boolean;
28
39
  siteFooterHtml?: string;
29
40
  }
30
41
 
42
+ /**
43
+ * Collect the collections and smart collections already placed in navigation.
44
+ *
45
+ * The directory offers both kinds the same "Add to Navigation" action, so it
46
+ * needs one list to check against; TypeID prefixes (`col_`, `smc_`) keep the
47
+ * two apart without a second field.
48
+ *
49
+ * @param links - Navigation items as rendered for this request
50
+ * @returns TypeIDs of every collection and smart collection in navigation
51
+ * @example
52
+ * const placed = collectNavigationCollectionIds(navData.links);
53
+ * // ["col_01abc", "smc_01xyz"]
54
+ */
55
+ export function collectNavigationCollectionIds(links: NavItemView[]): string[] {
56
+ return links.flatMap((item) => {
57
+ if (item.type === "collection" && item.collectionId) {
58
+ return [item.collectionId];
59
+ }
60
+ if (item.type === "smart_collection" && item.smartCollectionId) {
61
+ return [item.smartCollectionId];
62
+ }
63
+ return [];
64
+ });
65
+ }
66
+
31
67
  /**
32
68
  * Fetch navigation data for public pages.
33
69
  *
@@ -49,20 +85,45 @@ export interface NavigationData {
49
85
  */
50
86
  export async function getNavigationData(
51
87
  c: Context,
52
- options?: { preloadedItems?: NavItem[]; includeCollections?: boolean },
88
+ options?: {
89
+ preloadedItems?: NavItem[];
90
+ includeCollections?: boolean;
91
+ /**
92
+ * Content language whose view the page's chrome should live in. Post
93
+ * pages pass their post's language here: their URLs carry no language
94
+ * prefix, so `viewLang` is never set on them, yet a Japanese post's
95
+ * logo and nav should lead to `/ja`, not to the primary view.
96
+ */
97
+ languageScope?: string | null;
98
+ },
53
99
  ): Promise<NavigationData> {
100
+ const appConfig = c.var.appConfig;
101
+ const langBase =
102
+ options?.languageScope !== undefined
103
+ ? languageScopeBasePath(c, options.languageScope)
104
+ : viewBasePath(c);
105
+ // An empty prefix means the primary language's view, whose nav items already
106
+ // point where they should — so the whole primary-language site, multilingual
107
+ // or not, never pays for the translation lookup.
108
+ const scopeLanguage = langBase
109
+ ? (options?.languageScope ?? c.var.viewLang ?? null)
110
+ : null;
111
+
54
112
  // Callers that already fetched nav items can pass them in to avoid a
55
113
  // redundant DB round-trip.
56
114
  const savedItems =
57
- options?.preloadedItems ?? (await c.var.services.navItems.list());
115
+ options?.preloadedItems ??
116
+ (await c.var.services.navItems.list({ language: scopeLanguage }));
58
117
  const currentPath = c.var.publicPath;
59
- const appConfig = c.var.appConfig;
60
- // Keep the saved RSS item untouched so its placement and custom label come
61
- // back when feeds are re-enabled. Only the rendered projection is filtered.
118
+ // Keep the saved feed items untouched so their placement and custom labels
119
+ // come back when feeds are re-enabled. Only the rendered projection is
120
+ // filtered. Both entries go: `rss` addresses a feed that is off, and
121
+ // `subscribe` a page that 404s without one.
62
122
  const items = appConfig.rssFeedsEnabled
63
123
  ? savedItems
64
124
  : savedItems.filter(
65
- (item: NavItem) => item.type !== "system" || item.systemKey !== "rss",
125
+ (item: NavItem) =>
126
+ item.type !== "system" || !isFeedNavKey(item.systemKey),
66
127
  );
67
128
 
68
129
  const siteName = appConfig.siteName;
@@ -80,8 +141,10 @@ export async function getNavigationData(
80
141
  })
81
142
  : undefined;
82
143
 
83
- // Avatar URL and display flag come from appConfig
84
- const siteAvatarUrl = appConfig.siteAvatarUrl || undefined;
144
+ // Avatar URL and display flag come from appConfig. The chrome takes the
145
+ // sized variant: the full-resolution asset belongs to the social card and
146
+ // the settings editor, not to a 32px mark on every page.
147
+ const siteAvatarThumbUrl = appConfig.siteAvatarThumbUrl || undefined;
85
148
  const showHeaderAvatar = appConfig.showHeaderAvatar;
86
149
 
87
150
  // Render footer markdown
@@ -107,6 +170,8 @@ export async function getNavigationData(
107
170
  ? await c.var.services.navItems.getCollectionFreshness(collectionNavIds)
108
171
  : undefined;
109
172
 
173
+ const basePath = `${appConfig.sitePathPrefix}${langBase}`;
174
+
110
175
  const links = toNavItemViews(
111
176
  items,
112
177
  currentPath,
@@ -114,6 +179,7 @@ export async function getNavigationData(
114
179
  appConfig.sitePathPrefix,
115
180
  collectionFreshness,
116
181
  appConfig.siteOrigin,
182
+ basePath,
117
183
  );
118
184
 
119
185
  // Only load collections when authenticated (for compose dialog)
@@ -125,13 +191,14 @@ export async function getNavigationData(
125
191
  links,
126
192
  currentPath,
127
193
  sitePathPrefix: appConfig.sitePathPrefix,
194
+ basePath,
128
195
  siteName,
129
196
  siteDescription,
130
197
  siteDescriptionHtml,
131
198
  isAuthenticated,
132
199
  collections,
133
- siteAvatarUrl,
134
- showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
200
+ siteAvatarThumbUrl,
201
+ showHeaderAvatar: showHeaderAvatar && !!siteAvatarThumbUrl,
135
202
  siteFooterHtml,
136
203
  };
137
204
  }
@@ -38,10 +38,18 @@ export function formatPageLabel(
38
38
  return `Page ${String(currentPage)}`;
39
39
  }
40
40
 
41
+ /** Slots a numbered pagination control fills once there are more pages. */
42
+ const PAGE_SLOT_COUNT = 7;
43
+
41
44
  /**
42
45
  * Computes which page numbers to display in a numbered pagination control.
43
- * Always includes: first page, last page, current page, and 1 page on each side of current.
44
- * Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
46
+ *
47
+ * Seven or fewer pages are all shown. Past that, the control always fills
48
+ * exactly seven slots — the first page, the last page, and a three-page
49
+ * window around the current page — so it keeps the same width, and the
50
+ * Previous/Next links stay put, as the reader moves through pages. Skipped
51
+ * pages are represented by 0 (ellipsis marker). An ellipsis always stands in
52
+ * for at least two pages; where it would hide just one, that page is shown.
45
53
  *
46
54
  * @param currentPage - The current active page (1-indexed)
47
55
  * @param totalPages - Total number of pages
@@ -50,36 +58,32 @@ export function formatPageLabel(
50
58
  * @example
51
59
  * ```ts
52
60
  * getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
53
- * getPageNumbers(1, 20) // [1, 2, 0, 20]
54
- * getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
61
+ * getPageNumbers(1, 19) // [1, 2, 3, 4, 5, 0, 19]
62
+ * getPageNumbers(10, 19) // [1, 0, 9, 10, 11, 0, 19]
63
+ * getPageNumbers(19, 19) // [1, 0, 15, 16, 17, 18, 19]
55
64
  * ```
56
65
  */
57
66
  export function getPageNumbers(
58
67
  currentPage: number,
59
68
  totalPages: number,
60
69
  ): number[] {
61
- if (totalPages <= 7) {
70
+ if (totalPages <= PAGE_SLOT_COUNT) {
62
71
  return Array.from({ length: totalPages }, (_, i) => i + 1);
63
72
  }
64
73
 
65
- const pages = new Set<number>();
66
- pages.add(1);
67
- pages.add(totalPages);
68
- pages.add(currentPage);
69
- if (currentPage > 1) pages.add(currentPage - 1);
70
- if (currentPage < totalPages) pages.add(currentPage + 1);
71
-
72
- const sorted = [...pages].sort((a, b) => a - b);
73
-
74
- // Insert 0 for gaps
75
- const result: number[] = [];
76
- for (let i = 0; i < sorted.length; i++) {
77
- const current = sorted[i] as number;
78
- if (i > 0 && current - (sorted[i - 1] as number) > 1) {
79
- result.push(0); // ellipsis marker
80
- }
81
- result.push(current);
82
- }
74
+ // The second slot holds page 2 or an ellipsis, and the second-to-last slot
75
+ // holds page totalPages - 1 or an ellipsis, so the window around the current
76
+ // page starts no earlier than page 3 and ends no later than totalPages - 2.
77
+ const windowStart = Math.min(Math.max(currentPage - 1, 3), totalPages - 4);
78
+ const windowEnd = windowStart + 2;
83
79
 
84
- return result;
80
+ return [
81
+ 1,
82
+ windowStart === 3 ? 2 : 0,
83
+ windowStart,
84
+ windowStart + 1,
85
+ windowEnd,
86
+ windowEnd === totalPages - 2 ? totalPages - 1 : 0,
87
+ totalPages,
88
+ ];
85
89
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Which reader surfaces exist once per language.
3
+ *
4
+ * Mirrors the `langGet()` table in `routes/pages/language.tsx` — the two must
5
+ * change together. Kept in its own module, free of server imports, because the
6
+ * client needs the same answer: `viewPath()` uses it to decide whether a link
7
+ * carries the current view's language prefix, and getting it wrong in either
8
+ * direction is a bug you can see — a missing prefix drops the reader into
9
+ * another language, an extra one lands on a 404.
10
+ *
11
+ * Everything not listed here — the dashboard, auth, collection editors, and
12
+ * post permalinks — has one address site-wide.
13
+ */
14
+
15
+ /** Surfaces served per language at exactly this path. */
16
+ const PER_LANGUAGE_SURFACES = new Set([
17
+ "/",
18
+ "/feed",
19
+ "/latest",
20
+ "/featured",
21
+ "/archive",
22
+ "/search",
23
+ "/subscribe",
24
+ "/collections",
25
+ ]);
26
+
27
+ /**
28
+ * Surfaces whose sub-paths are all per-language too. `/collections/` is absent
29
+ * on purpose: aggregate selections and their feeds are, but nothing else
30
+ * nested under it is.
31
+ */
32
+ const PER_LANGUAGE_SURFACE_PREFIXES = ["/latest/", "/featured/", "/archive/"];
33
+
34
+ const COLLECTIONS_PREFIX = "/collections/";
35
+
36
+ /** `/collections/{selection}` and its feed, and nothing else under it. */
37
+ function isCollectionSelectionSurface(path: string): boolean {
38
+ const rest = path.slice(COLLECTIONS_PREFIX.length);
39
+ if (!rest) return false;
40
+
41
+ const segments = rest.split("/");
42
+ if (segments[0] === "") return false;
43
+ if (segments.length === 1) return true;
44
+ return segments.length === 2 && segments[1] === "feed";
45
+ }
46
+
47
+ /**
48
+ * Whether a path exists in every language's view.
49
+ *
50
+ * @param path - Internal app path, without any language prefix. A query string
51
+ * or fragment is ignored, so callers can pass a built link as-is.
52
+ * @returns True when `/ja{path}` is a page rather than a 404
53
+ * @example
54
+ * isPerLanguageSurface("/archive?media=any"); // true — /ja/archive exists
55
+ * isPerLanguageSurface("/collections/a+b/feed"); // true — /ja/collections/a+b/feed exists
56
+ * isPerLanguageSurface("/settings/language"); // false — the dash is one place
57
+ */
58
+ export function isPerLanguageSurface(path: string): boolean {
59
+ const bare = path.split(/[?#]/, 1)[0] ?? path;
60
+ if (PER_LANGUAGE_SURFACES.has(bare)) return true;
61
+ if (bare.startsWith(COLLECTIONS_PREFIX)) {
62
+ return isCollectionSelectionSurface(bare);
63
+ }
64
+ return PER_LANGUAGE_SURFACE_PREFIXES.some((prefix) =>
65
+ bare.startsWith(prefix),
66
+ );
67
+ }
@@ -97,6 +97,25 @@ function getDescriptionCandidate(post: Post): string {
97
97
  return "";
98
98
  }
99
99
 
100
+ /**
101
+ * What to call a Post in a list, when it may have no title of its own.
102
+ *
103
+ * Notes are usually untitled, so falling back to the slug would show readers
104
+ * and authors a URL fragment where they expect a sentence. The chain is the
105
+ * same one browser titles and Open Graph tags already use: title, then the
106
+ * quoted text, then the summary, then the opening of the body, then a link's
107
+ * domain.
108
+ *
109
+ * @param post - The Post to name
110
+ * @returns A short plain-text label, or an empty string when there is nothing
111
+ * to derive one from
112
+ * @example
113
+ * getPostDisplayTitle(untitledNote); // "An untitled note about espresso."
114
+ */
115
+ export function getPostDisplayTitle(post: Post): string {
116
+ return getTitleCandidate(post);
117
+ }
118
+
100
119
  export interface PostMeta {
101
120
  title: string;
102
121
  description?: string;
@@ -9,9 +9,20 @@ import type { Context } from "hono";
9
9
  import type { Child } from "hono/jsx";
10
10
  import type { SiteLayoutProps } from "../types.js";
11
11
  import { SETTINGS_KEYS } from "./constants.js";
12
- import { BaseLayout, type ToastProps } from "../ui/layouts/BaseLayout.js";
12
+ import {
13
+ BaseLayout,
14
+ type PageFeedLink,
15
+ type ToastProps,
16
+ } from "../ui/layouts/BaseLayout.js";
13
17
  import { SiteLayout } from "../ui/layouts/SiteLayout.js";
14
18
  import type { NavigationData } from "./navigation.js";
19
+ import {
20
+ buildComposeLanguages,
21
+ buildLanguageSwitcher,
22
+ getViewLang,
23
+ type LanguageAlternate,
24
+ type LanguageSwitcherOption,
25
+ } from "./view-language.js";
15
26
 
16
27
  export interface RenderPublicPageOptions {
17
28
  /** Page title for <title> tag */
@@ -45,6 +56,23 @@ export interface RenderPublicPageOptions {
45
56
  * thread root).
46
57
  */
47
58
  canonicalHref?: string;
59
+ /**
60
+ * `hreflang` alternates for this page. List surfaces pass
61
+ * `buildSurfaceAlternates(c)`; a post passes its translations. Pages that
62
+ * exist once for the whole site pass nothing.
63
+ */
64
+ alternateLanguages?: LanguageAlternate[];
65
+ /**
66
+ * The feed this page publishes, for `<link rel="alternate">` autodiscovery.
67
+ * Set on pages with a feed of their own — a collection, the archive — so a
68
+ * reader's extension finds that feed rather than only the site-wide ones.
69
+ */
70
+ pageFeed?: PageFeedLink;
71
+ /**
72
+ * Language switcher entries. Defaults to the current surface in each of the
73
+ * site's languages; a post overrides it with its translations.
74
+ */
75
+ languageSwitcher?: LanguageSwitcherOption[];
48
76
  /** Navigation data (from getNavigationData) */
49
77
  navData: NavigationData;
50
78
  /** Page content JSX to render inside SiteLayout */
@@ -57,14 +85,24 @@ export interface RenderPublicPageOptions {
57
85
  toast?: ToastProps;
58
86
  /** Whether to render the shared compose dialog shell */
59
87
  showComposeDialog?: boolean;
60
- /** Override the site-wide crawler setting for sensitive utility pages. */
61
- noindex?: boolean;
88
+ /**
89
+ * Override the site-wide crawler setting for this page. `true` means
90
+ * `noindex, nofollow`; `"follow"` keeps the URL unindexed while leaving its
91
+ * links crawlable. See `BaseLayoutProps["noindex"]`.
92
+ */
93
+ noindex?: boolean | "follow";
62
94
  /** Whether to render the site header */
63
95
  showHeader?: boolean;
64
96
  /** Whether to render the home branding credit after the site footer */
65
97
  showHomeBranding?: boolean;
66
98
  /** When set, the mobile compose FAB pre-selects this collection. */
67
99
  composeCollectionId?: string;
100
+ /**
101
+ * Content language of the page the composer opens from, for its default.
102
+ * List surfaces leave this unset and get the view language; a post page
103
+ * passes its post's language, since its URL carries no view prefix.
104
+ */
105
+ composeContextLanguage?: string | null;
68
106
  }
69
107
 
70
108
  /**
@@ -99,6 +137,9 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
99
137
  articleModifiedTime,
100
138
  jsonLd,
101
139
  canonicalHref,
140
+ alternateLanguages,
141
+ pageFeed,
142
+ languageSwitcher,
102
143
  navData,
103
144
  content,
104
145
  pageChrome,
@@ -109,6 +150,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
109
150
  showHeader,
110
151
  showHomeBranding,
111
152
  composeCollectionId,
153
+ composeContextLanguage,
112
154
  } = options;
113
155
 
114
156
  // Use siteDescription as meta description fallback when not explicitly provided
@@ -123,9 +165,10 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
123
165
  links: navData.links,
124
166
  currentPath: navData.currentPath,
125
167
  sitePathPrefix: navData.sitePathPrefix,
168
+ basePath: navData.basePath,
126
169
  isAuthenticated: navData.isAuthenticated,
127
170
  collections: navData.collections,
128
- siteAvatarUrl: navData.siteAvatarUrl,
171
+ siteAvatarThumbUrl: navData.siteAvatarThumbUrl,
129
172
  showHeaderAvatar: navData.showHeaderAvatar,
130
173
  siteDescriptionHtml: navData.siteDescriptionHtml,
131
174
  siteFooterHtml: navData.siteFooterHtml,
@@ -141,11 +184,14 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
141
184
  allSettings[SETTINGS_KEYS.DISCOVERY_SLASH_COMMAND_AT],
142
185
  ),
143
186
  composeCollectionId,
187
+ languageSwitcher: languageSwitcher ?? buildLanguageSwitcher(c),
188
+ composeLanguages: buildComposeLanguages(c),
189
+ composeContextLanguage: composeContextLanguage ?? getViewLang(c),
144
190
  };
145
191
  const faviconUrl = appConfig.siteAvatarUrl || undefined;
146
192
  const faviconVersion = appConfig.faviconVersion || undefined;
147
- const resolvedNoindex = noindex ?? appConfig.noindex;
148
-
193
+ // `noindex` is passed straight through: BaseLayout reads the site-wide
194
+ // setting from this same context and is the single place the two resolve.
149
195
  return c.html(
150
196
  <BaseLayout
151
197
  title={title}
@@ -162,9 +208,11 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
162
208
  articleModifiedTime={articleModifiedTime}
163
209
  jsonLd={jsonLd}
164
210
  canonicalHref={canonicalHref}
211
+ alternateLanguages={alternateLanguages}
212
+ pageFeed={pageFeed}
165
213
  faviconUrl={faviconUrl}
166
214
  faviconVersion={faviconVersion}
167
- noindex={resolvedNoindex}
215
+ noindex={noindex}
168
216
  isAuthenticated={navData.isAuthenticated}
169
217
  toast={toast}
170
218
  >
@@ -12,15 +12,18 @@
12
12
  import type { Bindings } from "../types/bindings.js";
13
13
  import type { AppConfig } from "../types/config.js";
14
14
  import { CONFIG_FIELDS } from "../types/config.js";
15
- import type { FeedKind } from "../types/constants.js";
15
+ import type { ArchiveLayout, FeedKind } from "../types/constants.js";
16
16
  import { ASSET_BASE_SEGMENT, getPublicAssetBasePath } from "./asset-path.js";
17
+ import { resolveDiscoverMode } from "./discover.js";
17
18
  import {
18
19
  getAuthSecret,
19
20
  getConfiguredSingleSiteUrl,
20
21
  getConfiguredStorageDriver,
22
+ getDiscoverDefault,
21
23
  getEnvString,
22
24
  } from "./env.js";
23
- import { getPublicUrlForProvider, getMediaUrl } from "./image.js";
25
+ import { parseLanguageList } from "../i18n/locales.js";
26
+ import { getPublicUrlForProvider, getMediaUrl, getImageUrl } from "./image.js";
24
27
  import { normalizeTimeZone } from "./timezones.js";
25
28
  import { getSiteOrigin, getSitePathPrefix, normalizeSiteUrl } from "./url.js";
26
29
 
@@ -147,6 +150,13 @@ function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
147
150
  return value === "latest" || value === "featured" ? value : fallback;
148
151
  }
149
152
 
153
+ function parseArchiveLayout(
154
+ value: string,
155
+ fallback: ArchiveLayout,
156
+ ): ArchiveLayout {
157
+ return value === "list" || value === "grid" ? value : fallback;
158
+ }
159
+
150
160
  /**
151
161
  * Build a complete AppConfig from environment bindings and DB settings.
152
162
  *
@@ -198,6 +208,7 @@ export function resolveConfig(
198
208
  // Resolve avatar URL from storage key
199
209
  const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
200
210
  let siteAvatarUrl = "";
211
+ let siteAvatarThumbUrl = "";
201
212
  if (siteAvatar) {
202
213
  const publicUrl = getPublicUrlForProvider(
203
214
  storageDriver,
@@ -206,6 +217,17 @@ export function resolveConfig(
206
217
  localPublicUrl,
207
218
  );
208
219
  siteAvatarUrl = getMediaUrl(siteAvatar, publicUrl, sitePathPrefix);
220
+ // The header and drawer marks render this at 32px and 24px, on every page,
221
+ // in parallel with the render-blocking stylesheet. An untransformed avatar
222
+ // is whatever the author uploaded — commonly a 512px PNG — so it competes
223
+ // for the critical path with tens of times the bytes it can use. 128px
224
+ // still covers a high-DPR header and a theme that enlarges the mark.
225
+ siteAvatarThumbUrl = getImageUrl(siteAvatarUrl, imageTransformUrl, {
226
+ width: 128,
227
+ quality: 80,
228
+ format: "auto",
229
+ fit: "scale-down",
230
+ });
209
231
  }
210
232
 
211
233
  // Description is "explicit" when set in DB or ENV (not just the default)
@@ -216,6 +238,16 @@ export function resolveConfig(
216
238
  ? !!dbDescription
217
239
  : !!envDescription;
218
240
 
241
+ // The owner's stored choice and the deployment's default stay apart, and
242
+ // the registry default must not stand in for either: an absent row is what
243
+ // lets `noindex` decide, and an absent binding is what makes Discover
244
+ // opt-in for a self-hosted site.
245
+ const discoverStoredValue = allSettings["DISCOVER"];
246
+ const discoverDefaultValue = getDiscoverDefault(env);
247
+ const noindex = demoMode || resolve("NOINDEX", allSettings, env) === "true";
248
+ const rssFeedsEnabled =
249
+ resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true";
250
+
219
251
  return {
220
252
  // Site identity (DB > ENV > Default)
221
253
  siteName: resolve("SITE_NAME", allSettings, env),
@@ -223,19 +255,34 @@ export function resolveConfig(
223
255
  siteDescriptionExplicit,
224
256
  siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
225
257
  dashboardLanguage: resolve("DASHBOARD_LANGUAGE", allSettings, env),
226
- cjkSerifFont: resolve("CJK_SERIF_FONT", allSettings, env),
258
+ multilingualEnabled:
259
+ resolve("MULTILINGUAL_ENABLED", allSettings, env) === "true",
260
+ additionalLanguages: parseLanguageList(
261
+ resolve("ADDITIONAL_LANGUAGES", allSettings, env),
262
+ ),
227
263
  mainRssFeed: parseFeedKind(
228
264
  resolve("MAIN_RSS_FEED", allSettings, env),
229
265
  "featured",
230
266
  ),
267
+ archiveDefaultLayout: parseArchiveLayout(
268
+ resolve("ARCHIVE_DEFAULT_LAYOUT", allSettings, env),
269
+ "list",
270
+ ),
231
271
  timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
232
272
  siteFooter: resolve("SITE_FOOTER", allSettings, env),
233
273
  showJantBrandingOnHome:
234
274
  resolve("SHOW_JANT_BRANDING_ON_HOME", allSettings, env) === "true",
235
- noindex: demoMode || resolve("NOINDEX", allSettings, env) === "true",
275
+ noindex,
276
+ discover: resolveDiscoverMode({
277
+ storedValue: discoverStoredValue,
278
+ defaultValue: discoverDefaultValue,
279
+ demoMode,
280
+ noindex,
281
+ rssFeedsEnabled,
282
+ }),
236
283
  publicApiEnabled:
237
284
  resolve("PUBLIC_API_ENABLED", allSettings, env) === "true",
238
- rssFeedsEnabled: resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true",
285
+ rssFeedsEnabled,
239
286
 
240
287
  // Infrastructure (ENV only)
241
288
  siteUrl,
@@ -318,6 +365,7 @@ export function resolveConfig(
318
365
  siteAvatar,
319
366
  showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
320
367
  siteAvatarUrl,
368
+ siteAvatarThumbUrl,
321
369
  faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
322
370
 
323
371
  // Rate limiting (ENV only). Defaults are conservative enough for a