@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
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Jant Discover
3
+ *
4
+ * Discover is a public directory of Jant blogs: a crawler polls the Atom
5
+ * feeds of sites that opt in and lists their posts — the ones marked
6
+ * Featured on its home, link and quote posts on lists of their own. Core owns
7
+ * the protocol, not the directory — this module holds the site setting's
8
+ * effective-mode rules and the identifiers the feed declaration is built
9
+ * from. The declaration is specified in `docs/feeds.md`; how a directory
10
+ * behaves is that directory's own business, documented where it lives.
11
+ *
12
+ * `featured` survives as a stored value: older releases offered it as a
13
+ * choice, and a site that made it still declares it, which a directory reads
14
+ * as "only this feed". Nothing writes it any more.
15
+ */
16
+
17
+ /**
18
+ * XML namespace for Jant's own feed extensions.
19
+ *
20
+ * A permanent identifier, not an environment-dependent URL: a feed served
21
+ * from any host declares this exact URI, the same policy published markdown
22
+ * links follow. Feed readers ignore elements in namespaces they do not know,
23
+ * so subscribers see nothing.
24
+ */
25
+ export const DISCOVER_NAMESPACE_URI = "https://jant.me/ns";
26
+
27
+ /** Stored value of the `DISCOVER` site setting. */
28
+ export type DiscoverSetting = "latest" | "featured" | "off";
29
+
30
+ /**
31
+ * Effective Discover mode, as declared in the feed.
32
+ *
33
+ * `none` rather than `off` because it is what a crawler reads, and the
34
+ * element's absence already means "this site predates Discover" — the two
35
+ * states are different and must stay tellable apart.
36
+ */
37
+ export type DiscoverMode = "latest" | "featured" | "none";
38
+
39
+ const DISCOVER_SETTINGS: readonly DiscoverSetting[] = [
40
+ "latest",
41
+ "featured",
42
+ "off",
43
+ ];
44
+
45
+ /**
46
+ * Parse a stored or environment-supplied Discover setting.
47
+ *
48
+ * @param raw - Raw setting value, from the DB or an environment binding
49
+ * @returns The setting, or `null` when absent or unrecognized
50
+ * @example
51
+ * ```ts
52
+ * parseDiscoverSetting("featured"); // "featured"
53
+ * parseDiscoverSetting("yes"); // null
54
+ * ```
55
+ */
56
+ export function parseDiscoverSetting(
57
+ raw: string | undefined | null,
58
+ ): DiscoverSetting | null {
59
+ const value = raw?.trim();
60
+ if (!value) return null;
61
+ return DISCOVER_SETTINGS.includes(value as DiscoverSetting)
62
+ ? (value as DiscoverSetting)
63
+ : null;
64
+ }
65
+
66
+ /**
67
+ * Derive the effective Discover mode from the site's configuration.
68
+ *
69
+ * The rules, in the order they are applied:
70
+ *
71
+ * 1. A demo site is never listed. Demos exist to be thrown away, and the
72
+ * same lock already applies to search indexing.
73
+ * 2. A site with feeds turned off has nothing to poll — every feed path
74
+ * 404s — so it cannot honestly declare that it is listed.
75
+ * 3. The owner's stored choice wins over anything derived, including
76
+ * `noindex`. Someone who ticked the box meant it.
77
+ * 4. Without a stored choice, `noindex` implies `none`: hiding from search
78
+ * engines and being surfaced by a directory contradict each other, and the
79
+ * quieter reading is the safe one.
80
+ * 5. Then the deployment default, from the `DISCOVER` binding. Hosted Jant
81
+ * sets it, so every blog it serves takes part until its owner says
82
+ * otherwise. It sits below `noindex` on purpose: a deployment saying
83
+ * "list my sites" is a default, not the answer of the person who hid this
84
+ * one from search engines.
85
+ * 6. Otherwise `none`. A site nobody configured has not opted in, and the
86
+ * declaration is the consent record a directory reads — a site listed on
87
+ * the strength of a default its owner never chose was never asked.
88
+ *
89
+ * @param input - Resolved site configuration relevant to Discover
90
+ * @returns The mode this site's feeds should declare
91
+ * @example
92
+ * ```ts
93
+ * resolveDiscoverMode({
94
+ * storedValue: null, defaultValue: null,
95
+ * demoMode: false, noindex: false, rssFeedsEnabled: true,
96
+ * }); // "none" — self-hosted, never opted in
97
+ * resolveDiscoverMode({
98
+ * storedValue: null, defaultValue: "latest",
99
+ * demoMode: false, noindex: false, rssFeedsEnabled: true,
100
+ * }); // "latest" — a deployment that lists its sites by default
101
+ * ```
102
+ */
103
+ export function resolveDiscoverMode(input: {
104
+ /** The owner's own choice, stored in this site's settings. */
105
+ storedValue: string | undefined | null;
106
+ /**
107
+ * What this deployment lists by default, from the `DISCOVER` binding.
108
+ *
109
+ * Absent for a self-hosted site, which is what makes Discover opt-in
110
+ * there; hosted Jant sets it so its fleet is listed without every owner
111
+ * having to ask.
112
+ */
113
+ defaultValue?: string | undefined | null;
114
+ demoMode: boolean;
115
+ /** Effective `noindex`, as resolved onto `AppConfig`. */
116
+ noindex: boolean;
117
+ rssFeedsEnabled: boolean;
118
+ }): DiscoverMode {
119
+ if (input.demoMode) return "none";
120
+ if (!input.rssFeedsEnabled) return "none";
121
+
122
+ const stored = parseDiscoverSetting(input.storedValue);
123
+ if (stored) return stored === "off" ? "none" : stored;
124
+
125
+ if (input.noindex) return "none";
126
+
127
+ const fallback = parseDiscoverSetting(input.defaultValue);
128
+ if (fallback) return fallback === "off" ? "none" : fallback;
129
+
130
+ return "none";
131
+ }
132
+
133
+ /**
134
+ * Site-relative path of the feed a Discover crawler should poll.
135
+ *
136
+ * The declaration names the feed to poll rather than leaving a crawler to
137
+ * guess, so a site that lists only its featured posts is polled at
138
+ * `/featured/feed` and never at `/latest/feed`.
139
+ *
140
+ * @param mode - Effective Discover mode
141
+ * @returns Site-relative feed path, or `null` when the site is not listed
142
+ * @example
143
+ * ```ts
144
+ * getDiscoverFeedPath("featured"); // "/featured/feed"
145
+ * getDiscoverFeedPath("none"); // null
146
+ * ```
147
+ */
148
+ export function getDiscoverFeedPath(mode: DiscoverMode): string | null {
149
+ switch (mode) {
150
+ case "latest":
151
+ return "/latest/feed";
152
+ case "featured":
153
+ return "/featured/feed";
154
+ case "none":
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Site-relative path of the endpoint a directory asks about posts it holds.
161
+ *
162
+ * A feed shows a site's newest posts, so a post missing from it may have been
163
+ * taken out or may only have been pushed past the feed's length — and the
164
+ * permalink cannot settle it either, because a post hidden from Latest or
165
+ * unfeatured is still a live page. This answers the question outright: for
166
+ * each post, whether it is in the Latest feed and whether its Thread is in the
167
+ * featured feed. The declaration names it in its `status` attribute, so a
168
+ * directory never builds the address itself.
169
+ */
170
+ export const DISCOVER_STATUS_PATH = "/api/discover/posts";
171
+
172
+ /** Most post ids one status request may ask about. */
173
+ export const DISCOVER_STATUS_MAX_IDS = 50;
174
+
175
+ /**
176
+ * Public posts a blog needs before the jant.me directory will list it.
177
+ *
178
+ * Not core's rule — the directory's, published on the Discover page — but
179
+ * core states it so a site owner can see where they stand without asking
180
+ * anybody. A directory of your own may decide differently.
181
+ *
182
+ * One post, and no minimum history at all. The directory used to ask for a
183
+ * week of it as well, and dropped that: for a self-hosted blog the age can
184
+ * only be read off the oldest post the feed still carries, which is strict
185
+ * against an honest blog and no obstacle whatsoever to one that backdates.
186
+ * What keeps day-one throwaways out is the opt-in itself.
187
+ */
188
+ export const DISCOVER_MIN_PUBLIC_POSTS = 1;
189
+
190
+ /** Longest a directory waits before reading a newly announced feed. */
191
+ export const DISCOVER_FIRST_READ_MAX_HOURS = 6;
192
+
193
+ /**
194
+ * How long the jant.me directory holds a post it has read before showing it.
195
+ *
196
+ * The directory's rule — its `PUBLIC_DELAY_SECONDS` — stated here for the same
197
+ * reason as `DISCOVER_MIN_PUBLIC_POSTS`: the help line under the Discover
198
+ * checkbox tells an author how long they have to edit a post before it shows.
199
+ * A tuning value on the directory's side; this moves with it.
200
+ */
201
+ export const DISCOVER_PUBLIC_DELAY_HOURS = 24;
202
+
203
+ /**
204
+ * The directory's names for its two format lists.
205
+ *
206
+ * Proper names that stay in English in every locale, as the directory's own
207
+ * tabs spell them. They are placeholder values rather than catalog entries: a
208
+ * catalog entry for a word that must not be translated is one a later
209
+ * translation pass can quietly "fix", and the help line links each list by
210
+ * finding this exact run of text.
211
+ */
212
+ export const DISCOVER_LIST_NAMES = {
213
+ links: "Links",
214
+ quotes: "Quotes",
215
+ } as const;
216
+
217
+ /** Where a site stands against the directory's threshold. */
218
+ export interface DiscoverMaturity {
219
+ publicPostCount: number;
220
+ /** The threshold is met. */
221
+ established: boolean;
222
+ }
223
+
224
+ /**
225
+ * Measure a site against the directory's threshold.
226
+ *
227
+ * @param input - The site's own public-post evidence
228
+ * @returns The count to show, and whether the threshold is met
229
+ * @example
230
+ * ```ts
231
+ * measureDiscoverMaturity({ publicPostCount: 5 });
232
+ * // { publicPostCount: 5, established: true }
233
+ * ```
234
+ */
235
+ export function measureDiscoverMaturity(input: {
236
+ publicPostCount: number;
237
+ }): DiscoverMaturity {
238
+ return {
239
+ publicPostCount: input.publicPostCount,
240
+ established: input.publicPostCount >= DISCOVER_MIN_PUBLIC_POSTS,
241
+ };
242
+ }
243
+
244
+ /** The directory's public pages that the Discover help line links to. */
245
+ export interface DiscoverPageUrls {
246
+ /** The directory's home: the posts blogs have marked Featured. */
247
+ home: string;
248
+ /** Every link post, under the list name `Links`. */
249
+ links: string;
250
+ /** Every quote post, under the list name `Quotes`. */
251
+ quotes: string;
252
+ /** How blogs are listed: joining, review, and what takes a blog off. */
253
+ rules: string;
254
+ }
255
+
256
+ /**
257
+ * The directory's public pages, derived from its base address.
258
+ *
259
+ * What "Jant Discover" means is best answered by the list itself, so the name
260
+ * links to the home rather than to a page describing it; the rules page is
261
+ * linked separately, for an author asking how their own posts get there.
262
+ * Derived for the same reason as the submission form: a site announcing to a
263
+ * directory of its own must link to that one, not to jant.me. The paths are
264
+ * jant.me's, where the three lists sit flat and the rules sit under
265
+ * `/discover`.
266
+ *
267
+ * @param directoryBaseUrl - The directory this deployment belongs to, from
268
+ * `getDiscoverDirectoryBaseUrl`
269
+ * @returns Absolute URLs of the pages, or `null` when there is no directory
270
+ * @example
271
+ * ```ts
272
+ * getDiscoverPageUrls("https://jant.me/");
273
+ * // {
274
+ * // home: "https://jant.me/discover",
275
+ * // links: "https://jant.me/links",
276
+ * // quotes: "https://jant.me/quotes",
277
+ * // rules: "https://jant.me/discover/about",
278
+ * // }
279
+ * ```
280
+ */
281
+ export function getDiscoverPageUrls(
282
+ directoryBaseUrl: string | undefined | null,
283
+ ): DiscoverPageUrls | null {
284
+ if (!directoryBaseUrl) return null;
285
+ try {
286
+ const at = (path: string) => new URL(path, directoryBaseUrl).toString();
287
+ return {
288
+ home: at("/discover"),
289
+ links: at("/links"),
290
+ quotes: at("/quotes"),
291
+ rules: at("/discover/about"),
292
+ };
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * The directory's manual submission form, derived from its base address.
300
+ *
301
+ * Never hardcoded: a site announcing to a directory of its own must not be
302
+ * sent to somebody else's form. Both paths are part of the same directory, so
303
+ * one address is enough to find the other.
304
+ *
305
+ * @param directoryBaseUrl - The directory this deployment belongs to, from
306
+ * `getDiscoverDirectoryBaseUrl`
307
+ * @returns Absolute URL of the form, or `null` when there is no directory
308
+ * @example
309
+ * ```ts
310
+ * getDiscoverSubmitUrl("https://jant.me/");
311
+ * // "https://jant.me/discover/submit"
312
+ * ```
313
+ */
314
+ export function getDiscoverSubmitUrl(
315
+ directoryBaseUrl: string | undefined | null,
316
+ ): string | null {
317
+ if (!directoryBaseUrl) return null;
318
+ try {
319
+ return new URL("/discover/submit", directoryBaseUrl).toString();
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
324
+
325
+ /** One run of a sentence, and where it links when it is a link. */
326
+ export interface TextRun {
327
+ text: string;
328
+ href?: string;
329
+ }
330
+
331
+ /**
332
+ * Split a sentence into runs, making each given term a link.
333
+ *
334
+ * The Discover help line reads as one sentence in every locale, so each link is
335
+ * found by searching the translated line for its translated term rather than by
336
+ * gluing fragments together. A term the translation drops or rewrites stays
337
+ * plain text — a sentence short one link, never a broken one. Each term links
338
+ * once, at its first occurrence no other link has claimed; longer terms are
339
+ * placed first, so a term inside another cannot take its place.
340
+ *
341
+ * @param text - The translated sentence
342
+ * @param links - Each run of text to link, and where it goes
343
+ * @returns The whole sentence as consecutive runs, in order
344
+ * @example
345
+ * ```ts
346
+ * linkTerms("See Links and Quotes.", [
347
+ * { term: "Links", href: "/links" },
348
+ * { term: "Quotes", href: "/quotes" },
349
+ * ]);
350
+ * // [
351
+ * // { text: "See " },
352
+ * // { text: "Links", href: "/links" },
353
+ * // { text: " and " },
354
+ * // { text: "Quotes", href: "/quotes" },
355
+ * // { text: "." },
356
+ * // ]
357
+ * ```
358
+ */
359
+ export function linkTerms(
360
+ text: string,
361
+ links: readonly { term: string; href: string }[],
362
+ ): TextRun[] {
363
+ const claimed: { start: number; end: number; href: string }[] = [];
364
+ const ordered = links
365
+ .filter((link) => link.term && link.href)
366
+ .sort((a, b) => b.term.length - a.term.length);
367
+
368
+ for (const { term, href } of ordered) {
369
+ for (
370
+ let start = text.indexOf(term);
371
+ start !== -1;
372
+ start = text.indexOf(term, start + 1)
373
+ ) {
374
+ const end = start + term.length;
375
+ if (claimed.some((run) => start < run.end && run.start < end)) continue;
376
+ claimed.push({ start, end, href });
377
+ break;
378
+ }
379
+ }
380
+
381
+ claimed.sort((a, b) => a.start - b.start);
382
+ const runs: TextRun[] = [];
383
+ let at = 0;
384
+ for (const { start, end, href } of claimed) {
385
+ if (start > at) runs.push({ text: text.slice(at, start) });
386
+ runs.push({ text: text.slice(start, end), href });
387
+ at = end;
388
+ }
389
+ if (at < text.length) runs.push({ text: text.slice(at) });
390
+ return runs;
391
+ }
392
+
393
+ /**
394
+ * The Discover help line as runs, each page it names linking to that page.
395
+ *
396
+ * The directory's name goes to its home, the two list names to the lists, and
397
+ * the rules to the page of rules. What Discover is, is best answered by the
398
+ * list itself, which is why the name links there rather than to a page about
399
+ * it, and why every link sits in the sentence that explains the list rather
400
+ * than on the checkbox label. The settings page and the setup screen both
401
+ * render their line through here, so the two can never link different words;
402
+ * setup's line is the settings line's opening sentence, so only the name is
403
+ * there to find.
404
+ *
405
+ * @param intro - The translated help line
406
+ * @param terms - The translated directory and rules-page names, as `intro`
407
+ * carries them
408
+ * @param pages - The directory's pages, or `null` when there is no directory
409
+ * @returns The line as runs; a single plain run when there is nothing to link to
410
+ * @example
411
+ * ```ts
412
+ * discoverIntroRuns(
413
+ * "Jant Discover is a directory. See the Discover community rules.",
414
+ * { name: "Jant Discover", rules: "Discover community rules" },
415
+ * getDiscoverPageUrls("https://jant.me/"),
416
+ * );
417
+ * // [
418
+ * // { text: "Jant Discover", href: "https://jant.me/discover" },
419
+ * // { text: " is a directory. See the " },
420
+ * // { text: "Discover community rules", href: "https://jant.me/discover/about" },
421
+ * // { text: "." },
422
+ * // ]
423
+ * ```
424
+ */
425
+ export function discoverIntroRuns(
426
+ intro: string,
427
+ terms: { name: string; rules: string },
428
+ pages: DiscoverPageUrls | null,
429
+ ): TextRun[] {
430
+ if (!pages) return [{ text: intro }];
431
+ return linkTerms(intro, [
432
+ { term: terms.name, href: pages.home },
433
+ { term: DISCOVER_LIST_NAMES.links, href: pages.links },
434
+ { term: DISCOVER_LIST_NAMES.quotes, href: pages.quotes },
435
+ { term: terms.rules, href: pages.rules },
436
+ ]);
437
+ }
package/src/lib/env.ts CHANGED
@@ -5,6 +5,21 @@ type EnvSource = object | undefined | null;
5
5
 
6
6
  export const DEFAULT_APP_PORT = 3000;
7
7
 
8
+ /** Jant's own directory, for a deployment that belongs to no other. */
9
+ const DEFAULT_DISCOVER_DIRECTORY_URL = "https://jant.me";
10
+
11
+ /**
12
+ * Where a directory receives announcements.
13
+ *
14
+ * The same path at every directory, which is what lets one base address stand
15
+ * for the whole of it — and a long-lived public contract at Jant's own:
16
+ * shipped self-hosted versions POST here for years, so it never moves.
17
+ */
18
+ const DISCOVER_PING_PATH = "/api/discover/ping";
19
+
20
+ /** Where a deployment announces when nothing else names a directory. */
21
+ export const DEFAULT_DISCOVER_PING_URL = `${DEFAULT_DISCOVER_DIRECTORY_URL}${DISCOVER_PING_PATH}`;
22
+
8
23
  function toEnvRecord(env: EnvSource): Record<string, unknown> {
9
24
  return (env ?? {}) as Record<string, unknown>;
10
25
  }
@@ -200,6 +215,125 @@ export function getHostedControlPlaneInternalToken(
200
215
  return getEnvString(env, "HOSTED_CONTROL_PLANE_INTERNAL_TOKEN");
201
216
  }
202
217
 
218
+ /**
219
+ * Which directory this deployment belongs to, resolved against one base.
220
+ *
221
+ * `DISCOVER_PING_URL` is read presence-aware rather than through
222
+ * `getEnvString`, because the two states that matter are spelled differently:
223
+ * an **absent** binding means "work it out", while a binding set to the
224
+ * **empty string** means "announce nowhere". `getEnvString` collapses both
225
+ * into undefined, which would make the documented way of switching the ping
226
+ * off silently fall back to switching it on.
227
+ *
228
+ * With nothing configured, a deployment that hosts blogs belongs to its own
229
+ * control plane's directory. Naming that twice is what let the two drift: a
230
+ * hosted deployment pointed at one control plane announced its blogs to
231
+ * jant.me, which knows nothing about them, and the failure was recorded where
232
+ * nobody reads it.
233
+ *
234
+ * @param env - Worker bindings or `process.env`
235
+ * @param controlPlaneBaseUrl - The control plane to fall back to, public or
236
+ * internal depending on who is asking
237
+ * @returns A URL on the directory's origin, or undefined when there is none
238
+ */
239
+ function resolveDiscoverDirectory(
240
+ env: EnvSource,
241
+ controlPlaneBaseUrl: string | undefined,
242
+ ): string | undefined {
243
+ const record = toEnvRecord(env);
244
+ if (Object.hasOwn(record, "DISCOVER_PING_URL")) {
245
+ const configured = normalizeEnvScalar(record["DISCOVER_PING_URL"]);
246
+ return configured?.trim() || undefined;
247
+ }
248
+
249
+ if (controlPlaneBaseUrl) {
250
+ try {
251
+ return new URL(DISCOVER_PING_PATH, controlPlaneBaseUrl).toString();
252
+ } catch {
253
+ // A base URL this malformed fails louder elsewhere; announcing to Jant's
254
+ // own directory is a better answer here than taking a settings save down.
255
+ }
256
+ }
257
+
258
+ return DEFAULT_DISCOVER_PING_URL;
259
+ }
260
+
261
+ /**
262
+ * Where a site announces itself when its owner turns Jant Discover on.
263
+ *
264
+ * Server-to-server, so a hosted deployment reaches its control plane by the
265
+ * internal address like every other core→cloud call — the ping carries no
266
+ * secret, but the internal route is the one that works without depending on
267
+ * the public hostname resolving and its certificate being trusted from inside.
268
+ *
269
+ * @param env - Worker bindings or `process.env`
270
+ * @returns The endpoint to announce to, or undefined when announcing is off
271
+ * @example
272
+ * ```ts
273
+ * getDiscoverPingUrl({}); // the default directory
274
+ * getDiscoverPingUrl({ DISCOVER_PING_URL: "" }); // undefined — off
275
+ * ```
276
+ */
277
+ export function getDiscoverPingUrl(env: EnvSource): string | undefined {
278
+ return resolveDiscoverDirectory(
279
+ env,
280
+ getHostedControlPlaneInternalBaseUrl(env),
281
+ );
282
+ }
283
+
284
+ /**
285
+ * The directory a site owner is sent to, for the links on the settings page.
286
+ *
287
+ * The public counterpart of `getDiscoverPingUrl`: same directory, the address
288
+ * a browser can open. Both derive from the same configuration, so the page can
289
+ * never link to one directory while announcing to another.
290
+ *
291
+ * @param env - Worker bindings or `process.env`
292
+ * @returns The directory's base URL, or undefined when there is no directory
293
+ * @example
294
+ * ```ts
295
+ * getDiscoverDirectoryBaseUrl({}); // "https://jant.me/"
296
+ * getDiscoverDirectoryBaseUrl({ DISCOVER_PING_URL: "" }); // undefined
297
+ * ```
298
+ */
299
+ export function getDiscoverDirectoryBaseUrl(
300
+ env: EnvSource,
301
+ ): string | undefined {
302
+ const resolved = resolveDiscoverDirectory(
303
+ env,
304
+ getHostedControlPlaneBaseUrl(env),
305
+ );
306
+ if (!resolved) return undefined;
307
+ try {
308
+ return new URL("/", resolved).toString();
309
+ } catch {
310
+ return undefined;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * What this deployment lists in Jant Discover by default.
316
+ *
317
+ * Absent for a self-hosted site, and that is the point: with nothing stored
318
+ * and nothing configured, a site declares `none` and stays out of every
319
+ * directory until its owner turns it on. Hosted Jant sets `DISCOVER=latest`,
320
+ * so the blogs it serves are listed without each owner having to ask.
321
+ *
322
+ * A default, not a choice — the site's own stored setting overrides it, and
323
+ * so does `noindex`. See `resolveDiscoverMode`.
324
+ *
325
+ * @param env - Worker bindings or `process.env`
326
+ * @returns The raw binding value, for `resolveDiscoverMode` to parse
327
+ * @example
328
+ * ```ts
329
+ * getDiscoverDefault({}); // undefined — opt-in
330
+ * getDiscoverDefault({ DISCOVER: "latest" }); // "latest"
331
+ * ```
332
+ */
333
+ export function getDiscoverDefault(env: EnvSource): string | undefined {
334
+ return getEnvString(env, "DISCOVER");
335
+ }
336
+
203
337
  export function getStorageDriverEnv(env: EnvSource): string | undefined {
204
338
  return getEnvString(env, "STORAGE_DRIVER");
205
339
  }
package/src/lib/errors.ts CHANGED
@@ -60,6 +60,24 @@ export class ConflictError extends DomainError {
60
60
  }
61
61
  }
62
62
 
63
+ /**
64
+ * A content language cannot be dropped while posts still carry it — 409.
65
+ *
66
+ * Thrown when removing a language, and when turning multilingual on with a
67
+ * language list that leaves stamped posts behind. Carries the language and
68
+ * post count so the settings route can compose a localized message; the
69
+ * plain-English `message` serves API consumers.
70
+ */
71
+ export class LanguageInUseError extends DomainError {
72
+ constructor(
73
+ message: string,
74
+ public readonly language: string,
75
+ public readonly postCount: number,
76
+ ) {
77
+ super(message, 409, "LANGUAGE_IN_USE");
78
+ }
79
+ }
80
+
63
81
  /** Hosted media quota exceeded — 409 */
64
82
  export class MediaQuotaExceededError extends DomainError {
65
83
  constructor(
@@ -90,6 +108,21 @@ export class ConfigurationError extends DomainError {
90
108
  }
91
109
  }
92
110
 
111
+ /**
112
+ * The requested site exists but is not being served — 503.
113
+ *
114
+ * Distinct from `NotFoundError` on purpose: a suspended site has a real
115
+ * hostname and real data behind it, and it can come back. 404 tells both the
116
+ * visitor and search engines that nothing was ever here, so a suspended site
117
+ * would quietly lose its index entries during a window it can still be
118
+ * restored from.
119
+ */
120
+ export class SiteUnavailableError extends DomainError {
121
+ constructor(message = "This site is not being served right now.") {
122
+ super(message, 503, "SITE_UNAVAILABLE");
123
+ }
124
+ }
125
+
93
126
  // =============================================================================
94
127
  // Route Helpers
95
128
  // =============================================================================