@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
@@ -10,11 +10,13 @@
10
10
 
11
11
  import { z } from "zod";
12
12
  import {
13
+ ARCHIVE_LAYOUTS,
13
14
  FORMATS,
14
15
  STATUSES,
15
16
  VISIBILITIES,
16
17
  SORT_ORDERS,
17
18
  COLLECTION_SORT_ORDERS,
19
+ SMART_COLLECTION_SORT_ORDERS,
18
20
  NAV_ITEM_TYPES,
19
21
  SYSTEM_NAV_KEY_VALUES,
20
22
  MAX_MEDIA_ATTACHMENTS,
@@ -30,14 +32,20 @@ import {
30
32
  type ConfigEditorDefinition,
31
33
  type ConfigKey,
32
34
  } from "../types.js";
33
- import { isCjkSerifFont } from "../i18n/detect.js";
34
35
  import {
36
+ formatLanguageList,
35
37
  isLocale,
36
38
  isValidContentLanguage,
37
39
  normalizeContentLanguage,
40
+ parseLanguageList,
38
41
  } from "../i18n/locales.js";
39
42
  import { ValidationError } from "./errors.js";
40
43
  import { createTypeIdSchema, ID_PREFIX } from "./ids.js";
44
+ import {
45
+ FILTER_DIMENSION_KEYS,
46
+ FILTER_DIMENSIONS,
47
+ type PostFilterSelection,
48
+ } from "./filter-dimensions.js";
41
49
  import { normalizeSlug } from "./slug-format.js";
42
50
  import { isReservedPath } from "./constants.js";
43
51
  import { sanitizeUrl, normalizePath } from "./url.js";
@@ -69,6 +77,27 @@ export function normalizeEmail(email: string): string {
69
77
  return email.trim().toLowerCase();
70
78
  }
71
79
 
80
+ /**
81
+ * A display name to open an account under, before the author has given one.
82
+ *
83
+ * The first setup screen asks for credentials only, so better-auth needs a
84
+ * name it can create the row with; the second screen overwrites it with the
85
+ * site's name. This stands in only for the gap between the two, and for the
86
+ * setup that is abandoned in it — which is why it is the address the author
87
+ * just typed rather than a placeholder they would not recognise.
88
+ *
89
+ * @param email - The normalized address the account is being opened under
90
+ * @returns The part before the `@`, or the whole address when there is none
91
+ * @example
92
+ * ```ts
93
+ * deriveAccountName("owner@example.com");
94
+ * // Returns: "owner"
95
+ * ```
96
+ */
97
+ export function deriveAccountName(email: string): string {
98
+ return email.split("@")[0]?.trim() || email;
99
+ }
100
+
72
101
  /** Trim, strip control characters, and collapse to undefined when empty. */
73
102
  function sanitizeText(maxLength: number) {
74
103
  return z
@@ -166,13 +195,20 @@ export const NavItemTypeSchema = z.enum(NAV_ITEM_TYPES);
166
195
  export const SystemNavKeySchema = z.enum(SYSTEM_NAV_KEY_VALUES);
167
196
 
168
197
  /**
169
- * Redirect type enum schema
170
- * Form input validation for redirect type (stored as number in DB)
198
+ * Redirect type, as forms send it ("301") or as the API answers it (301).
199
+ * Parses to the string form; stored as a number.
171
200
  */
172
- export const RedirectTypeSchema = z.enum(["301", "302"]);
201
+ export const RedirectTypeSchema = z.union([
202
+ z.enum(["301", "302"]),
203
+ z.literal(301).transform(() => "301" as const),
204
+ z.literal(302).transform(() => "302" as const),
205
+ ]);
173
206
 
174
207
  /**
175
- * Custom URL target type enum schema
208
+ * Custom URL target type enum schema.
209
+ *
210
+ * Every kind a stored path can be, including `archive` — which is read and
211
+ * listed but no longer created. See {@link CreatableCustomUrlTargetTypeSchema}.
176
212
  */
177
213
  export const CustomUrlTargetTypeSchema = z.enum([
178
214
  "post",
@@ -181,6 +217,20 @@ export const CustomUrlTargetTypeSchema = z.enum([
181
217
  "archive",
182
218
  ]);
183
219
 
220
+ /**
221
+ * The target kinds a new custom URL may take.
222
+ *
223
+ * `archive` is absent: hand-typing `format=note&title=none` into a text field
224
+ * is the problem smart collections exist to replace, and a form that stopped
225
+ * offering it while the endpoint still accepted it would not have stopped
226
+ * offering it. Existing archive paths keep working indefinitely.
227
+ */
228
+ export const CreatableCustomUrlTargetTypeSchema = z.enum([
229
+ "post",
230
+ "collection",
231
+ "redirect",
232
+ ]);
233
+
184
234
  /**
185
235
  * Rating schema (1-5 integer)
186
236
  */
@@ -193,9 +243,24 @@ export const RatingSchema = z.coerce
193
243
  .or(z.literal("").transform(() => undefined))
194
244
  .transform((v) => (v === 0 ? undefined : v));
195
245
 
246
+ /**
247
+ * Any syntactically valid BCP 47 tag, normalized to canonical case.
248
+ *
249
+ * Deliberately open: Jant ships dashboard catalogs for a handful of locales but
250
+ * content can be written in any language.
251
+ */
252
+ export const ContentLanguageSchema = z
253
+ .string()
254
+ .trim()
255
+ .refine(isValidContentLanguage, {
256
+ message: "Enter a valid BCP 47 language tag (e.g. en, zh-Hans, ja).",
257
+ })
258
+ .transform(normalizeContentLanguage);
259
+
196
260
  const PostIdSchema = createTypeIdSchema(ID_PREFIX.post);
197
261
  const MediaIdSchema = createTypeIdSchema(ID_PREFIX.media);
198
262
  const CollectionIdSchema = createTypeIdSchema(ID_PREFIX.collection);
263
+ const SmartCollectionIdSchema = createTypeIdSchema(ID_PREFIX.smartCollection);
199
264
  const CollectionDirectoryItemIdSchema = createTypeIdSchema(
200
265
  ID_PREFIX.collectionDirectoryItem,
201
266
  );
@@ -294,6 +359,12 @@ const PostFieldsSchema = z.object({
294
359
  .optional(),
295
360
  replyToId: PostIdSchema.optional(),
296
361
  quietReply: z.boolean().optional(),
362
+ // Content language. The post service normalizes and enforces the
363
+ // thread-uniform rule; this only rejects tags that are not BCP 47 at all.
364
+ language: ContentLanguageSchema.optional().or(
365
+ z.literal("").transform(() => undefined),
366
+ ),
367
+ translationOfId: PostIdSchema.optional(),
297
368
  publishedAt: z.number().int().positive().optional(),
298
369
  mediaIds: z.array(MediaIdSchema).max(MAX_MEDIA_ATTACHMENTS).optional(),
299
370
  mediaAlts: z.record(MediaIdSchema, z.string()).optional(),
@@ -480,15 +551,32 @@ function refineSlugPathExclusivity<T extends { slug?: string; path?: string }>(
480
551
  });
481
552
  }
482
553
 
554
+ /**
555
+ * Create only: a post's own record timestamps, in Unix seconds. An import or
556
+ * a migration restores them, so "last edited" (feed `<updated>`, sitemap
557
+ * `lastmod`) and Thread order (the root, then replies by creation time, then
558
+ * ID) survive the move.
559
+ * Omitted, both are the time of the request; `updatedAt` alone defaults to
560
+ * `createdAt`.
561
+ */
562
+ const RestoredTimestampFields = {
563
+ createdAt: z.number().int().positive().optional(),
564
+ updatedAt: z.number().int().positive().optional(),
565
+ };
566
+
483
567
  /**
484
568
  * API request body schema for creating a post
485
569
  */
486
570
  export const CreatePostSchema = refineSlugPathExclusivity(
487
- refineCreatePostFormatShape(refineBodyExclusivity(PostFieldsSchema)),
571
+ refineCreatePostFormatShape(
572
+ refineBodyExclusivity(PostFieldsSchema.extend(RestoredTimestampFields)),
573
+ ),
488
574
  );
489
575
 
490
576
  export const CreatePostApiSchema = refineSlugPathExclusivity(
491
- refineCreatePostFormatShape(refineBodyExclusivity(ApiPostFieldsSchema)),
577
+ refineCreatePostFormatShape(
578
+ refineBodyExclusivity(ApiPostFieldsSchema.extend(RestoredTimestampFields)),
579
+ ),
492
580
  );
493
581
 
494
582
  /**
@@ -566,6 +654,14 @@ export const CreateNavItemSchema = z.discriminatedUnion("type", [
566
654
  label: sanitizeText(100).pipe(z.string().min(1)).optional(),
567
655
  placement: z.enum(["header", "more"]).optional(),
568
656
  }),
657
+ // Placed exactly like a collection — the label follows the target's title
658
+ // and the URL follows its address; only which column holds the key differs.
659
+ z.object({
660
+ type: z.literal("smart_collection"),
661
+ smartCollectionId: SmartCollectionIdSchema,
662
+ label: sanitizeText(100).pipe(z.string().min(1)).optional(),
663
+ placement: z.enum(["header", "more"]).optional(),
664
+ }),
569
665
  z.object({
570
666
  type: z.literal("page"),
571
667
  postId: PostIdSchema,
@@ -620,6 +716,19 @@ export const CreateCollectionDirectoryItemSchema = z.discriminatedUnion(
620
716
  ],
621
717
  );
622
718
 
719
+ /**
720
+ * A row in the collections directory, as the drag surface names it.
721
+ *
722
+ * A placed row is named by its directory row id. A collection or smart
723
+ * collection that has never been placed is named by its own id, because that is
724
+ * all the directory has to render — moving one is what gives it a row.
725
+ */
726
+ export const CollectionDirectoryRowIdSchema = z.union([
727
+ CollectionDirectoryItemIdSchema,
728
+ CollectionIdSchema,
729
+ SmartCollectionIdSchema,
730
+ ]);
731
+
623
732
  export const UpdateCollectionDirectoryItemSchema = z.object({
624
733
  label: z.union([CollectionDirectoryLabelSchema, z.null()]).optional(),
625
734
  url: CollectionDirectoryLinkUrlSchema.optional(),
@@ -633,6 +742,7 @@ export {
633
742
  NavItemIdSchema,
634
743
  PathIdSchema,
635
744
  PostIdSchema,
745
+ SmartCollectionIdSchema,
636
746
  };
637
747
 
638
748
  /**
@@ -677,6 +787,61 @@ export const CreateCollectionSchema = z.object({
677
787
  */
678
788
  export const UpdateCollectionSchema = CreateCollectionSchema.partial();
679
789
 
790
+ /**
791
+ * What a stored selection may hold — the validator a create or update request
792
+ * runs its conditions through.
793
+ *
794
+ * Assembled from the dimension registry rather than restated, so a new
795
+ * dimension is accepted by every endpoint the moment it is declared, and a
796
+ * dimension whose stored vocabulary is narrower than its URL one (`visibility`)
797
+ * is narrow everywhere at once.
798
+ *
799
+ * The registry itself carries plain predicates, not schemas, because the smart
800
+ * collection dialog compiles it into the browser bundle. The Zod wrapper lives
801
+ * here, on the server side of that line, where request bodies are validated and
802
+ * a rejected condition still has to say which one it was.
803
+ */
804
+ export const PostFilterSelectionSchema = z
805
+ .object(
806
+ Object.fromEntries(
807
+ FILTER_DIMENSION_KEYS.map((key) => [
808
+ key,
809
+ z
810
+ .custom((value) => FILTER_DIMENSIONS[key].storage.isStorable(value), {
811
+ message: `Invalid value for the ${key} condition`,
812
+ })
813
+ .optional(),
814
+ ]),
815
+ ),
816
+ )
817
+ .strict() as unknown as z.ZodType<PostFilterSelection>;
818
+
819
+ /**
820
+ * API request body schema for creating a smart collection.
821
+ *
822
+ * The conditions come from the dimension registry rather than being restated
823
+ * here, so a new dimension is accepted the moment it is declared and a stored
824
+ * vocabulary narrower than the URL one (`visibility` never being `private`)
825
+ * is narrow at every entry point at once.
826
+ */
827
+ export const CreateSmartCollectionSchema = z.object({
828
+ slug: CollectionSlugSchema,
829
+ title: CollectionTitleSchema,
830
+ description: CollectionDescriptionValueSchema.nullable().optional(),
831
+ // Omitted means no conditions, which is the honest spelling of "every post".
832
+ selection: PostFilterSelectionSchema.optional(),
833
+ sort: z.enum(SMART_COLLECTION_SORT_ORDERS).optional(),
834
+ layout: z.enum(ARCHIVE_LAYOUTS).nullable().optional(),
835
+ });
836
+
837
+ export const UpdateSmartCollectionSchema =
838
+ CreateSmartCollectionSchema.partial();
839
+
840
+ /** The body of a preview request: the conditions, and nothing else. */
841
+ export const SmartCollectionPreviewSchema = z.object({
842
+ selection: PostFilterSelectionSchema.optional(),
843
+ });
844
+
680
845
  export const SiteNameSettingSchema = sanitizeSettingText(MAX_SITE_NAME_LENGTH);
681
846
  export const SiteDescriptionSettingSchema = sanitizeSettingText(
682
847
  MAX_SITE_DESCRIPTION_LENGTH,
@@ -808,11 +973,16 @@ export function normalizeEditableSettingValue(
808
973
  );
809
974
  }
810
975
  break;
811
- case "CJK_SERIF_FONT":
812
- if (!isCjkSerifFont(normalized)) {
813
- throw new ValidationError("Choose an available CJK font fallback.");
976
+ case "MULTILINGUAL_ENABLED":
977
+ if (normalized !== "true" && normalized !== "false") {
978
+ throw new ValidationError("Multilingual content is on or off.");
814
979
  }
815
980
  break;
981
+ case "ADDITIONAL_LANGUAGES":
982
+ // Re-serialized from the parsed form so a hand-written value cannot
983
+ // store blanks, duplicates, or non-canonical casing.
984
+ normalized = formatLanguageList(parseLanguageList(normalized));
985
+ break;
816
986
  case "TIME_ZONE":
817
987
  if (!isSupportedTimeZone(normalized)) {
818
988
  throw new ValidationError("Choose a valid time zone.");
@@ -837,11 +1007,10 @@ export const CreateCustomUrlSchema = z.object({
837
1007
  "Path must contain only lowercase alphanumeric characters, hyphens, slashes, and dots",
838
1008
  )
839
1009
  .transform((p) => (p.startsWith("/") ? p : `/${p}`)),
840
- targetType: CustomUrlTargetTypeSchema,
1010
+ targetType: CreatableCustomUrlTargetTypeSchema,
841
1011
  targetId: z.string().optional(),
842
1012
  toPath: z.string().optional(),
843
1013
  redirectType: RedirectTypeSchema.optional(),
844
- archiveQuery: z.string().optional(),
845
1014
  });
846
1015
 
847
1016
  // =============================================================================
@@ -849,10 +1018,13 @@ export const CreateCustomUrlSchema = z.object({
849
1018
  // =============================================================================
850
1019
 
851
1020
  /**
852
- * Setup form validation schema
1021
+ * The first of the two screens a self-hosted site is set up through.
1022
+ *
1023
+ * Credentials and nothing else. Everything about the site itself waits for the
1024
+ * second screen, which is the same screen a hosted site shows — so the two
1025
+ * install kinds converge instead of keeping two forms that drift.
853
1026
  */
854
- export const SetupSchema = z.object({
855
- siteName: z.string().min(1, "Site name is required"),
1027
+ export const SetupAccountSchema = z.object({
856
1028
  email: z
857
1029
  .string()
858
1030
  .transform(normalizeEmail)
@@ -863,6 +1035,70 @@ export const SetupSchema = z.object({
863
1035
  .max(128),
864
1036
  });
865
1037
 
1038
+ /**
1039
+ * Setup on a site whose shell already exists and is already named.
1040
+ *
1041
+ * A control plane created the site and its owner, so the only thing left to
1042
+ * settle is the language its author writes in — the one fact no header, host,
1043
+ * or billing record can supply.
1044
+ */
1045
+ export const SetupLanguageSchema = z.object({
1046
+ contentLanguage: ContentLanguageSchema,
1047
+ /**
1048
+ * The Jant Discover answer, from the checkbox on the setup screen.
1049
+ *
1050
+ * Optional, and absence is not `false`: an older client or a scripted setup
1051
+ * sends nothing, and that has to keep meaning "leave the deployment default
1052
+ * in force" rather than "the author said no".
1053
+ */
1054
+ discover: z.boolean().optional(),
1055
+ });
1056
+
1057
+ /**
1058
+ * The second of the two self-hosted screens: the same questions the hosted
1059
+ * screen asks, plus the name only a self-hosted site is still missing.
1060
+ *
1061
+ * Which of the two applies is not a property of the form but of the site — a
1062
+ * site that already has a name is never asked for one — so the route picks the
1063
+ * schema by reading the stored name rather than trusting a field.
1064
+ */
1065
+ export const SetupSiteSchema = SetupLanguageSchema.extend({
1066
+ // The message is set twice over on purpose: `.min(1)` catches a field left
1067
+ // blank, and the type-level `error` catches a form that carried no field at
1068
+ // all. Without the second, a scripted setup is told "expected string,
1069
+ // received undefined" — which names nothing the author can act on.
1070
+ siteName: z
1071
+ .string({ error: "Site name is required" })
1072
+ .min(1, "Site name is required"),
1073
+ });
1074
+
1075
+ /** The answers the last setup screen collects from either install kind. */
1076
+ export type SetupLanguageAnswers = z.infer<typeof SetupLanguageSchema>;
1077
+
1078
+ /**
1079
+ * Both setup screens' answers at once, for `jant setup`: a self-hosted install
1080
+ * set up by its deployment rather than in a browser.
1081
+ *
1082
+ * Only the credentials are required. Everything the second screen asks has
1083
+ * the default an empty answer gets there, and a browser's guesses — its
1084
+ * language, its time zone — have no counterpart here.
1085
+ */
1086
+ export const InstanceSetupSchema = SetupAccountSchema.extend({
1087
+ siteId: createTypeIdSchema(ID_PREFIX.site).optional(),
1088
+ siteName: z
1089
+ .string()
1090
+ .trim()
1091
+ .min(1, "Site name cannot be empty")
1092
+ .max(MAX_SITE_NAME_LENGTH)
1093
+ .optional(),
1094
+ siteLanguage: ContentLanguageSchema.optional(),
1095
+ timeZone: z
1096
+ .string()
1097
+ .refine(isSupportedTimeZone, "Choose a valid time zone.")
1098
+ .transform(normalizeTimeZone)
1099
+ .optional(),
1100
+ });
1101
+
866
1102
  /**
867
1103
  * Sign-in form validation schema
868
1104
  */
@@ -29,7 +29,8 @@ export async function renderSiteHeaderHtml(c: Context<Env>): Promise<string> {
29
29
  links={navData.links}
30
30
  currentPath={navData.currentPath}
31
31
  sitePathPrefix={navData.sitePathPrefix}
32
- siteAvatarUrl={navData.siteAvatarUrl}
32
+ basePath={navData.basePath}
33
+ siteAvatarThumbUrl={navData.siteAvatarThumbUrl}
33
34
  showHeaderAvatar={navData.showHeaderAvatar}
34
35
  />
35
36
  </I18nProvider>,
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Standalone page served when a site resolves but is suspended.
3
+ *
4
+ * Kept deliberately self-contained (no theme, no site services, no i18n
5
+ * catalog): it renders before any site context exists, and the site whose
6
+ * theme we would otherwise use is exactly the one that is unavailable.
7
+ *
8
+ * The copy stays provider-neutral. Hosted deployments pass their brand across
9
+ * the boundary through `HOSTED_CONTROL_PLANE_PROVIDER_NAME` /
10
+ * `HOSTED_CONTROL_PLANE_BASE_URL`, which turn the last line into a real link
11
+ * back to the control plane; a self-hosted instance renders the same page
12
+ * without one.
13
+ */
14
+
15
+ import { getHostedControlPlaneProviderLabel } from "./env.js";
16
+ import { getHostedControlPlaneDashboardUrl } from "./hosted-signin.js";
17
+ import { escapeHtml } from "./html.js";
18
+
19
+ export function renderSiteUnavailablePage(
20
+ env: object | undefined | null,
21
+ ): string {
22
+ const providerLabel = getHostedControlPlaneProviderLabel(env);
23
+ const dashboardUrl = getHostedControlPlaneDashboardUrl(env);
24
+
25
+ const ownerLine =
26
+ dashboardUrl && providerLabel
27
+ ? `<p>If it's your site, sign in to <a href="${escapeHtml(dashboardUrl)}">${escapeHtml(providerLabel)}</a> to bring it back online.</p>`
28
+ : `<p>If it's your site, sign in to your hosting provider to bring it back online.</p>`;
29
+
30
+ return `<!DOCTYPE html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="utf-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1">
35
+ <meta name="robots" content="noindex">
36
+ <title>This site is offline</title>
37
+ <style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111;padding:24px}div{max-width:460px;text-align:left}h1{font-size:1.25rem;font-weight:600;margin:0 0 12px}p{color:#4a4a4a;line-height:1.6;margin:12px 0}a{color:#0f766e}@media(prefers-color-scheme:dark){body{background:#111;color:#fafafa}p{color:#a1a1a1}a{color:#2dd4bf}}</style>
38
+ </head>
39
+ <body>
40
+ <div>
41
+ <h1>This site is offline</h1>
42
+ <p>There's nothing to read here right now &mdash; the site's hosting has stopped.</p>
43
+ ${ownerLine}
44
+ </div>
45
+ </body>
46
+ </html>`;
47
+ }
package/src/lib/slug.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Handles three cases: user-provided slug, title-based slug, and random-only slug.
6
6
  */
7
7
 
8
- import { slugify } from "./url.js";
8
+ import { slugify } from "./slugify.js";
9
9
  import { generateRandomId } from "./nanoid.js";
10
10
  import { isReservedPath } from "./constants.js";
11
11
  import { ValidationError, ConflictError } from "./errors.js";
@@ -19,6 +19,21 @@ export interface SlugOptions {
19
19
  title?: string;
20
20
  /** Length of random IDs */
21
21
  idLength: number;
22
+ /**
23
+ * URL form of the post's language (lowercased BCP 47 tag, e.g. `zh-hant`),
24
+ * set only for posts written in a non-primary language. Used as the first
25
+ * fallback when a title-based slug collides, so the transliterations of a
26
+ * translated pair land on `shu-ping` and `shu-ping-en` rather than a random
27
+ * suffix. Omitted for primary-language posts: the bare namespace belongs to
28
+ * them, and a language suffix among same-language posts distinguishes nothing.
29
+ */
30
+ languageSuffix?: string;
31
+ /**
32
+ * URL prefixes currently served by language views. A slug matching one would
33
+ * be shadowed by that view and never resolve, so it is treated as reserved
34
+ * for as long as the language is configured.
35
+ */
36
+ reservedPrefixes?: readonly string[];
22
37
  /** Callback to check if a slug is available (checks post slugs + path_registry paths) */
23
38
  isAvailable: (slug: string) => Promise<boolean>;
24
39
  }
@@ -28,7 +43,7 @@ export interface SlugOptions {
28
43
  *
29
44
  * Resolution order:
30
45
  * 1. User-provided slug → validate format, check reserved, check availability
31
- * 2. Title exists → slugify(title), append -{randomId} if conflict
46
+ * 2. Title exists → slugify(title), then -{languageSuffix}, then -{randomId}
32
47
  * 3. No title → pure random ID
33
48
  *
34
49
  * @param opts - Slug generation options
@@ -42,16 +57,33 @@ export interface SlugOptions {
42
57
  * // Title-based
43
58
  * await generatePostSlug({ title: "Hello World", idLength: 5, isAvailable: check });
44
59
  *
60
+ * // Title-based in a secondary language, colliding with an existing slug
61
+ * await generatePostSlug({
62
+ * title: "書評",
63
+ * languageSuffix: "en",
64
+ * idLength: 5,
65
+ * isAvailable: check,
66
+ * }); // => "shu-ping-en"
67
+ *
45
68
  * // Random
46
69
  * await generatePostSlug({ idLength: 5, isAvailable: check });
47
70
  * ```
48
71
  */
49
72
  export async function generatePostSlug(opts: SlugOptions): Promise<string> {
50
- const { slug, title, idLength, isAvailable } = opts;
73
+ const {
74
+ slug,
75
+ title,
76
+ idLength,
77
+ languageSuffix,
78
+ reservedPrefixes = [],
79
+ isAvailable,
80
+ } = opts;
81
+ const isReserved = (candidate: string) =>
82
+ isReservedPath(candidate, reservedPrefixes);
51
83
 
52
84
  // Case 1: User-provided slug
53
85
  if (slug) {
54
- if (isReservedPath(slug)) {
86
+ if (isReserved(slug)) {
55
87
  throw new ValidationError(
56
88
  `Slug "${slug}" is reserved and cannot be used`,
57
89
  );
@@ -66,15 +98,27 @@ export async function generatePostSlug(opts: SlugOptions): Promise<string> {
66
98
  // Case 2: Title-based slug
67
99
  if (title) {
68
100
  const base = slugify(title);
69
- if (base && !isReservedPath(base)) {
101
+ if (base && !isReserved(base)) {
70
102
  const available = await isAvailable(base);
71
103
  if (available) return base;
104
+
105
+ // A readable language suffix beats a random one, and it is exactly the
106
+ // collision a translated pair produces.
107
+ if (languageSuffix) {
108
+ const languageCandidate = `${base}-${languageSuffix}`;
109
+ if (
110
+ !isReserved(languageCandidate) &&
111
+ (await isAvailable(languageCandidate))
112
+ ) {
113
+ return languageCandidate;
114
+ }
115
+ }
72
116
  }
73
117
 
74
118
  // Append random suffix on conflict or reserved base
75
119
  for (let i = 0; i < MAX_RETRIES; i++) {
76
120
  const candidate = `${base || generateRandomId(idLength)}-${generateRandomId(idLength)}`;
77
- if (!isReservedPath(candidate) && (await isAvailable(candidate))) {
121
+ if (!isReserved(candidate) && (await isAvailable(candidate))) {
78
122
  return candidate;
79
123
  }
80
124
  }
@@ -86,7 +130,7 @@ export async function generatePostSlug(opts: SlugOptions): Promise<string> {
86
130
  // Case 3: Pure random
87
131
  for (let i = 0; i < MAX_RETRIES; i++) {
88
132
  const candidate = generateRandomId(idLength);
89
- if (!isReservedPath(candidate) && (await isAvailable(candidate))) {
133
+ if (!isReserved(candidate) && (await isAvailable(candidate))) {
90
134
  return candidate;
91
135
  }
92
136
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Slug generation from free text.
3
+ *
4
+ * Lives apart from `url.ts` on purpose: `limax` drags the whole `pinyin-pro`
5
+ * dictionary (≈390 KB minified) into any bundle that imports it. Keeping the
6
+ * dictionary behind this single module lets the browser load it lazily via
7
+ * `client/lazy-slugify.ts`, while `url.ts` stays cheap enough for the public
8
+ * bundle. The build guard in `vite.shared.ts` fails the client build if
9
+ * `pinyin-pro` ever lands in an entry chunk again.
10
+ */
11
+
12
+ import limax from "limax";
13
+
14
+ import {
15
+ HAN_RE,
16
+ HANGUL_RE,
17
+ KANA_RE,
18
+ hangulToLatin,
19
+ kanaToRomaji,
20
+ } from "./translit.js";
21
+
22
+ const HAN_GLOBAL_RE = /\p{Script=Han}/gu;
23
+ const CONTENT_CHAR_RE = /[\p{L}\p{N}]/u;
24
+ const ASCII_ALNUM_RE = /[a-zA-Z0-9]/;
25
+
26
+ /**
27
+ * Minimum fraction of a title's letters/digits the transliteration pipeline
28
+ * must handle for the result to be usable. Below this, too much meaning is
29
+ * lost (e.g. mixed kanji+kana Japanese where the semantic core is kanji, or
30
+ * scripts limax drops entirely) and `slugify` returns "" so callers fall back
31
+ * to their random-ID path instead of shipping a garbled slug.
32
+ */
33
+ const SLUG_COVERAGE_THRESHOLD = 0.75;
34
+
35
+ /** Per-character limax probe cache: does this char transliterate to anything? */
36
+ const charCoverageCache = new Map<string, boolean>();
37
+
38
+ function isCharCovered(char: string): boolean {
39
+ if (ASCII_ALNUM_RE.test(char)) return true;
40
+ let covered = charCoverageCache.get(char);
41
+ if (covered === undefined) {
42
+ covered = limax(char, { tone: false }).length > 0;
43
+ charCoverageCache.set(char, covered);
44
+ }
45
+ return covered;
46
+ }
47
+
48
+ function slugCoverage(text: string, japanese: boolean): number {
49
+ let content = 0;
50
+ let covered = 0;
51
+ for (const char of text) {
52
+ if (!CONTENT_CHAR_RE.test(char)) continue;
53
+ content++;
54
+ if (KANA_RE.test(char) || HANGUL_RE.test(char)) {
55
+ covered++;
56
+ } else if (japanese && HAN_RE.test(char)) {
57
+ // Kanji in a kana-containing title is dropped, never pinyin-read.
58
+ } else if (isCharCovered(char)) {
59
+ covered++;
60
+ }
61
+ }
62
+ return content === 0 ? 0 : covered / content;
63
+ }
64
+
65
+ /**
66
+ * Converts text to a URL-friendly slug.
67
+ *
68
+ * Transliterates i18n text to Latin before slugifying: Korean hangul via
69
+ * Revised Romanization, Japanese kana via Hepburn romaji (both in-house, see
70
+ * `translit.ts`), everything else (Han → pinyin, Cyrillic, accents, …) via
71
+ * limax. When the title contains kana it is treated as Japanese and Han
72
+ * characters are dropped rather than misread as Chinese pinyin; Han without
73
+ * kana is treated as Chinese.
74
+ *
75
+ * Returns `""` when transliteration would lose too much of the title (see
76
+ * `SLUG_COVERAGE_THRESHOLD`) — every caller treats an empty base as "no
77
+ * usable title" and falls back to a random ID, which beats publishing a
78
+ * garbled slug.
79
+ *
80
+ * @param text - The text to convert to a slug
81
+ * @returns The slugified string, or `""` when no usable slug can be derived
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * slugify("Hello World! This is a Test.");
86
+ * // Returns: "hello-world-this-is-a-test"
87
+ *
88
+ * slugify("书评");
89
+ * // Returns: "shu-ping"
90
+ *
91
+ * slugify("안녕하세요 세계");
92
+ * // Returns: "annyeonghaseyo-segye"
93
+ *
94
+ * slugify("ハンバーガー");
95
+ * // Returns: "hanbaga"
96
+ *
97
+ * slugify("日本語のタイトルです");
98
+ * // Returns: "" (kanji carries the meaning and can't be transliterated)
99
+ * ```
100
+ */
101
+ export function slugify(text: string): string {
102
+ const japanese = KANA_RE.test(text);
103
+ let pre = text;
104
+ if (japanese) {
105
+ pre = kanaToRomaji(pre.replace(HAN_GLOBAL_RE, " "));
106
+ }
107
+ if (HANGUL_RE.test(pre)) {
108
+ pre = hangulToLatin(pre);
109
+ }
110
+ const base = limax(pre, { tone: false }).replace(/_/g, "-");
111
+ if (!base) return "";
112
+ return slugCoverage(text, japanese) >= SLUG_COVERAGE_THRESHOLD ? base : "";
113
+ }