@jant/core 0.6.16 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/export.js +3 -1
  4. package/bin/commands/import-site.js +749 -220
  5. package/bin/commands/reset-password.js +21 -8
  6. package/bin/commands/setup.js +136 -0
  7. package/bin/commands/site/export.js +71 -34
  8. package/bin/commands/site/pull-media.js +2 -6
  9. package/bin/commands/site/snapshot/export.js +19 -61
  10. package/bin/commands/site/snapshot/import.js +23 -24
  11. package/bin/lib/d1-query.js +92 -18
  12. package/bin/lib/hugo-markdown.js +4 -0
  13. package/bin/lib/node-env.js +40 -4
  14. package/bin/lib/site-pull-media.js +21 -28
  15. package/bin/lib/site-selection.js +94 -29
  16. package/bin/lib/site-snapshot.js +338 -3
  17. package/bin/lib/sql-export.js +68 -5
  18. package/bin/lib/wrangler-cli.js +37 -2
  19. package/bin/lib/wrangler-json.js +35 -0
  20. package/bin/lib/zip-archive.js +187 -0
  21. package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
  22. package/dist/client/.vite/manifest.json +255 -90
  23. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  24. package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
  25. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  26. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  27. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  28. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  29. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  30. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  31. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  32. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  33. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  34. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  35. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  36. package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
  37. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  38. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  39. package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
  40. package/dist/client/_assets/client-DAhoqPdr.css +2 -0
  41. package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
  42. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  43. package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
  44. package/dist/client/_assets/client-knSEyUJO.js +295 -0
  45. package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
  46. package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
  47. package/dist/github-sync-Gw4orAHk.js +8398 -0
  48. package/dist/index.js +2 -5
  49. package/dist/node.js +11 -9
  50. package/package.json +15 -5
  51. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  52. package/src/__tests__/basecoat-variants.test.ts +121 -0
  53. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  54. package/src/__tests__/dev-scripts.test.ts +203 -0
  55. package/src/__tests__/export-collection-order.test.ts +276 -0
  56. package/src/__tests__/export-feed-ids.test.ts +184 -0
  57. package/src/__tests__/export-feed-order.test.ts +294 -0
  58. package/src/__tests__/export-hugo-build.test.ts +390 -0
  59. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  60. package/src/__tests__/export-service.test.ts +842 -28
  61. package/src/__tests__/export-smart-collection.test.ts +329 -0
  62. package/src/__tests__/helpers/component-css.ts +91 -0
  63. package/src/__tests__/helpers/db.ts +33 -0
  64. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  65. package/src/__tests__/helpers/hugo-site.ts +146 -0
  66. package/src/__tests__/import-site-command.test.ts +487 -1
  67. package/src/__tests__/mise-config.test.ts +75 -4
  68. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  69. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  70. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  71. package/src/__tests__/snapshot-settings.test.ts +97 -0
  72. package/src/__tests__/snapshot-tables.test.ts +219 -0
  73. package/src/__tests__/sql-export.test.ts +173 -0
  74. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  75. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  76. package/src/__tests__/zip-archive.test.ts +106 -0
  77. package/src/app.tsx +86 -22
  78. package/src/auth.ts +18 -1
  79. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  80. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  81. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  82. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  83. package/src/client/__tests__/compose-launch.test.ts +62 -1
  84. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  85. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  86. package/src/client/__tests__/copy-field.test.ts +162 -0
  87. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  88. package/src/client/__tests__/image-processor.test.ts +324 -2
  89. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  90. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  91. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  92. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  93. package/src/client/__tests__/toast.test.ts +23 -0
  94. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  95. package/src/client/__tests__/video-processor.test.ts +181 -0
  96. package/src/client/audio-processor.ts +24 -15
  97. package/src/client/audio-waveform.ts +49 -0
  98. package/src/client/collection-dialog-host.ts +72 -0
  99. package/src/client/collection-navigation.ts +51 -16
  100. package/src/client/collection-page-actions.ts +26 -0
  101. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  102. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  103. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  104. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  105. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  106. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  107. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  108. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  109. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  110. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  111. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  112. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  113. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  114. package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
  115. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  116. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  117. package/src/client/components/collection-dialog-types.ts +49 -0
  118. package/src/client/components/collection-manager-types.ts +32 -1
  119. package/src/client/components/collection-types.ts +1 -15
  120. package/src/client/components/compose-types.ts +75 -1
  121. package/src/client/components/jant-collection-dialog.ts +722 -0
  122. package/src/client/components/jant-collection-directory.ts +567 -182
  123. package/src/client/components/jant-collection-form.ts +53 -232
  124. package/src/client/components/jant-command-palette.ts +100 -49
  125. package/src/client/components/jant-compose-dialog.ts +1486 -707
  126. package/src/client/components/jant-compose-editor.ts +673 -336
  127. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  128. package/src/client/components/jant-config-editor.ts +65 -53
  129. package/src/client/components/jant-confirm-dialog.ts +13 -11
  130. package/src/client/components/jant-locale-picker.ts +418 -0
  131. package/src/client/components/jant-media-lightbox.ts +146 -136
  132. package/src/client/components/jant-nav-manager.ts +690 -366
  133. package/src/client/components/jant-post-menu.ts +1086 -282
  134. package/src/client/components/jant-post-picker.ts +324 -0
  135. package/src/client/components/jant-repo-picker-types.ts +6 -1
  136. package/src/client/components/jant-repo-picker.ts +137 -108
  137. package/src/client/components/jant-settings-avatar.ts +31 -27
  138. package/src/client/components/jant-settings-general.ts +622 -543
  139. package/src/client/components/jant-settings-language.ts +1087 -0
  140. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  141. package/src/client/components/jant-text-preview.ts +94 -86
  142. package/src/client/components/nav-manager-types.ts +62 -1
  143. package/src/client/components/settings-types.ts +14 -29
  144. package/src/client/components/smart-collection-conditions.ts +285 -0
  145. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  146. package/src/client/compose-bridge.ts +214 -141
  147. package/src/client/compose-launch.ts +76 -2
  148. package/src/client/compose-shortcuts.ts +6 -8
  149. package/src/client/compose-triggers.ts +50 -0
  150. package/src/client/copy-field.ts +108 -0
  151. package/src/client/custom-url-menu.ts +60 -4
  152. package/src/client/icons.ts +14 -0
  153. package/src/client/image-plan.ts +165 -0
  154. package/src/client/image-processor.ts +195 -117
  155. package/src/client/image-worker-client.ts +45 -0
  156. package/src/client/image-worker.ts +135 -0
  157. package/src/client/lazy-entries.ts +95 -0
  158. package/src/client/lazy-slugify.ts +2 -2
  159. package/src/client/media-scroll-hint.ts +43 -2
  160. package/src/client/mediabunny.ts +29 -0
  161. package/src/client/post-picker.ts +41 -0
  162. package/src/client/runtime-paths.ts +74 -1
  163. package/src/client/settings-bridge.ts +4 -10
  164. package/src/client/site-header-nav.js +18 -6
  165. package/src/client/smart-collection-dialog-host.ts +116 -0
  166. package/src/client/smart-collection-page-actions.ts +208 -0
  167. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  168. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  169. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  170. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  171. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  172. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  173. package/src/client/tiptap/bubble-menu.ts +47 -16
  174. package/src/client/tiptap/extensions.ts +0 -3
  175. package/src/client/tiptap/floating-position.ts +216 -2
  176. package/src/client/tiptap/inline-image-upload.ts +7 -3
  177. package/src/client/tiptap/link-toolbar.ts +22 -9
  178. package/src/client/tiptap/structural-keymap.ts +158 -47
  179. package/src/client/toast.ts +13 -0
  180. package/src/client/types/vite-worker.d.ts +12 -0
  181. package/src/client/upload-with-metadata.ts +3 -13
  182. package/src/client/video-processor.ts +351 -107
  183. package/src/client-auth.ts +14 -31
  184. package/src/client-compose.ts +19 -0
  185. package/src/client-manage.ts +13 -0
  186. package/src/client-settings.ts +14 -0
  187. package/src/client-site.ts +10 -3
  188. package/src/client.ts +4 -0
  189. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  190. package/src/db/__tests__/d1-query.test.ts +56 -1
  191. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  192. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  193. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  194. package/src/db/migrations/0032_married_tomas.sql +3 -0
  195. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  196. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  197. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  198. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  199. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  200. package/src/db/migrations/meta/_journal.json +21 -0
  201. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  202. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  203. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  204. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  205. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  206. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  207. package/src/db/migrations/pg/meta/_journal.json +21 -0
  208. package/src/db/pg/schema.ts +195 -27
  209. package/src/db/post-visibility.ts +117 -0
  210. package/src/db/schema.ts +222 -27
  211. package/src/i18n/__tests__/detect.test.ts +26 -81
  212. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  213. package/src/i18n/context.tsx +12 -2
  214. package/src/i18n/detect.ts +7 -66
  215. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  216. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  217. package/src/i18n/locales/public/en.po +490 -158
  218. package/src/i18n/locales/public/en.ts +1 -1
  219. package/src/i18n/locales/public/zh-Hans.po +487 -155
  220. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  221. package/src/i18n/locales/public/zh-Hant.po +487 -155
  222. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  223. package/src/i18n/locales/settings/en.po +555 -73
  224. package/src/i18n/locales/settings/en.ts +1 -1
  225. package/src/i18n/locales/settings/zh-Hans.po +601 -119
  226. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  227. package/src/i18n/locales/settings/zh-Hant.po +614 -132
  228. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  229. package/src/i18n/locales.ts +94 -0
  230. package/src/i18n/middleware.ts +12 -2
  231. package/src/i18n/supported-locales.ts +80 -0
  232. package/src/lib/__tests__/api-settings.test.ts +11 -2
  233. package/src/lib/__tests__/asset-path.test.ts +92 -0
  234. package/src/lib/__tests__/deferred.test.ts +90 -0
  235. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  236. package/src/lib/__tests__/discover.test.ts +410 -0
  237. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  238. package/src/lib/__tests__/feed.test.ts +1526 -15
  239. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  240. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  241. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  242. package/src/lib/__tests__/http-cache.test.ts +162 -0
  243. package/src/lib/__tests__/image.test.ts +27 -1
  244. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  245. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  246. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  247. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  248. package/src/lib/__tests__/markdown.test.ts +10 -0
  249. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  250. package/src/lib/__tests__/navigation.test.ts +135 -1
  251. package/src/lib/__tests__/resolve-config.test.ts +131 -3
  252. package/src/lib/__tests__/schemas.test.ts +69 -5
  253. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  254. package/src/lib/__tests__/slug.test.ts +70 -0
  255. package/src/lib/__tests__/slugify.test.ts +85 -0
  256. package/src/lib/__tests__/storage.test.ts +32 -0
  257. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  258. package/src/lib/__tests__/timeline.test.ts +87 -0
  259. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  260. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  261. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  262. package/src/lib/__tests__/url.test.ts +78 -72
  263. package/src/lib/__tests__/view-language.test.ts +413 -0
  264. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  265. package/src/lib/address.ts +57 -0
  266. package/src/lib/api-posts.ts +12 -0
  267. package/src/lib/asset-path.ts +98 -1
  268. package/src/lib/build-env.ts +16 -0
  269. package/src/lib/collection-groups.ts +5 -1
  270. package/src/lib/collection-paths.ts +3 -10
  271. package/src/lib/constants.ts +28 -3
  272. package/src/lib/copy-field.ts +36 -0
  273. package/src/lib/crypto.ts +40 -0
  274. package/src/lib/csp-builder.ts +2 -1
  275. package/src/lib/deferred.ts +79 -0
  276. package/src/lib/discover-ping.ts +141 -0
  277. package/src/lib/discover.ts +439 -0
  278. package/src/lib/env.ts +134 -0
  279. package/src/lib/errors.ts +33 -0
  280. package/src/lib/feed-policy.ts +236 -2
  281. package/src/lib/feed.ts +926 -73
  282. package/src/lib/filter-dimensions.ts +1349 -0
  283. package/src/lib/github-sync-repo-name.ts +45 -0
  284. package/src/lib/github-sync-site-config.ts +3 -0
  285. package/src/lib/hosted-signin.ts +1 -1
  286. package/src/lib/http-cache.ts +148 -0
  287. package/src/lib/hugo-markdown.ts +62 -0
  288. package/src/lib/icons.ts +9 -2
  289. package/src/lib/ids.ts +3 -0
  290. package/src/lib/image.ts +17 -4
  291. package/src/lib/jant-docs.ts +42 -0
  292. package/src/lib/lang-detect.ts +308 -0
  293. package/src/lib/markdown-manager.ts +392 -2
  294. package/src/lib/media-helpers.ts +13 -1
  295. package/src/lib/navigation.ts +78 -11
  296. package/src/lib/pagination.ts +28 -24
  297. package/src/lib/per-language-surfaces.ts +67 -0
  298. package/src/lib/post-body-html.ts +11 -4
  299. package/src/lib/post-meta.ts +19 -0
  300. package/src/lib/render.tsx +55 -7
  301. package/src/lib/resolve-config.ts +111 -6
  302. package/src/lib/schemas.ts +251 -15
  303. package/src/lib/site-header-fragment.tsx +2 -1
  304. package/src/lib/site-unavailable-page.ts +47 -0
  305. package/src/lib/slug.ts +51 -7
  306. package/src/lib/slugify.ts +113 -0
  307. package/src/lib/startup-config.ts +4 -6
  308. package/src/lib/storage.ts +7 -0
  309. package/src/lib/summary.ts +50 -0
  310. package/src/lib/thread-fold.ts +161 -0
  311. package/src/lib/time.ts +32 -0
  312. package/src/lib/timeline.ts +43 -2
  313. package/src/lib/tiptap-render.ts +1 -2
  314. package/src/lib/tiptap-to-markdown.ts +13 -7
  315. package/src/lib/upload.ts +16 -13
  316. package/src/lib/url.ts +159 -86
  317. package/src/lib/version.ts +10 -8
  318. package/src/lib/view-language.ts +436 -0
  319. package/src/lib/view.ts +53 -25
  320. package/src/lib/viewer-context.ts +62 -0
  321. package/src/middleware/__tests__/auth.test.ts +70 -2
  322. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  323. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  324. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  325. package/src/middleware/__tests__/session.test.ts +181 -33
  326. package/src/middleware/auth.ts +50 -31
  327. package/src/middleware/cache-control.ts +75 -10
  328. package/src/middleware/config.ts +6 -6
  329. package/src/middleware/error-handler.ts +12 -0
  330. package/src/middleware/onboarding.ts +32 -10
  331. package/src/middleware/secure-headers.ts +1 -1
  332. package/src/middleware/session.ts +96 -8
  333. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  334. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  335. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  336. package/src/node/__tests__/cli-setup.test.ts +163 -0
  337. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  338. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  339. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  340. package/src/node/__tests__/runtime.test.ts +75 -0
  341. package/src/node/index.ts +3 -0
  342. package/src/node/request-handler.ts +3 -1
  343. package/src/preset.css +76 -22
  344. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  345. package/src/routes/api/__tests__/collections.test.ts +114 -0
  346. package/src/routes/api/__tests__/discover.test.ts +222 -0
  347. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  348. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  349. package/src/routes/api/__tests__/posts.test.ts +24 -0
  350. package/src/routes/api/__tests__/settings.test.ts +24 -3
  351. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  352. package/src/routes/api/__tests__/upload.test.ts +34 -0
  353. package/src/routes/api/collections.ts +49 -6
  354. package/src/routes/api/custom-urls.ts +0 -1
  355. package/src/routes/api/discover.ts +95 -0
  356. package/src/routes/api/export.ts +6 -3
  357. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  358. package/src/routes/api/internal/sites.ts +0 -1
  359. package/src/routes/api/nav-items.ts +55 -8
  360. package/src/routes/api/posts.ts +161 -0
  361. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  362. package/src/routes/api/public/archive.ts +77 -81
  363. package/src/routes/api/public/posts.ts +44 -7
  364. package/src/routes/api/smart-collections.ts +132 -0
  365. package/src/routes/api/telegram.ts +2 -40
  366. package/src/routes/api/upload.ts +10 -3
  367. package/src/routes/api/uploads.ts +1 -2
  368. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  369. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  370. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  371. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  372. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  373. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  374. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  375. package/src/routes/auth/setup.tsx +934 -186
  376. package/src/routes/auth/signin.tsx +10 -8
  377. package/src/routes/compose.tsx +9 -0
  378. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  379. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  380. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  381. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  382. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  383. package/src/routes/dash/custom-urls.tsx +1 -37
  384. package/src/routes/dash/settings.tsx +714 -111
  385. package/src/routes/discover-announce.ts +96 -0
  386. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  387. package/src/routes/feed/feed.ts +69 -40
  388. package/src/routes/feed/sitemap.ts +95 -11
  389. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  390. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  391. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  392. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  393. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  394. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  395. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  396. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  397. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  398. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  399. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  400. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  401. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  402. package/src/routes/pages/archive.tsx +487 -379
  403. package/src/routes/pages/collection.tsx +94 -96
  404. package/src/routes/pages/collections.tsx +38 -50
  405. package/src/routes/pages/featured.tsx +43 -18
  406. package/src/routes/pages/home.tsx +25 -6
  407. package/src/routes/pages/language.tsx +113 -0
  408. package/src/routes/pages/latest.tsx +26 -16
  409. package/src/routes/pages/new.tsx +3 -0
  410. package/src/routes/pages/page.tsx +228 -45
  411. package/src/routes/pages/partials.tsx +41 -0
  412. package/src/routes/pages/search.tsx +22 -5
  413. package/src/routes/pages/smart-collection.tsx +340 -0
  414. package/src/routes/pages/subscribe.tsx +182 -0
  415. package/src/runtime/__tests__/index.test.ts +19 -0
  416. package/src/runtime/__tests__/node.test.ts +129 -4
  417. package/src/runtime/__tests__/readiness.test.ts +23 -0
  418. package/src/runtime/cloudflare.ts +21 -2
  419. package/src/runtime/index.ts +27 -1
  420. package/src/runtime/node.ts +67 -1
  421. package/src/runtime/readiness.ts +15 -0
  422. package/src/runtime/site.ts +95 -24
  423. package/src/services/__tests__/auth.test.ts +116 -7
  424. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  425. package/src/services/__tests__/collection.test.ts +108 -4
  426. package/src/services/__tests__/custom-url.test.ts +34 -0
  427. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  428. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  429. package/src/services/__tests__/language.test.ts +495 -0
  430. package/src/services/__tests__/media.test.ts +31 -0
  431. package/src/services/__tests__/navigation.test.ts +213 -10
  432. package/src/services/__tests__/path.test.ts +236 -0
  433. package/src/services/__tests__/post-language.test.ts +830 -0
  434. package/src/services/__tests__/post-timeline.test.ts +127 -0
  435. package/src/services/__tests__/post.test.ts +120 -9
  436. package/src/services/__tests__/settings.test.ts +228 -17
  437. package/src/services/__tests__/site-admin.test.ts +5 -5
  438. package/src/services/__tests__/smart-collection.test.ts +651 -0
  439. package/src/services/about-page.ts +22 -1
  440. package/src/services/auth.ts +51 -49
  441. package/src/services/bootstrap.ts +408 -39
  442. package/src/services/collection-directory-position.ts +76 -0
  443. package/src/services/collection.ts +331 -46
  444. package/src/services/custom-url.ts +29 -24
  445. package/src/services/export-theme/assets/client-site.css +1 -1
  446. package/src/services/export-theme/assets/client-site.js +123 -123
  447. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  448. package/src/services/export-theme/layouts/_default/list.html +2 -63
  449. package/src/services/export-theme/layouts/_default/rss.xml +103 -38
  450. package/src/services/export-theme/layouts/collections/list.html +3 -3
  451. package/src/services/export-theme/layouts/featured/list.html +1 -4
  452. package/src/services/export-theme/layouts/index.html +40 -26
  453. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  454. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  455. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  456. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  457. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  458. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  459. package/src/services/export-theme/layouts/partials/head.html +1 -1
  460. package/src/services/export-theme/layouts/partials/header.html +5 -3
  461. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  462. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  463. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  464. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  465. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  466. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  467. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  468. package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
  469. package/src/services/export-theme/layouts/post/list.html +1 -1
  470. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  471. package/src/services/export-theme/styles/main.css +13 -2
  472. package/src/services/export-theme/theme.toml +1 -1
  473. package/src/services/export.ts +880 -72
  474. package/src/services/github-app-installations.ts +171 -4
  475. package/src/services/github-sync.ts +69 -15
  476. package/src/services/index.ts +34 -3
  477. package/src/services/language.ts +412 -0
  478. package/src/services/mcp.ts +22 -2
  479. package/src/services/media.ts +22 -7
  480. package/src/services/navigation.ts +429 -67
  481. package/src/services/path.ts +254 -6
  482. package/src/services/post.ts +1234 -190
  483. package/src/services/search.ts +39 -2
  484. package/src/services/settings.ts +238 -43
  485. package/src/services/site-admin.ts +27 -14
  486. package/src/services/site.ts +160 -32
  487. package/src/services/smart-collection.ts +636 -0
  488. package/src/services/telegram.ts +20 -22
  489. package/src/services/upload-session.ts +1 -3
  490. package/src/style-author.css +27 -0
  491. package/src/styles/components-author.css +1622 -0
  492. package/src/styles/components.css +24 -1571
  493. package/src/styles/site-media.css +16 -2
  494. package/src/styles/tokens.css +82 -12
  495. package/src/styles/ui-author.css +7602 -0
  496. package/src/styles/ui.css +1090 -7216
  497. package/src/types/app-context.ts +23 -0
  498. package/src/types/bindings.ts +8 -0
  499. package/src/types/config.ts +94 -15
  500. package/src/types/constants.ts +188 -16
  501. package/src/types/entities.ts +87 -1
  502. package/src/types/operations.ts +74 -2
  503. package/src/types/props.ts +157 -31
  504. package/src/types/views.ts +55 -1
  505. package/src/ui/__tests__/font-themes.test.ts +52 -12
  506. package/src/ui/compose/ComposeDialog.tsx +201 -7
  507. package/src/ui/compose/ComposePrompt.tsx +1 -5
  508. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  509. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  510. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  511. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  512. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  513. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  514. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  515. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  516. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  517. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  518. package/src/ui/dash/settings/GeneralContent.tsx +277 -113
  519. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  520. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  521. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  522. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  523. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  524. package/src/ui/dash/settings/discover-copy.ts +116 -0
  525. package/src/ui/feed/LinkCard.tsx +55 -45
  526. package/src/ui/feed/NoteCard.tsx +9 -4
  527. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  528. package/src/ui/feed/QuoteCard.tsx +55 -30
  529. package/src/ui/feed/ThreadPreview.tsx +6 -2
  530. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  531. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  532. package/src/ui/feed/thread-preview-state.ts +20 -6
  533. package/src/ui/font-themes.ts +39 -29
  534. package/src/ui/layouts/BaseLayout.tsx +155 -10
  535. package/src/ui/layouts/SiteLayout.tsx +192 -34
  536. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  537. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  538. package/src/ui/pages/ArchivePage.tsx +282 -289
  539. package/src/ui/pages/CollectionPage.tsx +86 -76
  540. package/src/ui/pages/CollectionsPage.tsx +5 -0
  541. package/src/ui/pages/ComposePage.tsx +8 -0
  542. package/src/ui/pages/PostPage.tsx +47 -0
  543. package/src/ui/pages/SearchPage.tsx +3 -3
  544. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  545. package/src/ui/pages/SubscribePage.tsx +236 -0
  546. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  547. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  548. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  549. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  550. package/src/ui/shared/CollectionsManager.tsx +70 -17
  551. package/src/ui/shared/CopyField.tsx +127 -0
  552. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  553. package/src/ui/shared/MediaGallery.tsx +125 -46
  554. package/src/ui/shared/Pagination.tsx +20 -5
  555. package/src/ui/shared/PostFooter.tsx +20 -11
  556. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  557. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  558. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  559. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  560. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  561. package/src/ui/shared/archive-labels.ts +98 -0
  562. package/src/ui/shared/collection-management-labels.ts +94 -16
  563. package/src/ui/shared/navigation-labels.ts +45 -71
  564. package/src/ui/shared/post-article-attributes.ts +2 -0
  565. package/src/ui/shared/smart-collection-labels.ts +343 -0
  566. package/dist/app-C75Zc8dD.js +0 -6
  567. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  568. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  569. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  570. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  571. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  572. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  573. package/dist/env-BPYViDJJ.js +0 -281
  574. package/dist/export-Dl5KCiEs.js +0 -5562
  575. package/dist/github-api-BNF35Lkx.js +0 -176
  576. package/dist/github-app-DdiD-bC4.js +0 -275
  577. package/dist/github-sync-Bqg62IvV.js +0 -4
  578. package/dist/github-sync-HdK2EgvJ.js +0 -431
  579. package/dist/url-CbLAtlZe.js +0 -900
  580. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  581. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  582. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  583. package/src/client/collection-created-notice.ts +0 -66
  584. package/src/client/collection-form-bridge.ts +0 -191
  585. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  586. package/src/client/components/jant-post-form.ts +0 -422
  587. package/src/client/components/post-form-template.ts +0 -352
  588. package/src/client/components/post-form-types.ts +0 -96
  589. package/src/client/media-metadata.ts +0 -247
  590. package/src/client/post-form-bridge.ts +0 -193
  591. package/src/client/tiptap/exitable-marks.ts +0 -73
  592. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,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
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { JSONContent } from "@tiptap/core";
10
10
  import { upgradeLegacyFootnotes } from "./footnotes.js";
11
+ import { fillRequiredContent } from "./markdown-manager.js";
11
12
  import {
12
13
  renderTiptapDocument,
13
14
  type TiptapRenderResult,
@@ -50,8 +51,9 @@ export function renderPostBodyHtml(postId: string, body: string): string {
50
51
  }
51
52
 
52
53
  /**
53
- * Normalize recognized historical footnotes and render the current HTML
54
- * projection in one parse pass.
54
+ * Normalize recognized historical footnotes, fill in children the schema
55
+ * requires (an empty list item gets its paragraph), and render the current
56
+ * HTML projection in one parse pass.
55
57
  *
56
58
  * @param postId - Immutable post TypeID
57
59
  * @param body - Canonical TipTap JSON
@@ -73,10 +75,15 @@ export function tryPreparePostBodyHtml(
73
75
  }
74
76
 
75
77
  const upgraded = upgradeLegacyFootnotes(parsed);
78
+ const doc = fillRequiredContent(upgraded.doc);
79
+ const canonical = JSON.stringify(doc);
76
80
  return {
77
81
  ok: true,
78
- body: upgraded.upgraded ? JSON.stringify(upgraded.doc) : body,
79
- html: renderTiptapDocument(upgraded.doc, { namespace: postId }),
82
+ body:
83
+ upgraded.upgraded || canonical !== JSON.stringify(upgraded.doc)
84
+ ? canonical
85
+ : body,
86
+ html: renderTiptapDocument(doc, { namespace: postId }),
80
87
  upgradedLegacyFootnotes: upgraded.upgraded,
81
88
  };
82
89
  } catch (error) {
@@ -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,21 +12,78 @@
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
 
30
+ /**
31
+ * Whether an empty stored value means "never configured" for this field.
32
+ *
33
+ * The test is whether the editor could have produced the empty value, and it
34
+ * mirrors what `normalizeConfigEditorDefinitionValue` accepts. A boolean takes
35
+ * only `true` or `false`; a number has to parse; an enum has to name one of its
36
+ * options. None of those can be stored empty, so an empty row for one of them
37
+ * is not a choice anybody made — it comes from a path that skips validation,
38
+ * and a snapshot's `db.sql`, replayed as raw SQL, is exactly that. Read as a
39
+ * configured value it silences the environment variable the operator did set,
40
+ * for as long as the row exists.
41
+ *
42
+ * Text is the opposite: clearing a description or a footer is an ordinary edit,
43
+ * so an empty value there is a decision and keeps its precedence. So is an enum
44
+ * that lists `""` among its options, the way `DASHBOARD_LANGUAGE` does, and one
45
+ * whose options come from a runtime source, where nothing here can rule it
46
+ * out.
47
+ *
48
+ * @param field - Config registry entry for the key being resolved
49
+ * @returns True when an empty stored value should fall through to env/default
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * isUnsetWhenEmpty(CONFIG_FIELDS.NOINDEX); // true (boolean)
54
+ * isUnsetWhenEmpty(CONFIG_FIELDS.SITE_FOOTER); // false (string)
55
+ * ```
56
+ */
57
+ function isUnsetWhenEmpty(
58
+ field: (typeof CONFIG_FIELDS)[keyof typeof CONFIG_FIELDS],
59
+ ): boolean {
60
+ if (!("editor" in field)) return false;
61
+ const editor = field.editor;
62
+ switch (editor.type) {
63
+ case "boolean":
64
+ case "number":
65
+ return true;
66
+ case "enum":
67
+ // Only a fixed option list proves the value is impossible. `""` is a real
68
+ // choice for `DASHBOARD_LANGUAGE`, which offers it, and an enum drawing
69
+ // its options from a runtime source cannot rule it out either.
70
+ return (
71
+ "options" in editor &&
72
+ !(editor.options as readonly string[]).includes("")
73
+ );
74
+ default:
75
+ return false;
76
+ }
77
+ }
78
+
27
79
  /**
28
80
  * Resolve a single config value following priority rules.
29
81
  *
82
+ * Settings saved in the dashboard outrank the environment, which outranks the
83
+ * default. The exception is an empty boolean or numeric value — see
84
+ * {@link isUnsetWhenEmpty}, which explains why that is absence rather than
85
+ * choice.
86
+ *
30
87
  * @param key - CONFIG_FIELDS key
31
88
  * @param allSettings - DB settings map
32
89
  * @param env - Worker bindings
@@ -43,7 +100,10 @@ function resolve(
43
100
 
44
101
  // User-configurable: DB > ENV > Default
45
102
  if (!field.envOnly && Object.hasOwn(allSettings, key)) {
46
- return allSettings[key] ?? "";
103
+ const stored = allSettings[key] ?? "";
104
+ if (stored !== "" || !isUnsetWhenEmpty(field)) {
105
+ return stored;
106
+ }
47
107
  }
48
108
 
49
109
  // ENV > Default
@@ -147,6 +207,13 @@ function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
147
207
  return value === "latest" || value === "featured" ? value : fallback;
148
208
  }
149
209
 
210
+ function parseArchiveLayout(
211
+ value: string,
212
+ fallback: ArchiveLayout,
213
+ ): ArchiveLayout {
214
+ return value === "list" || value === "grid" ? value : fallback;
215
+ }
216
+
150
217
  /**
151
218
  * Build a complete AppConfig from environment bindings and DB settings.
152
219
  *
@@ -198,6 +265,7 @@ export function resolveConfig(
198
265
  // Resolve avatar URL from storage key
199
266
  const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
200
267
  let siteAvatarUrl = "";
268
+ let siteAvatarThumbUrl = "";
201
269
  if (siteAvatar) {
202
270
  const publicUrl = getPublicUrlForProvider(
203
271
  storageDriver,
@@ -206,6 +274,17 @@ export function resolveConfig(
206
274
  localPublicUrl,
207
275
  );
208
276
  siteAvatarUrl = getMediaUrl(siteAvatar, publicUrl, sitePathPrefix);
277
+ // The header and drawer marks render this at 32px and 24px, on every page,
278
+ // in parallel with the render-blocking stylesheet. An untransformed avatar
279
+ // is whatever the author uploaded — commonly a 512px PNG — so it competes
280
+ // for the critical path with tens of times the bytes it can use. 128px
281
+ // still covers a high-DPR header and a theme that enlarges the mark.
282
+ siteAvatarThumbUrl = getImageUrl(siteAvatarUrl, imageTransformUrl, {
283
+ width: 128,
284
+ quality: 80,
285
+ format: "auto",
286
+ fit: "scale-down",
287
+ });
209
288
  }
210
289
 
211
290
  // Description is "explicit" when set in DB or ENV (not just the default)
@@ -216,6 +295,16 @@ export function resolveConfig(
216
295
  ? !!dbDescription
217
296
  : !!envDescription;
218
297
 
298
+ // The owner's stored choice and the deployment's default stay apart, and
299
+ // the registry default must not stand in for either: an absent row is what
300
+ // lets `noindex` decide, and an absent binding is what makes Discover
301
+ // opt-in for a self-hosted site.
302
+ const discoverStoredValue = allSettings["DISCOVER"];
303
+ const discoverDefaultValue = getDiscoverDefault(env);
304
+ const noindex = demoMode || resolve("NOINDEX", allSettings, env) === "true";
305
+ const rssFeedsEnabled =
306
+ resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true";
307
+
219
308
  return {
220
309
  // Site identity (DB > ENV > Default)
221
310
  siteName: resolve("SITE_NAME", allSettings, env),
@@ -223,19 +312,34 @@ export function resolveConfig(
223
312
  siteDescriptionExplicit,
224
313
  siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
225
314
  dashboardLanguage: resolve("DASHBOARD_LANGUAGE", allSettings, env),
226
- cjkSerifFont: resolve("CJK_SERIF_FONT", allSettings, env),
315
+ multilingualEnabled:
316
+ resolve("MULTILINGUAL_ENABLED", allSettings, env) === "true",
317
+ additionalLanguages: parseLanguageList(
318
+ resolve("ADDITIONAL_LANGUAGES", allSettings, env),
319
+ ),
227
320
  mainRssFeed: parseFeedKind(
228
321
  resolve("MAIN_RSS_FEED", allSettings, env),
229
322
  "featured",
230
323
  ),
324
+ archiveDefaultLayout: parseArchiveLayout(
325
+ resolve("ARCHIVE_DEFAULT_LAYOUT", allSettings, env),
326
+ "list",
327
+ ),
231
328
  timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
232
329
  siteFooter: resolve("SITE_FOOTER", allSettings, env),
233
330
  showJantBrandingOnHome:
234
331
  resolve("SHOW_JANT_BRANDING_ON_HOME", allSettings, env) === "true",
235
- noindex: demoMode || resolve("NOINDEX", allSettings, env) === "true",
332
+ noindex,
333
+ discover: resolveDiscoverMode({
334
+ storedValue: discoverStoredValue,
335
+ defaultValue: discoverDefaultValue,
336
+ demoMode,
337
+ noindex,
338
+ rssFeedsEnabled,
339
+ }),
236
340
  publicApiEnabled:
237
341
  resolve("PUBLIC_API_ENABLED", allSettings, env) === "true",
238
- rssFeedsEnabled: resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true",
342
+ rssFeedsEnabled,
239
343
 
240
344
  // Infrastructure (ENV only)
241
345
  siteUrl,
@@ -318,6 +422,7 @@ export function resolveConfig(
318
422
  siteAvatar,
319
423
  showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
320
424
  siteAvatarUrl,
425
+ siteAvatarThumbUrl,
321
426
  faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
322
427
 
323
428
  // Rate limiting (ENV only). Defaults are conservative enough for a