@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
@@ -11,8 +11,10 @@
11
11
  import type { FC, PropsWithChildren } from "hono/jsx";
12
12
  import type { Context } from "hono";
13
13
  import { raw } from "hono/utils/html";
14
+ import { escapeHtml } from "../../lib/html.js";
14
15
  import { msg } from "@lingui/core/macro";
15
16
  import {
17
+ getPreconnectHints,
16
18
  getPublicAssetBasePath,
17
19
  toAssetPath,
18
20
  toPublicAssetPath,
@@ -21,8 +23,12 @@ import { getJantIconHref } from "../../lib/jant-branding.js";
21
23
  import { getPublicUrlForProvider } from "../../lib/image.js";
22
24
  import { getThemeBrowserColors, resolveBuiltinTheme } from "../../lib/theme.js";
23
25
  import { toAbsoluteAssetUrl, toPublicPath } from "../../lib/url.js";
26
+ import type { LanguageAlternate } from "../../lib/view-language.js";
27
+ import { toLanguagePrefix } from "../../i18n/locales.js";
24
28
  import {
25
29
  CLIENT_AUTH_JS_FILE,
30
+ CLIENT_AUTHOR_CSS_FILE,
31
+ CLIENT_COMPOSE_PRELOAD,
26
32
  CLIENT_CJK_CSS_FILE,
27
33
  CLIENT_CJK_JP_CSS_FILE,
28
34
  CLIENT_CJK_KR_CSS_FILE,
@@ -30,10 +36,13 @@ import {
30
36
  CLIENT_CSS_FILE,
31
37
  CLIENT_JS_FILE,
32
38
  CORE_VERSION,
33
- IS_VITE_DEV,
34
39
  } from "../../lib/version.js";
40
+ import { IS_VITE_DEV } from "../../lib/build-env.js";
35
41
  import { I18nProvider } from "../../i18n/index.js";
36
- import { resolveCjkFontProfile } from "../font-themes.js";
42
+ import {
43
+ getCjkFontCssVariables,
44
+ resolveCjkFontProfile,
45
+ } from "../font-themes.js";
37
46
  import { resetIconCollector } from "../shared/icon-collector.js";
38
47
  import { Icon } from "../shared/Icon.js";
39
48
  import { IconSprite } from "../shared/IconSprite.js";
@@ -43,6 +52,17 @@ export interface ToastProps {
43
52
  type?: "success" | "error";
44
53
  }
45
54
 
55
+ /** One `<link rel="alternate" type="application/atom+xml">` autodiscovery target. */
56
+ export interface PageFeedLink {
57
+ /** Public path (or absolute URL) of the feed. */
58
+ href: string;
59
+ /**
60
+ * Label feed readers show for this feed, e.g. a collection's title. Omitted
61
+ * when blank, so the reader falls back to the feed document's own title.
62
+ */
63
+ title?: string;
64
+ }
65
+
46
66
  export interface BaseLayoutProps {
47
67
  title: string;
48
68
  description?: string;
@@ -81,7 +101,32 @@ export interface BaseLayoutProps {
81
101
  * thread at both the reply URL and the thread-root URL).
82
102
  */
83
103
  canonicalHref?: string;
84
- noindex?: boolean;
104
+ /**
105
+ * Absolute URLs of this page in the site's other languages, rendered as
106
+ * `<link rel="alternate" hreflang>`. Set on pages that exist once per
107
+ * language (list surfaces) or that have translations (posts).
108
+ */
109
+ alternateLanguages?: LanguageAlternate[];
110
+ /**
111
+ * The feed this page itself publishes — a collection's feed on a collection
112
+ * page, the filtered archive feed on `/archive`. Rendered ahead of the
113
+ * site-wide feed links, since a reader subscribing from a page means the
114
+ * page they are on.
115
+ */
116
+ pageFeed?: PageFeedLink;
117
+ /**
118
+ * Crawler policy for this page, overriding the site-wide setting.
119
+ *
120
+ * `true` is the blunt form for utility pages that should leave no trace:
121
+ * `noindex, nofollow`. `"follow"` keeps the URL out of the index while
122
+ * letting a crawler walk its links — for a page that is one URL out of a
123
+ * combinatorial family (a filtered archive) but whose links lead to the
124
+ * real pages.
125
+ *
126
+ * A site-wide noindex always wins: a page may be stricter than the site
127
+ * setting, never looser.
128
+ */
129
+ noindex?: boolean | "follow";
85
130
  isAuthenticated?: boolean;
86
131
  clientBundle?: "public" | "full";
87
132
  }
@@ -105,6 +150,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
105
150
  articlePublishedTime,
106
151
  articleModifiedTime,
107
152
  canonicalHref,
153
+ alternateLanguages,
154
+ pageFeed,
108
155
  noindex,
109
156
  isAuthenticated = false,
110
157
  clientBundle,
@@ -130,8 +177,17 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
130
177
  getJantIconHref("socialImage", appConfig?.sitePathPrefix || "");
131
178
  const resolvedFaviconVersion =
132
179
  faviconVersion ?? (appConfig?.faviconVersion || undefined);
133
- const resolvedNoindex = noindex ?? appConfig?.noindex;
180
+ // A site-wide noindex is the strictest statement available, so it wins
181
+ // outright — a per-page policy can only narrow further, never relax.
182
+ const resolvedNoindex = appConfig?.noindex ? true : (noindex ?? false);
134
183
  const sitePathPrefix = appConfig?.sitePathPrefix || "";
184
+ // Where "here" is for links the client builds. The server has `toViewPath`
185
+ // for this; without the same base in the DOM, a client-rendered link to a
186
+ // per-language surface silently drops the reader into the primary view.
187
+ const viewLang = c?.get("viewLang");
188
+ const viewBasePath = viewLang
189
+ ? `${sitePathPrefix}/${toLanguagePrefix(viewLang)}`
190
+ : sitePathPrefix;
135
191
  const assetBasePath = IS_VITE_DEV
136
192
  ? "/"
137
193
  : appConfig?.assetBasePath || getPublicAssetBasePath(sitePathPrefix);
@@ -145,6 +201,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
145
201
  appConfig?.s3PublicUrl,
146
202
  appConfig?.localPublicUrl,
147
203
  ) ?? "";
204
+ // Empty unless assets or media live on their own host; the two collapse to
205
+ // one set of hints when they share a host.
206
+ const preconnectHints = getPreconnectHints({
207
+ assetBasePath,
208
+ mediaBaseUrl: mediaBase,
209
+ siteUrl: appConfig?.siteUrl,
210
+ });
148
211
  const currentUrl = c ? c.get("publicRequestUrl") : undefined;
149
212
  const rawPath = c?.req?.path ?? "/";
150
213
  const manifestStartPath = sitePathPrefix
@@ -177,11 +240,22 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
177
240
  const browserThemeColors = getThemeBrowserColors(activeTheme);
178
241
  const resolvedClientBundle =
179
242
  clientBundle ?? (isAuthenticated ? "full" : "public");
180
- const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
181
- const cjkFontProfile = resolveCjkFontProfile(
182
- appConfig?.siteLanguage ?? resolvedLang,
183
- cjkSerifFont,
184
- );
243
+ // Derived from the language of *this page*, not the site's: on a post page
244
+ // that is the post's own language, on a language-filtered list it is the view
245
+ // language, and otherwise it falls back to the site language. Simplified,
246
+ // Traditional, Japanese and Korean render the same code points with different
247
+ // glyphs, so a page in one must not inherit another's font stack.
248
+ const cjkFontProfile = resolveCjkFontProfile(resolvedLang);
249
+ const cjkFontDeclarations = Object.entries(
250
+ getCjkFontCssVariables(resolvedLang),
251
+ )
252
+ .map(([name, value]) => ` ${name}: ${value};`)
253
+ .join("\n");
254
+ // `:root:root` matches the specificity `buildThemeStyle` uses, so ordering
255
+ // alone decides the winner.
256
+ const cjkFontStyle = cjkFontDeclarations
257
+ ? `:root:root {\n${cjkFontDeclarations}\n}`
258
+ : "";
185
259
  const cjkStylesheetPath =
186
260
  cjkFontProfile === "zh-Hans"
187
261
  ? IS_VITE_DEV
@@ -200,6 +274,16 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
200
274
  ? assetPath("/src/style-cjk-kr.css")
201
275
  : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath)
202
276
  : null;
277
+ // The composer, the editor chrome, the settings pages and the draft preview
278
+ // bar are the bulk of the hand-written component CSS, and none of it renders
279
+ // for a signed-out visitor. Linking it only here keeps it off the critical
280
+ // path of every public page.
281
+ const authorStylesheetPath =
282
+ resolvedClientBundle === "full"
283
+ ? IS_VITE_DEV
284
+ ? assetPath("/src/style-author.css")
285
+ : toPublicAssetPath(CLIENT_AUTHOR_CSS_FILE, assetBasePath)
286
+ : null;
203
287
  const clientScriptPath = IS_VITE_DEV
204
288
  ? resolvedClientBundle === "full"
205
289
  ? assetPath("/src/client-auth.ts")
@@ -211,6 +295,15 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
211
295
  resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE,
212
296
  assetBasePath,
213
297
  );
298
+ // The composer loads on first use; fetching its files now means the first
299
+ // open costs no round trip. Dev serves modules straight from source, so
300
+ // there is nothing to preload there.
301
+ const composePreloadPaths =
302
+ !IS_VITE_DEV && resolvedClientBundle === "full"
303
+ ? CLIENT_COMPOSE_PRELOAD.map((file) =>
304
+ toPublicAssetPath(file, assetBasePath),
305
+ )
306
+ : [];
214
307
  const faviconAssetVersion = resolvedFaviconVersion || CORE_VERSION;
215
308
  const resolvedFaviconHref =
216
309
  faviconHref ??
@@ -290,6 +383,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
290
383
  appConfig?.mainRssFeed === "latest"
291
384
  ? { href: featuredFeedHref, title: featuredFeedTitle }
292
385
  : { href: latestFeedHref, title: latestFeedTitle };
386
+ const pageFeedHref = feedsEnabled ? pageFeed?.href.trim() || null : null;
387
+ const pageFeedTitle = pageFeed?.title?.trim() || undefined;
293
388
 
294
389
  return (
295
390
  <>
@@ -299,6 +394,8 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
299
394
  data-theme={appConfig?.themeId}
300
395
  data-theme-mode={themeMode}
301
396
  data-site-path-prefix={sitePathPrefix}
397
+ data-view-base-path={viewBasePath}
398
+ data-view-lang={viewLang ?? undefined}
302
399
  data-asset-base-path={assetBasePath}
303
400
  data-media-base={mediaBase}
304
401
  >
@@ -308,6 +405,13 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
308
405
  name="viewport"
309
406
  content="width=device-width, initial-scale=1.0"
310
407
  />
408
+ {preconnectHints.map((hint) => (
409
+ <link
410
+ rel="preconnect"
411
+ href={hint.href}
412
+ {...(hint.crossorigin ? { crossorigin: "anonymous" } : {})}
413
+ />
414
+ ))}
311
415
  {themeMode === "dark" ? (
312
416
  <meta name="theme-color" content={browserThemeColors.dark} />
313
417
  ) : themeMode === "light" ? (
@@ -381,7 +485,14 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
381
485
  <meta name="twitter:image:alt" content={socialImageAltText} />
382
486
  )}
383
487
  {resolvedNoindex && (
384
- <meta name="robots" content="noindex, nofollow" />
488
+ <meta
489
+ name="robots"
490
+ content={
491
+ resolvedNoindex === "follow"
492
+ ? "noindex, follow"
493
+ : "noindex, nofollow"
494
+ }
495
+ />
385
496
  )}
386
497
  {!resolvedNoindex && jsonLd != null && (
387
498
  <script
@@ -397,6 +508,21 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
397
508
  />
398
509
  )}
399
510
  {canonicalHref && <link rel="canonical" href={canonicalHref} />}
511
+ {/* Emitted as raw HTML on purpose: hono/jsx deduplicates `<link>`
512
+ elements by `href` alone, which silently drops the self-referential
513
+ alternate (it shares the canonical link's URL) and any `x-default`
514
+ (it shares the primary language's). Both are required for an
515
+ hreflang set to be honoured. Values are escaped here. */}
516
+ {alternateLanguages && alternateLanguages.length > 0
517
+ ? raw(
518
+ alternateLanguages
519
+ .map(
520
+ (alternate) =>
521
+ `<link rel="alternate" hreflang="${escapeHtml(alternate.hreflang)}" href="${escapeHtml(alternate.href)}"/>`,
522
+ )
523
+ .join(""),
524
+ )
525
+ : null}
400
526
  <link rel="icon" href={resolvedFaviconHref} sizes="16x16 32x32" />
401
527
  <link rel="apple-touch-icon" href={resolvedAppleTouchHref} />
402
528
  <link
@@ -408,6 +534,14 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
408
534
  sitePathPrefix,
409
535
  )}
410
536
  />
537
+ {pageFeedHref && (
538
+ <link
539
+ rel="alternate"
540
+ type="application/atom+xml"
541
+ title={pageFeedTitle}
542
+ href={pageFeedHref}
543
+ />
544
+ )}
411
545
  {mainFeedHref && (
412
546
  <link
413
547
  rel="alternate"
@@ -438,6 +572,9 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
438
572
  {cjkStylesheetPath && (
439
573
  <link rel="stylesheet" href={cjkStylesheetPath} />
440
574
  )}
575
+ {authorStylesheetPath && (
576
+ <link rel="stylesheet" href={authorStylesheetPath} />
577
+ )}
441
578
  {/* Critical inline style: prevent mobile nav jitter by applying
442
579
  responsive header layout before external CSS/JS loads */}
443
580
  <style
@@ -445,6 +582,11 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
445
582
  __html: `.site-header-search-link,.site-header-hamburger,.site-header-more-responsive-only,.site-header-more-link-responsive,.site-header-more-divider-responsive{display:none!important}@media(max-width:1200px){.site-header-search-form{display:none!important}.site-header-search-link{display:inline-flex!important}}@media(max-width:960px){.site-header-link-collapse-lg{display:none!important}.site-header-more-responsive-only.site-header-more-tier-lg{display:inline-flex!important}.site-header-more-link-show-lg{display:flex!important}.site-header-more-divider-show-lg{display:block!important}}@media(max-width:780px){.site-header-link-collapse-md{display:none!important}.site-header-more-responsive-only.site-header-more-tier-md{display:inline-flex!important}.site-header-more-link-show-md{display:flex!important}.site-header-more-divider-show-md{display:block!important}}@media(max-width:580px){.site-header-link-collapse-sm{display:none!important}.site-header-more-responsive-only.site-header-more-tier-sm{display:inline-flex!important}.site-header-more-link-show-sm{display:flex!important}.site-header-more-divider-show-sm{display:block!important}}@media(max-width:480px){.site-header-nav,.site-header-more{display:none!important}.site-header-search-slot{display:flex!important}.site-header-hamburger{display:flex!important}.site-header-right{margin-left:.35rem}}`,
446
583
  }}
447
584
  />
585
+ {/* Emitted before the theme style so a font theme that names its own
586
+ CJK stack still wins. */}
587
+ {cjkFontStyle && (
588
+ <style dangerouslySetInnerHTML={{ __html: cjkFontStyle }} />
589
+ )}
448
590
  {themeStyle && (
449
591
  <style dangerouslySetInnerHTML={{ __html: themeStyle }} />
450
592
  )}
@@ -453,6 +595,9 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
453
595
  )}
454
596
  {customHeadHtml && raw(customHeadHtml)}
455
597
  <script type="module" src={clientScriptPath} />
598
+ {composePreloadPaths.map((href) => (
599
+ <link rel="modulepreload" href={href} />
600
+ ))}
456
601
  </head>
457
602
  <body
458
603
  class="bg-background text-foreground antialiased"
@@ -8,7 +8,11 @@
8
8
  import { msg } from "@lingui/core/macro";
9
9
  import type { FC, PropsWithChildren } from "hono/jsx";
10
10
  import { useLingui } from "../../i18n/context.js";
11
- import type { NavItemView, SiteLayoutProps } from "../../types.js";
11
+ import type {
12
+ LanguageSwitcherOption,
13
+ NavItemView,
14
+ SiteLayoutProps,
15
+ } from "../../types.js";
12
16
  import { toPublicPath } from "../../lib/url.js";
13
17
  import { ComposeDialog } from "../compose/ComposeDialog.js";
14
18
  import { ComposePrompt } from "../compose/ComposePrompt.js";
@@ -117,8 +121,17 @@ export interface SiteHeaderProps {
117
121
  links: NavItemView[];
118
122
  currentPath: string;
119
123
  sitePathPrefix?: string;
120
- siteAvatarUrl?: string;
124
+ /**
125
+ * Public base for language-scoped links — the logo, search, and home
126
+ * detection. Equals `sitePathPrefix` outside a language view, and carries
127
+ * the language prefix inside one (`/blog/ja`), so a page in the Japanese
128
+ * view leads home to `/ja` rather than to the primary language.
129
+ */
130
+ basePath?: string;
131
+ siteAvatarThumbUrl?: string;
121
132
  showHeaderAvatar?: boolean;
133
+ /** Languages this site publishes in. Empty on a single-language site. */
134
+ languageSwitcher?: LanguageSwitcherOption[];
122
135
  }
123
136
 
124
137
  function linkCollapseTier(idx: number): string {
@@ -148,10 +161,13 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
148
161
  links,
149
162
  currentPath,
150
163
  sitePathPrefix = "",
151
- siteAvatarUrl,
164
+ basePath = sitePathPrefix,
165
+ siteAvatarThumbUrl,
152
166
  showHeaderAvatar,
167
+ languageSwitcher = [],
153
168
  }) => {
154
169
  const { i18n } = useLingui();
170
+ const homeHref = basePath || "/";
155
171
  const linksWithLabels = links.map((link) => ({
156
172
  ...link,
157
173
  displayLabel: getNavItemDisplayLabel(link, i18n, sitePathPrefix),
@@ -164,7 +180,7 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
164
180
  comment: "@context: Search icon link in browse nav",
165
181
  }),
166
182
  );
167
- const searchHref = toPublicPath("/search", sitePathPrefix);
183
+ const searchHref = toPublicPath("/search", basePath);
168
184
 
169
185
  const menuLabel = i18n._(
170
186
  msg({
@@ -180,6 +196,32 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
180
196
  );
181
197
  const moreLabel = i18n._(NAV_MORE_LABEL);
182
198
 
199
+ // The switcher is a "take me to this language's site" control, so it names
200
+ // each language in that language — a reader looking for their own language
201
+ // should not have to read the current one to find it.
202
+ const languageLabel = i18n._(
203
+ msg({
204
+ message: "Language",
205
+ comment: "@context: Accessible label for the site language switcher",
206
+ }),
207
+ );
208
+ // Which language the reader is in, and whether that is worth saying out
209
+ // loud: the root is the site's default, everything else is a variant of it.
210
+ const currentLanguage = languageSwitcher.find((option) => option.isCurrent);
211
+ const offPrimaryLanguage = Boolean(
212
+ currentLanguage && !currentLanguage.isPrimary,
213
+ );
214
+ // The visible name is written in its own language, so the accessible name
215
+ // has to carry it too — a control read out as "Language" alone would lose
216
+ // the one thing it says.
217
+ const currentLanguageLabel = i18n._(
218
+ msg({
219
+ message: "Language: {language}",
220
+ comment:
221
+ "@context: Accessible label for the site language switcher when it names the language on screen",
222
+ }),
223
+ { language: currentLanguage?.label ?? "" },
224
+ );
183
225
  // Split custom links by placement.
184
226
  const headerLinks = linksWithLabels.filter(
185
227
  (l) => l.placement === "header" || !l.placement,
@@ -206,9 +248,9 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
206
248
  .filter(Boolean)
207
249
  .join(" ");
208
250
  const isHomePage =
209
- currentPath === toPublicPath("/", sitePathPrefix) ||
210
- currentPath === toPublicPath("/featured", sitePathPrefix) ||
211
- currentPath === toPublicPath("/latest", sitePathPrefix);
251
+ currentPath === homeHref ||
252
+ currentPath === toPublicPath("/featured", basePath) ||
253
+ currentPath === toPublicPath("/latest", basePath);
212
254
 
213
255
  return (
214
256
  <>
@@ -217,9 +259,9 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
217
259
  <div
218
260
  class={`site-header-top site-header-top-bordered${isHomePage ? " site-header-top-home" : ""}`}
219
261
  >
220
- <a href={toPublicPath("/", sitePathPrefix)} class="site-logo">
221
- {showHeaderAvatar && siteAvatarUrl && (
222
- <img src={siteAvatarUrl} class="site-logo-avatar" alt="" />
262
+ <a href={homeHref} class="site-logo">
263
+ {showHeaderAvatar && siteAvatarThumbUrl && (
264
+ <img src={siteAvatarThumbUrl} class="site-logo-avatar" alt="" />
223
265
  )}
224
266
  {siteName}
225
267
  </a>
@@ -357,6 +399,93 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
357
399
  </div>
358
400
 
359
401
  <div class="site-header-right">
402
+ {languageSwitcher.length > 1 && (
403
+ <div
404
+ class={`site-header-lang${
405
+ offPrimaryLanguage ? " site-header-lang-named" : ""
406
+ }`}
407
+ >
408
+ <button
409
+ type="button"
410
+ class="site-header-more-btn site-header-lang-btn"
411
+ id="site-nav-lang-trigger"
412
+ aria-haspopup="menu"
413
+ aria-expanded="false"
414
+ aria-label={
415
+ offPrimaryLanguage ? currentLanguageLabel : languageLabel
416
+ }
417
+ >
418
+ {/* A globe, never a flag: flags name countries rather
419
+ than languages. The primary language is the site as
420
+ it comes, so the globe stands alone there; every
421
+ other view is a variant of it, and a reader who
422
+ landed on one — from a search result, a shared link,
423
+ a post written in another language — is told which,
424
+ without opening anything. */}
425
+ <svg
426
+ xmlns="http://www.w3.org/2000/svg"
427
+ width="15"
428
+ height="15"
429
+ viewBox="0 0 24 24"
430
+ fill="none"
431
+ stroke="currentColor"
432
+ stroke-width="2"
433
+ stroke-linecap="round"
434
+ stroke-linejoin="round"
435
+ aria-hidden="true"
436
+ class="site-header-lang-globe"
437
+ >
438
+ <circle cx="12" cy="12" r="10" />
439
+ <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
440
+ <path d="M2 12h20" />
441
+ </svg>
442
+ {offPrimaryLanguage && (
443
+ <span
444
+ class="site-header-lang-name"
445
+ lang={currentLanguage?.lang}
446
+ >
447
+ {currentLanguage?.label}
448
+ </span>
449
+ )}
450
+ <svg
451
+ xmlns="http://www.w3.org/2000/svg"
452
+ width="12"
453
+ height="12"
454
+ viewBox="0 0 24 24"
455
+ fill="none"
456
+ stroke="currentColor"
457
+ stroke-width="2.5"
458
+ stroke-linecap="round"
459
+ stroke-linejoin="round"
460
+ aria-hidden="true"
461
+ >
462
+ <path d="m6 9 6 6 6-6" />
463
+ </svg>
464
+ </button>
465
+ <div
466
+ id="site-nav-lang-popover"
467
+ class="site-header-more-popover site-header-lang-popover"
468
+ data-popover
469
+ data-align="end"
470
+ aria-hidden="true"
471
+ >
472
+ {languageSwitcher.map((option) => (
473
+ <a
474
+ key={option.lang}
475
+ href={option.href}
476
+ hreflang={option.lang}
477
+ lang={option.lang}
478
+ class={`site-header-more-link ${option.isCurrent ? "site-header-more-link-active" : ""}`}
479
+ {...(option.isCurrent
480
+ ? { "aria-current": "true" }
481
+ : {})}
482
+ >
483
+ {option.label}
484
+ </a>
485
+ ))}
486
+ </div>
487
+ </div>
488
+ )}
360
489
  <button
361
490
  type="button"
362
491
  class="site-header-hamburger"
@@ -401,13 +530,10 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
401
530
  inert
402
531
  >
403
532
  <div class="site-nav-drawer-header">
404
- <a
405
- href={toPublicPath("/", sitePathPrefix)}
406
- class="site-nav-drawer-brand"
407
- >
408
- {showHeaderAvatar && siteAvatarUrl && (
533
+ <a href={homeHref} class="site-nav-drawer-brand">
534
+ {showHeaderAvatar && siteAvatarThumbUrl && (
409
535
  <img
410
- src={siteAvatarUrl}
536
+ src={siteAvatarThumbUrl}
411
537
  class="site-nav-drawer-brand-avatar"
412
538
  alt=""
413
539
  />
@@ -470,6 +596,24 @@ export const SiteHeader: FC<SiteHeaderProps> = ({
470
596
  ))}
471
597
  </>
472
598
  )}
599
+ {languageSwitcher.length > 1 && (
600
+ <>
601
+ <div class="site-nav-drawer-divider" />
602
+ <span class="site-nav-drawer-section-label">{languageLabel}</span>
603
+ {languageSwitcher.map((option) => (
604
+ <a
605
+ key={option.lang}
606
+ href={option.href}
607
+ hreflang={option.lang}
608
+ lang={option.lang}
609
+ class={`site-nav-drawer-link site-nav-drawer-link-secondary ${option.isCurrent ? "site-nav-drawer-link-active" : ""}`}
610
+ {...(option.isCurrent ? { "aria-current": "true" } : {})}
611
+ >
612
+ {option.label}
613
+ </a>
614
+ ))}
615
+ </>
616
+ )}
473
617
  </nav>
474
618
  </div>
475
619
  </>
@@ -481,9 +625,10 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
481
625
  links,
482
626
  currentPath,
483
627
  sitePathPrefix = "",
628
+ basePath = sitePathPrefix,
484
629
  isAuthenticated,
485
630
  collections,
486
- siteAvatarUrl,
631
+ siteAvatarThumbUrl,
487
632
  showHeaderAvatar,
488
633
  siteDescriptionHtml,
489
634
  siteFooterHtml,
@@ -495,6 +640,9 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
495
640
  composeOpenShortcutDiscovered = false,
496
641
  slashCommandDiscovered = false,
497
642
  composeCollectionId,
643
+ languageSwitcher,
644
+ composeLanguages,
645
+ composeContextLanguage,
498
646
  children,
499
647
  }) => {
500
648
  const { i18n } = useLingui();
@@ -507,35 +655,33 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
507
655
  );
508
656
 
509
657
  const isHomePage =
510
- currentPath === toPublicPath("/", sitePathPrefix) ||
511
- currentPath === toPublicPath("/featured", sitePathPrefix) ||
512
- currentPath === toPublicPath("/latest", sitePathPrefix);
658
+ currentPath === (basePath || "/") ||
659
+ currentPath === toPublicPath("/featured", basePath) ||
660
+ currentPath === toPublicPath("/latest", basePath);
513
661
  const showMobileComposeFab = Boolean(
514
662
  (isHomePage || composeCollectionId) && isAuthenticated && showComposeDialog,
515
663
  );
516
- const mobileComposeFabClick = composeCollectionId
517
- ? `document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew(${JSON.stringify(
518
- { collectionId: composeCollectionId },
519
- )})`
520
- : "document.getElementById('compose-dialog')?.querySelector('jant-compose-dialog')?.openNew()";
521
- const contentClass = [
522
- "site-content",
523
- isHomePage ? "site-content-home" : "",
524
- showMobileComposeFab ? "site-content-mobile-compose-enabled" : "",
525
- ]
664
+ const contentClass = ["site-content", isHomePage ? "site-content-home" : ""]
526
665
  .filter(Boolean)
527
666
  .join(" ");
667
+ // The page, not the content, keeps room for the button: the footer and the
668
+ // Jant credit come after the content, and they must end above it too.
669
+ const pageClass = showMobileComposeFab
670
+ ? "site-page site-page-mobile-compose-enabled"
671
+ : "site-page";
528
672
 
529
673
  return (
530
- <div class="site-page">
674
+ <div class={pageClass}>
531
675
  {showHeader && (
532
676
  <SiteHeader
533
677
  siteName={siteName}
534
678
  links={links}
535
679
  currentPath={currentPath}
536
680
  sitePathPrefix={sitePathPrefix}
537
- siteAvatarUrl={siteAvatarUrl}
681
+ basePath={basePath}
682
+ siteAvatarThumbUrl={siteAvatarThumbUrl}
538
683
  showHeaderAvatar={showHeaderAvatar}
684
+ languageSwitcher={languageSwitcher}
539
685
  />
540
686
  )}
541
687
 
@@ -582,7 +728,10 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
582
728
  type="button"
583
729
  class="site-mobile-compose-fab"
584
730
  aria-label={newPostLabel}
585
- data-on:click={mobileComposeFabClick}
731
+ data-compose-open
732
+ {...(composeCollectionId
733
+ ? { "data-compose-collection-id": composeCollectionId }
734
+ : {})}
586
735
  >
587
736
  <ComposeFeatherIcon class="site-mobile-compose-fab-icon" />
588
737
  </button>
@@ -602,12 +751,21 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
602
751
 
603
752
  <jant-media-lightbox />
604
753
  <jant-text-preview />
605
- {isAuthenticated && <jant-post-menu />}
754
+ {isAuthenticated && (
755
+ <jant-post-menu
756
+ languages={JSON.stringify(composeLanguages ?? []).replace(
757
+ /</g,
758
+ "\\u003c",
759
+ )}
760
+ />
761
+ )}
606
762
  {isAuthenticated && showComposeDialog && (
607
763
  <ComposeDialog
608
764
  collections={collections}
609
765
  uploadMaxFileSize={uploadMaxFileSize}
610
766
  slashCommandDiscovered={slashCommandDiscovered}
767
+ languages={composeLanguages}
768
+ contextLanguage={composeContextLanguage}
611
769
  />
612
770
  )}
613
771
  </div>