@jant/core 0.6.14 → 0.6.15

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 (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -42,15 +42,19 @@ function resolve(
42
42
  const envKeys = "envKeys" in field ? field.envKeys : undefined;
43
43
 
44
44
  // User-configurable: DB > ENV > Default
45
- if (!field.envOnly) {
46
- const dbValue = allSettings[key];
47
- if (dbValue) return dbValue;
45
+ if (!field.envOnly && Object.hasOwn(allSettings, key)) {
46
+ return allSettings[key] ?? "";
48
47
  }
49
48
 
50
49
  // ENV > Default
51
50
  const envValue = getEnvString(env, ...(envKeys ?? []));
52
51
  if (envValue) return envValue;
53
52
 
53
+ if (field.defaultValue) return field.defaultValue;
54
+ if ("fallbackKey" in field && field.fallbackKey) {
55
+ return resolve(field.fallbackKey, allSettings, env);
56
+ }
57
+
54
58
  return field.defaultValue;
55
59
  }
56
60
 
@@ -70,12 +74,73 @@ function resolveFallback(key: string, env: Bindings): string {
70
74
  const envValue = getEnvString(env, ...(envKeys ?? []));
71
75
  if (envValue) return envValue;
72
76
 
77
+ if (field.defaultValue) return field.defaultValue;
78
+ if ("fallbackKey" in field && field.fallbackKey) {
79
+ return resolveFallback(field.fallbackKey, env);
80
+ }
81
+
73
82
  return field.defaultValue;
74
83
  }
75
84
 
76
- function parseConfigInt(value: string, fallback: number): number {
77
- const parsed = Number.parseInt(value, 10);
78
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
85
+ type RuntimeNumberConfigKey =
86
+ | "PAGE_SIZE"
87
+ | "SEARCH_PAGE_SIZE"
88
+ | "ARCHIVE_PAGE_SIZE"
89
+ | "SUMMARY_MAX_PARAGRAPHS"
90
+ | "SUMMARY_MAX_CHARS"
91
+ | "RSS_FEED_LIMIT"
92
+ | "RSS_PUBLISH_DELAY_SECONDS";
93
+
94
+ function parseConfigInt(
95
+ key: RuntimeNumberConfigKey,
96
+ value: string,
97
+ fallback: number,
98
+ ): number {
99
+ const normalized = value.trim();
100
+ const parsed = Number(normalized);
101
+ const definition = CONFIG_FIELDS[key].editor;
102
+ return normalized.length > 0 &&
103
+ Number.isInteger(parsed) &&
104
+ (definition.min === undefined || parsed >= definition.min) &&
105
+ (definition.max === undefined || parsed <= definition.max)
106
+ ? parsed
107
+ : fallback;
108
+ }
109
+
110
+ /**
111
+ * Resolve the runtime limits used for automatic post summaries.
112
+ *
113
+ * Internal maintenance routes run before the full config middleware, so they
114
+ * use this focused resolver instead of assuming `c.var.appConfig` exists.
115
+ *
116
+ * @param env - Runtime bindings.
117
+ * @param allSettings - DB settings map when available.
118
+ * @returns Positive summary extraction limits with configured defaults.
119
+ * @example
120
+ * ```ts
121
+ * resolveSummaryConfig({ SUMMARY_MAX_CHARS: "240" }).maxChars;
122
+ * // 240
123
+ * ```
124
+ */
125
+ export function resolveSummaryConfig(
126
+ env: Bindings,
127
+ allSettings: Record<string, string> = {},
128
+ ): {
129
+ maxParagraphs: number;
130
+ maxChars: number;
131
+ } {
132
+ return {
133
+ maxParagraphs: parseConfigInt(
134
+ "SUMMARY_MAX_PARAGRAPHS",
135
+ resolve("SUMMARY_MAX_PARAGRAPHS", allSettings, env),
136
+ 5,
137
+ ),
138
+ maxChars: parseConfigInt(
139
+ "SUMMARY_MAX_CHARS",
140
+ resolve("SUMMARY_MAX_CHARS", allSettings, env),
141
+ 500,
142
+ ),
143
+ };
79
144
  }
80
145
 
81
146
  function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
@@ -102,12 +167,19 @@ export function resolveConfig(
102
167
  allSettings: Record<string, string>,
103
168
  options?: { siteUrl?: string },
104
169
  ): AppConfig {
105
- const pageSize = parseConfigInt(resolve("PAGE_SIZE", allSettings, env), 50);
170
+ const summaryConfig = resolveSummaryConfig(env, allSettings);
171
+ const pageSize = parseConfigInt(
172
+ "PAGE_SIZE",
173
+ resolve("PAGE_SIZE", allSettings, env),
174
+ 50,
175
+ );
106
176
  const searchPageSize = parseConfigInt(
177
+ "SEARCH_PAGE_SIZE",
107
178
  resolve("SEARCH_PAGE_SIZE", allSettings, env),
108
179
  pageSize,
109
180
  );
110
181
  const archivePageSize = parseConfigInt(
182
+ "ARCHIVE_PAGE_SIZE",
111
183
  resolve("ARCHIVE_PAGE_SIZE", allSettings, env),
112
184
  pageSize,
113
185
  );
@@ -137,9 +209,12 @@ export function resolveConfig(
137
209
  }
138
210
 
139
211
  // Description is "explicit" when set in DB or ENV (not just the default)
212
+ const hasDbDescription = Object.hasOwn(allSettings, "SITE_DESCRIPTION");
140
213
  const dbDescription = allSettings["SITE_DESCRIPTION"];
141
214
  const envDescription = getEnvString(env, "SITE_DESCRIPTION");
142
- const siteDescriptionExplicit = !!(dbDescription || envDescription);
215
+ const siteDescriptionExplicit = hasDbDescription
216
+ ? !!dbDescription
217
+ : !!envDescription;
143
218
 
144
219
  return {
145
220
  // Site identity (DB > ENV > Default)
@@ -158,6 +233,9 @@ export function resolveConfig(
158
233
  showJantBrandingOnHome:
159
234
  resolve("SHOW_JANT_BRANDING_ON_HOME", allSettings, env) === "true",
160
235
  noindex: demoMode || resolve("NOINDEX", allSettings, env) === "true",
236
+ publicApiEnabled:
237
+ resolve("PUBLIC_API_ENABLED", allSettings, env) === "true",
238
+ rssFeedsEnabled: resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true",
161
239
 
162
240
  // Infrastructure (ENV only)
163
241
  siteUrl,
@@ -185,21 +263,27 @@ export function resolveConfig(
185
263
  parseInt(getEnvString(env, "UPLOAD_MAX_FILE_SIZE_MB") ?? "1024", 10) ||
186
264
  1024,
187
265
 
188
- // Summary extraction (ENV only)
189
- summaryMaxParagraphs:
190
- parseInt(getEnvString(env, "SUMMARY_MAX_PARAGRAPHS") ?? "5", 10) || 5,
191
- summaryMaxChars:
192
- parseInt(getEnvString(env, "SUMMARY_MAX_CHARS") ?? "500", 10) || 500,
266
+ // Summary extraction (DB > ENV > Default)
267
+ summaryMaxParagraphs: summaryConfig.maxParagraphs,
268
+ summaryMaxChars: summaryConfig.maxChars,
193
269
 
194
270
  // Slug (ENV only)
195
271
  slugIdLength: parseInt(getEnvString(env, "SLUG_ID_LENGTH") ?? "5", 10) || 5,
196
272
 
197
- // Pagination/Feed (ENV only)
273
+ // Pagination/feed (DB > ENV > Default)
198
274
  pageSize,
199
275
  searchPageSize,
200
276
  archivePageSize,
201
- rssFeedLimit:
202
- parseInt(getEnvString(env, "RSS_FEED_LIMIT") ?? "50", 10) || 50,
277
+ rssFeedLimit: parseConfigInt(
278
+ "RSS_FEED_LIMIT",
279
+ resolve("RSS_FEED_LIMIT", allSettings, env),
280
+ 50,
281
+ ),
282
+ rssPublishDelaySeconds: parseConfigInt(
283
+ "RSS_PUBLISH_DELAY_SECONDS",
284
+ resolve("RSS_PUBLISH_DELAY_SECONDS", allSettings, env),
285
+ 300,
286
+ ),
203
287
 
204
288
  // Demo (ENV only)
205
289
  demoEmail: getEnvString(env, "DEMO_EMAIL") || "",
@@ -26,13 +26,22 @@ import {
26
26
  MAX_SITE_DESCRIPTION_LENGTH,
27
27
  MAX_SITE_FOOTER_LENGTH,
28
28
  TEXT_ATTACHMENT_CONTENT_FORMATS,
29
+ CONFIG_FIELDS,
30
+ type ConfigEditorDefinition,
29
31
  type ConfigKey,
30
32
  } from "../types.js";
33
+ import { isCjkSerifFont } from "../i18n/detect.js";
34
+ import {
35
+ isLocale,
36
+ isValidContentLanguage,
37
+ normalizeContentLanguage,
38
+ } from "../i18n/locales.js";
31
39
  import { ValidationError } from "./errors.js";
32
40
  import { createTypeIdSchema, ID_PREFIX } from "./ids.js";
33
41
  import { normalizeSlug } from "./slug-format.js";
34
42
  import { isReservedPath } from "./constants.js";
35
43
  import { sanitizeUrl, normalizePath } from "./url.js";
44
+ import { isSupportedTimeZone, normalizeTimeZone } from "./timezones.js";
36
45
 
37
46
  // =============================================================================
38
47
  // Shared Transforms
@@ -557,6 +566,12 @@ export const CreateNavItemSchema = z.discriminatedUnion("type", [
557
566
  label: sanitizeText(100).pipe(z.string().min(1)).optional(),
558
567
  placement: z.enum(["header", "more"]).optional(),
559
568
  }),
569
+ z.object({
570
+ type: z.literal("page"),
571
+ postId: PostIdSchema,
572
+ label: sanitizeText(100).pipe(z.string().min(1)).optional(),
573
+ placement: z.enum(["header", "more"]).optional(),
574
+ }),
560
575
  ]);
561
576
 
562
577
  /**
@@ -684,12 +699,129 @@ const EDITABLE_SETTING_VALUE_SCHEMAS: Partial<
684
699
  SITE_FOOTER: SiteFooterSettingSchema,
685
700
  };
686
701
 
702
+ /**
703
+ * Normalize a raw string according to a Config Editor control definition.
704
+ *
705
+ * @param definition - Typed editor metadata from the config registry
706
+ * @param value - Raw string received at the HTTP/service boundary
707
+ * @returns Canonical string suitable for the settings key-value store
708
+ * @example
709
+ * ```ts
710
+ * normalizeConfigEditorDefinitionValue({ type: "boolean" }, "true");
711
+ * // "true"
712
+ * ```
713
+ */
714
+ export function normalizeConfigEditorDefinitionValue(
715
+ definition: ConfigEditorDefinition,
716
+ value: string,
717
+ ): string {
718
+ switch (definition.type) {
719
+ case "boolean": {
720
+ const normalized = value.trim().toLowerCase();
721
+ if (normalized !== "true" && normalized !== "false") {
722
+ throw new ValidationError("Choose true or false.");
723
+ }
724
+ return normalized;
725
+ }
726
+ case "string": {
727
+ const normalized = value.trim();
728
+ if (
729
+ definition.maxLength !== undefined &&
730
+ normalized.length > definition.maxLength
731
+ ) {
732
+ throw new ValidationError(
733
+ `Keep this value at ${definition.maxLength} characters or fewer.`,
734
+ );
735
+ }
736
+ return normalized;
737
+ }
738
+ case "number": {
739
+ const normalized = value.trim();
740
+ const parsed = Number(normalized);
741
+ if (!normalized || !Number.isFinite(parsed)) {
742
+ throw new ValidationError("Enter a valid number.");
743
+ }
744
+ if (definition.min !== undefined && parsed < definition.min) {
745
+ throw new ValidationError(
746
+ `Enter a number greater than or equal to ${definition.min}.`,
747
+ );
748
+ }
749
+ if (definition.max !== undefined && parsed > definition.max) {
750
+ throw new ValidationError(
751
+ `Enter a number less than or equal to ${definition.max}.`,
752
+ );
753
+ }
754
+ if (definition.step !== undefined && definition.step > 0) {
755
+ const steps = parsed / definition.step;
756
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 4;
757
+ if (Math.abs(steps - Math.round(steps)) > tolerance) {
758
+ throw new ValidationError(
759
+ `Enter a number in steps of ${definition.step}.`,
760
+ );
761
+ }
762
+ }
763
+ return String(parsed);
764
+ }
765
+ case "enum": {
766
+ if (definition.options && !definition.options.includes(value)) {
767
+ throw new ValidationError("Choose one of the available options.");
768
+ }
769
+ return value;
770
+ }
771
+ }
772
+ }
773
+
687
774
  export function normalizeEditableSettingValue(
688
775
  key: ConfigKey,
689
776
  value: string,
690
777
  ): string {
778
+ const field = CONFIG_FIELDS[key];
779
+ if (!("editor" in field)) {
780
+ return value;
781
+ }
782
+
691
783
  const schema = EDITABLE_SETTING_VALUE_SCHEMAS[key];
692
- return schema ? parseValidated(schema, value) : value;
784
+ let normalized = schema
785
+ ? parseValidated(schema, value)
786
+ : normalizeConfigEditorDefinitionValue(field.editor, value);
787
+
788
+ switch (key) {
789
+ case "SITE_NAME":
790
+ if (!normalized) {
791
+ throw new ValidationError(
792
+ "Site name can't be empty. Enter a name or reset this setting.",
793
+ );
794
+ }
795
+ break;
796
+ case "SITE_LANGUAGE":
797
+ if (!isValidContentLanguage(normalized)) {
798
+ throw new ValidationError(
799
+ "Enter a valid BCP 47 language tag, such as en, fi, or zh-Hans.",
800
+ );
801
+ }
802
+ normalized = normalizeContentLanguage(normalized);
803
+ break;
804
+ case "DASHBOARD_LANGUAGE":
805
+ if (normalized && !isLocale(normalized)) {
806
+ throw new ValidationError(
807
+ "Choose a dashboard language Jant is translated into.",
808
+ );
809
+ }
810
+ break;
811
+ case "CJK_SERIF_FONT":
812
+ if (!isCjkSerifFont(normalized)) {
813
+ throw new ValidationError("Choose an available CJK font fallback.");
814
+ }
815
+ break;
816
+ case "TIME_ZONE":
817
+ if (!isSupportedTimeZone(normalized)) {
818
+ throw new ValidationError("Choose a valid time zone.");
819
+ }
820
+ normalized = normalizeTimeZone(normalized);
821
+ break;
822
+ }
823
+
824
+ return normalized;
693
825
  }
694
826
 
695
827
  /**
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Canonical paths for settings pages shared by server and client UI.
3
+ */
4
+
5
+ export const NAVIGATION_SETTINGS_PATH = "/settings/navigation";
@@ -0,0 +1,78 @@
1
+ import SITE_SKILL_TEMPLATE from "../../../../docs/skill.md?raw";
2
+ import { normalizeSiteUrl } from "./url.js";
3
+
4
+ const INTRODUCTION =
5
+ "This page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.";
6
+ const GENERIC_SITE_URL = "https://example.com";
7
+ const GENERIC_SITE_CONFIRMATION = `- The Jant site URL (e.g. \`${GENERIC_SITE_URL}\`).`;
8
+ const GENERIC_SITE_EXPORT = `export JANT_SITE=${GENERIC_SITE_URL}\n`;
9
+ const GENERIC_SITE_MARKERS = [
10
+ GENERIC_SITE_CONFIRMATION,
11
+ GENERIC_SITE_EXPORT,
12
+ "<site>",
13
+ "$JANT_SITE",
14
+ ];
15
+
16
+ function replaceLiteral(
17
+ content: string,
18
+ search: string,
19
+ replacement: string,
20
+ ): string {
21
+ return content.replaceAll(search, () => replacement);
22
+ }
23
+
24
+ /**
25
+ * Render the Agent skill for one concrete Jant site.
26
+ *
27
+ * @param siteUrl - Resolved public site URL, including any path prefix
28
+ * @returns Markdown with every target-site placeholder bound to `siteUrl`
29
+ * @example
30
+ * ```ts
31
+ * renderSiteSkill("https://example.com/blog");
32
+ * ```
33
+ */
34
+ export function renderSiteSkill(siteUrl: string): string {
35
+ const normalizedSiteUrl = normalizeSiteUrl(siteUrl);
36
+ if (!normalizedSiteUrl) {
37
+ throw new Error("A public site URL is required to render /skill.md");
38
+ }
39
+
40
+ const targetSiteUrl = normalizedSiteUrl.replace(/\/$/, "");
41
+ if (!SITE_SKILL_TEMPLATE.includes(INTRODUCTION)) {
42
+ throw new Error("The site skill template introduction could not be found");
43
+ }
44
+ const missingMarker = GENERIC_SITE_MARKERS.find(
45
+ (marker) => !SITE_SKILL_TEMPLATE.includes(marker),
46
+ );
47
+ if (missingMarker) {
48
+ throw new Error(
49
+ `The site skill template target marker could not be found: ${missingMarker}`,
50
+ );
51
+ }
52
+
53
+ let content = SITE_SKILL_TEMPLATE;
54
+ content = replaceLiteral(content, GENERIC_SITE_EXPORT, "");
55
+ content = replaceLiteral(
56
+ content,
57
+ GENERIC_SITE_CONFIRMATION,
58
+ `- That the target Jant site is \`${targetSiteUrl}\`.`,
59
+ );
60
+ content = replaceLiteral(content, "<site>", targetSiteUrl);
61
+ content = replaceLiteral(content, "$JANT_SITE", targetSiteUrl);
62
+ content = replaceLiteral(
63
+ content,
64
+ INTRODUCTION,
65
+ `${INTRODUCTION}\n\nThis site-scoped copy is bound to <${targetSiteUrl}>. Send every API request in this guide to this exact base URL. Do not substitute another target without the user's confirmation.`,
66
+ );
67
+
68
+ const unresolvedMarker = ["<site>", "$JANT_SITE"].find((marker) =>
69
+ content.includes(marker),
70
+ );
71
+ if (unresolvedMarker) {
72
+ throw new Error(
73
+ `The site skill template contains an unresolved target marker: ${unresolvedMarker}`,
74
+ );
75
+ }
76
+
77
+ return content;
78
+ }
@@ -6,7 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { JSONContent } from "@tiptap/core";
9
- import { renderTiptapDocument } from "./tiptap-render.js";
9
+ import {
10
+ renderTiptapDocument,
11
+ type TiptapRenderOptions,
12
+ } from "./tiptap-render.js";
10
13
 
11
14
  interface TiptapNode {
12
15
  type: string;
@@ -220,6 +223,7 @@ export function extractSummary(
220
223
  * included and `hasMore` is `false`. Avoids a "read more" that reveals only a
221
224
  * sliver of text. Explicit `moreBreak` markers reflect author intent and are
222
225
  * never subject to this tolerance.
226
+ * @param renderOptions - Renderer options such as the post footnote namespace
223
227
  * @returns HTML summary, whether content was truncated, and the index in
224
228
  * `doc.content` where the content after the summary boundary begins, or null.
225
229
  * `breakAtIndex` lets callers align the summary with the full-body rendering
@@ -236,6 +240,7 @@ export function extractSummaryHtml(
236
240
  maxBlocks: number = 5,
237
241
  maxChars: number = 500,
238
242
  minHiddenChars: number = 0,
243
+ renderOptions: TiptapRenderOptions = {},
239
244
  ): { html: string; hasMore: boolean; breakAtIndex: number } | null {
240
245
  let doc: TiptapNode;
241
246
  try {
@@ -260,10 +265,13 @@ export function extractSummaryHtml(
260
265
  if (selected.length === 0) return null;
261
266
  const subDoc: JSONContent = {
262
267
  type: "doc",
263
- content: selected as JSONContent[],
268
+ content: [
269
+ ...selected,
270
+ ...nodes.filter((node) => node.type === "footnoteDefinition"),
271
+ ] as JSONContent[],
264
272
  };
265
273
  return {
266
- html: renderTiptapDocument(subDoc),
274
+ html: renderTiptapDocument(subDoc, renderOptions),
267
275
  hasMore: true,
268
276
  // Anchor goes in place of the moreBreak marker, so the marker itself
269
277
  // is NOT part of the pre-anchor body. It remains in the post-anchor
@@ -329,10 +337,13 @@ export function extractSummaryHtml(
329
337
 
330
338
  const subDoc: JSONContent = {
331
339
  type: "doc",
332
- content: selected as JSONContent[],
340
+ content: [
341
+ ...selected,
342
+ ...nodes.filter((node) => node.type === "footnoteDefinition"),
343
+ ] as JSONContent[],
333
344
  };
334
345
  return {
335
- html: renderTiptapDocument(subDoc),
346
+ html: renderTiptapDocument(subDoc, renderOptions),
336
347
  hasMore,
337
348
  breakAtIndex: lastSelectedIdx + 1,
338
349
  };