@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -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-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -51,6 +51,48 @@ function makeFeedData(post: FeedPostView): FeedData {
51
51
  }
52
52
 
53
53
  describe("feed renderers", () => {
54
+ // Stamping the render time on <updated> tells every reader the feed changed
55
+ // on every poll, which is untrue and useless for change detection.
56
+ it("dates the feed by its newest entry, not the render time", () => {
57
+ const xml = defaultFeedRenderer({
58
+ ...makeFeedData(makePostView()),
59
+ posts: [
60
+ makePostView({
61
+ id: "a",
62
+ permalink: "/a",
63
+ feedUpdatedAt: "2026-01-02T00:00:00.000Z",
64
+ }),
65
+ makePostView({
66
+ id: "b",
67
+ permalink: "/b",
68
+ feedUpdatedAt: "2026-05-09T00:00:00.000Z",
69
+ }),
70
+ makePostView({
71
+ id: "c",
72
+ permalink: "/c",
73
+ feedUpdatedAt: "2026-03-04T00:00:00.000Z",
74
+ }),
75
+ ],
76
+ });
77
+
78
+ const feedUpdated = /<id>[^<]*<\/id>\s*<updated>([^<]+)<\/updated>/.exec(
79
+ xml,
80
+ )?.[1];
81
+ expect(feedUpdated).toBe("2026-05-09T00:00:00.000Z");
82
+ });
83
+
84
+ it("falls back to the render time for an empty feed", () => {
85
+ const xml = defaultFeedRenderer({
86
+ ...makeFeedData(makePostView()),
87
+ posts: [],
88
+ });
89
+
90
+ const feedUpdated = /<id>[^<]*<\/id>\s*<updated>([^<]+)<\/updated>/.exec(
91
+ xml,
92
+ )?.[1];
93
+ expect(feedUpdated).toMatch(/^\d{4}-\d{2}-\d{2}T/);
94
+ });
95
+
54
96
  it("keeps Atom entry titles empty for untitled posts and strips script tags from content", () => {
55
97
  const xml = defaultFeedRenderer(
56
98
  makeFeedData(
@@ -110,6 +152,24 @@ describe("feed renderers", () => {
110
152
  expect(xml).not.toContain("<iframe");
111
153
  });
112
154
 
155
+ it("resolves relative links and media URLs inside post HTML", () => {
156
+ const post = makePostView({
157
+ bodyHtml:
158
+ '<p><a href="/related">Related</a> <a href="#footnote">Footnote</a></p>' +
159
+ '<img src="/media/inline.jpg" alt="Inline">' +
160
+ '<video poster="/media/poster.jpg"><source src="/media/clip.mp4"></video>' +
161
+ '<span data-src="/leave-this-relative"></span>',
162
+ });
163
+ const xml = defaultFeedRenderer(makeFeedData(post));
164
+
165
+ expect(xml).toContain('href="https://example.com/related"');
166
+ expect(xml).toContain('href="#footnote"');
167
+ expect(xml).toContain('src="https://example.com/media/inline.jpg"');
168
+ expect(xml).toContain('poster="https://example.com/media/poster.jpg"');
169
+ expect(xml).toContain('src="https://example.com/media/clip.mp4"');
170
+ expect(xml).toContain('data-src="/leave-this-relative"');
171
+ });
172
+
113
173
  it("does not expose quote attribution as feed title", () => {
114
174
  const xml = defaultFeedRenderer(
115
175
  makeFeedData(
@@ -158,6 +218,57 @@ describe("feed renderers", () => {
158
218
  );
159
219
  });
160
220
 
221
+ it("renders YouTube Link previews as a linked thumbnail with a provider-aware action", () => {
222
+ const post = makePostView({
223
+ format: "link",
224
+ title: "A useful video",
225
+ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
226
+ bodyHtml: "<p>My notes on the video.</p>",
227
+ previewKind: "video",
228
+ previewProvider: "youtube",
229
+ previewImageUrl: "/media/previews/youtube.jpg",
230
+ });
231
+ const xml = defaultFeedRenderer(makeFeedData(post));
232
+
233
+ expect(xml).toContain(
234
+ '<figure><a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"><img src="https://example.com/media/previews/youtube.jpg" alt="A useful video"/></a><figcaption><a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">▶ Watch on YouTube</a></figcaption></figure>',
235
+ );
236
+ expect(xml.indexOf("<figure>")).toBeLessThan(
237
+ xml.indexOf("My notes on the video."),
238
+ );
239
+ expect(xml).not.toContain("<iframe");
240
+ expect(xml).not.toContain('rel="enclosure"');
241
+ });
242
+
243
+ it("renders non-video Link previews without a video action", () => {
244
+ const post = makePostView({
245
+ format: "link",
246
+ title: "Illustrated article",
247
+ url: "https://external.com/illustrated",
248
+ previewKind: "image",
249
+ previewImageUrl: "https://example.com/previews/article.jpg",
250
+ });
251
+ const xml = defaultFeedRenderer(makeFeedData(post));
252
+
253
+ expect(xml).toContain(
254
+ '<figure><a href="https://external.com/illustrated"><img src="https://example.com/previews/article.jpg" alt="Illustrated article"/></a></figure>',
255
+ );
256
+ expect(xml).not.toContain("Watch video");
257
+ expect(xml).not.toContain("Watch on");
258
+ });
259
+
260
+ it("keeps the text fallback when a Link post has no preview image", () => {
261
+ const post = makePostView({
262
+ format: "link",
263
+ title: "Text-only link",
264
+ url: "https://external.com/text-only",
265
+ });
266
+ const xml = defaultFeedRenderer(makeFeedData(post));
267
+
268
+ expect(xml).toContain("<![CDATA[<p>Text-only link</p>");
269
+ expect(xml).not.toContain("<figure>");
270
+ });
271
+
161
272
  it("note posts still link to blog permalink without ★", () => {
162
273
  const post = makePostView({
163
274
  format: "note",
@@ -272,6 +383,38 @@ describe("feed renderers", () => {
272
383
  );
273
384
  });
274
385
 
386
+ it("renders a YouTube Link preview inside a thread reply", () => {
387
+ const reply = makePostView({
388
+ id: "reply-video",
389
+ permalink: "/reply-video",
390
+ slug: "reply-video",
391
+ format: "link",
392
+ title: "Thread video",
393
+ url: "https://youtu.be/dQw4w9WgXcQ",
394
+ previewKind: "video",
395
+ previewProvider: "youtube",
396
+ previewImageUrl: "/media/previews/thread-video.jpg",
397
+ });
398
+
399
+ const xml = defaultFeedRenderer(
400
+ makeFeedData(
401
+ makePostView({
402
+ title: "Thread Root",
403
+ bodyHtml: "<p>Root content</p>",
404
+ threadReplies: [reply],
405
+ }),
406
+ ),
407
+ );
408
+
409
+ const replyTitle =
410
+ '<h2><a href="https://youtu.be/dQw4w9WgXcQ">Thread video</a></h2>';
411
+ const preview =
412
+ '<figure><a href="https://youtu.be/dQw4w9WgXcQ"><img src="https://example.com/media/previews/thread-video.jpg" alt="Thread video"/></a><figcaption><a href="https://youtu.be/dQw4w9WgXcQ">▶ Watch on YouTube</a></figcaption></figure>';
413
+ expect(xml).toContain(replyTitle);
414
+ expect(xml).toContain(preview);
415
+ expect(xml.indexOf(replyTitle)).toBeLessThan(xml.indexOf(preview));
416
+ });
417
+
275
418
  it("includes thread reply media as Atom enclosures on the combined entry", () => {
276
419
  const reply = makePostView({
277
420
  id: "reply-1",
@@ -368,6 +511,31 @@ describe("feed renderers", () => {
368
511
  );
369
512
  });
370
513
 
514
+ it("resolves relative attachment, poster, and enclosure URLs", () => {
515
+ const post = makePostView({
516
+ media: [
517
+ makeMediaView({
518
+ id: "med_local_vid",
519
+ url: "/media/local-clip.mp4",
520
+ thumbnailUrl: "/media/local-clip-thumb.jpg",
521
+ posterUrl: "/media/local-clip-poster.jpg",
522
+ mimeType: "video/mp4",
523
+ }),
524
+ ],
525
+ });
526
+ const xml = defaultFeedRenderer(makeFeedData(post));
527
+
528
+ expect(xml).toContain(
529
+ '<a href="https://example.com/media/local-clip.mp4"><img src="https://example.com/media/local-clip-poster.jpg"',
530
+ );
531
+ expect(xml).toContain(
532
+ '<a href="https://example.com/media/local-clip.mp4">▶ Watch video</a>',
533
+ );
534
+ expect(xml).toContain(
535
+ '<link rel="enclosure" type="video/mp4" href="https://example.com/media/local-clip.mp4"',
536
+ );
537
+ });
538
+
371
539
  it("renders audio attachments as a labeled link with duration and size", () => {
372
540
  const post = makePostView({
373
541
  media: [
@@ -0,0 +1,181 @@
1
+ import type { JSONContent } from "@tiptap/core";
2
+ import { describe, expect, it } from "vitest";
3
+ import { upgradeLegacyFootnotes } from "../footnotes.js";
4
+ import { markdownToTiptapJson } from "../markdown-to-tiptap.js";
5
+ import { renderTiptapDocument } from "../tiptap-render.js";
6
+
7
+ function linkedText(text: string, href: string): JSONContent {
8
+ return {
9
+ type: "text",
10
+ text,
11
+ marks: [
12
+ {
13
+ type: "link",
14
+ attrs: { href, target: "_blank", rel: "noopener noreferrer" },
15
+ },
16
+ ],
17
+ };
18
+ }
19
+
20
+ function legacyDocument(): JSONContent {
21
+ return {
22
+ type: "doc",
23
+ content: [
24
+ {
25
+ type: "paragraph",
26
+ content: [
27
+ { type: "text", text: "First" },
28
+ linkedText("[1]", "#fn-1"),
29
+ { type: "text", text: " and repeated" },
30
+ linkedText("[1]", "#fn-1"),
31
+ { type: "text", text: ". Second" },
32
+ linkedText("[2]", "#fn-2"),
33
+ ],
34
+ },
35
+ { type: "horizontalRule" },
36
+ {
37
+ type: "orderedList",
38
+ attrs: { start: 1, type: null },
39
+ content: [
40
+ {
41
+ type: "listItem",
42
+ content: [
43
+ {
44
+ type: "paragraph",
45
+ content: [
46
+ { type: "text", text: "Rich definition with " },
47
+ linkedText("source", "https://example.com"),
48
+ linkedText("↩︎", "#fnref-1"),
49
+ ],
50
+ },
51
+ {
52
+ type: "bulletList",
53
+ content: [
54
+ {
55
+ type: "listItem",
56
+ content: [
57
+ {
58
+ type: "paragraph",
59
+ content: [{ type: "text", text: "Nested detail" }],
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ },
67
+ {
68
+ type: "listItem",
69
+ content: [
70
+ {
71
+ type: "paragraph",
72
+ content: [
73
+ { type: "text", text: "Second definition" },
74
+ linkedText("↩︎", "#fnref-2"),
75
+ ],
76
+ },
77
+ ],
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ };
83
+ }
84
+
85
+ describe("upgradeLegacyFootnotes", () => {
86
+ it("upgrades the historical link, rule, list, and backlink shape", () => {
87
+ const result = upgradeLegacyFootnotes(legacyDocument());
88
+
89
+ expect(result.upgraded).toBe(true);
90
+ expect(result.doc.content?.map((node) => node.type)).toEqual([
91
+ "paragraph",
92
+ "footnoteDefinition",
93
+ "footnoteDefinition",
94
+ ]);
95
+ expect(
96
+ result.doc.content?.[0]?.content?.filter(
97
+ (node) => node.type === "footnoteReference",
98
+ ),
99
+ ).toEqual([
100
+ { type: "footnoteReference", attrs: { label: "1" } },
101
+ { type: "footnoteReference", attrs: { label: "1" } },
102
+ { type: "footnoteReference", attrs: { label: "2" } },
103
+ ]);
104
+
105
+ const serialized = JSON.stringify(result.doc);
106
+ expect(serialized).not.toContain("#fn-");
107
+ expect(serialized).not.toContain("#fnref-");
108
+ expect(serialized).not.toContain("↩");
109
+ expect(serialized).toContain("https://example.com");
110
+ expect(serialized).toContain("Nested detail");
111
+
112
+ const html = renderTiptapDocument(result.doc, {
113
+ namespace: "pst_legacy",
114
+ });
115
+ expect(html).toContain('role="doc-noteref"');
116
+ expect(html).toContain('role="doc-endnotes"');
117
+ expect(html).toContain('role="doc-backlink"');
118
+ expect(html).not.toContain("<hr>");
119
+ });
120
+
121
+ it("is idempotent after the canonical nodes have been created", () => {
122
+ const first = upgradeLegacyFootnotes(legacyDocument());
123
+ const second = upgradeLegacyFootnotes(first.doc);
124
+
125
+ expect(second).toEqual({ doc: first.doc, upgraded: false });
126
+ });
127
+
128
+ it("upgrades the Markdown round-trip shape observed on historical posts", () => {
129
+ const body = markdownToTiptapJson(
130
+ [
131
+ "Codex[\\[1\\]](#fn-1) and Claude[\\[2\\]](#fn-2).",
132
+ "",
133
+ "---",
134
+ "",
135
+ "1. First definition.[↩︎](#fnref-1)",
136
+ "2. Second definition.[↩︎](#fnref-2)",
137
+ ].join("\n"),
138
+ );
139
+ const result = upgradeLegacyFootnotes(JSON.parse(body) as JSONContent);
140
+
141
+ expect(result.upgraded).toBe(true);
142
+ expect(JSON.stringify(result.doc)).toContain('"type":"footnoteReference"');
143
+ expect(JSON.stringify(result.doc)).toContain('"type":"footnoteDefinition"');
144
+ });
145
+
146
+ it.each([
147
+ {
148
+ name: "has no separator",
149
+ mutate(doc: JSONContent) {
150
+ doc.content?.splice(-2, 1);
151
+ },
152
+ },
153
+ {
154
+ name: "has an unmatched backlink",
155
+ mutate(doc: JSONContent) {
156
+ const list = doc.content?.at(-1);
157
+ const backlink = list?.content?.[0]?.content?.[0]?.content?.at(-1);
158
+ if (backlink?.marks?.[0]?.attrs) {
159
+ backlink.marks[0].attrs.href = "#fnref-99";
160
+ }
161
+ },
162
+ },
163
+ {
164
+ name: "has an ordinary numbered list",
165
+ mutate(doc: JSONContent) {
166
+ const list = doc.content?.at(-1);
167
+ const paragraph = list?.content?.[0]?.content?.[0];
168
+ paragraph?.content?.pop();
169
+ },
170
+ },
171
+ ])("leaves a document unchanged when it $name", ({ mutate }) => {
172
+ const doc = legacyDocument();
173
+ mutate(doc);
174
+ const original = globalThis.structuredClone(doc);
175
+
176
+ expect(upgradeLegacyFootnotes(doc)).toEqual({
177
+ doc: original,
178
+ upgraded: false,
179
+ });
180
+ });
181
+ });
@@ -8,6 +8,8 @@ interface TiptapNode {
8
8
  type: string;
9
9
  attrs?: Record<string, unknown>;
10
10
  content?: TiptapNode[];
11
+ marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
12
+ text?: string;
11
13
  }
12
14
 
13
15
  function findNode(node: TiptapNode, type: string): TiptapNode | null {
@@ -86,3 +88,62 @@ describe("markdown round-trip: htmlBlock", () => {
86
88
  expect(out).toContain(inner);
87
89
  });
88
90
  });
91
+
92
+ describe("markdown manager integration", () => {
93
+ it("round-trips ordered-list numbering and nested inline formatting", () => {
94
+ const markdown = [
95
+ "5. **Five**",
96
+ "6. Six",
97
+ " 1. *Nested one*",
98
+ " 2. Nested two",
99
+ ].join("\n");
100
+
101
+ const parsed = parseMarkdownDocument(markdown) as TiptapNode;
102
+ const outerList = findNode(parsed, "orderedList");
103
+ expect(outerList?.attrs?.start).toBe(5);
104
+ expect(outerList?.content).toHaveLength(2);
105
+ const nestedList = findNode(
106
+ outerList?.content?.[1] ?? parsed,
107
+ "orderedList",
108
+ );
109
+ expect(nestedList?.attrs?.start ?? 1).toBe(1);
110
+ expect(nestedList?.content).toHaveLength(2);
111
+
112
+ const serialized = serializeMarkdownDocument(parsed);
113
+ expect(serialized).toContain("5. **Five**");
114
+ expect(serialized).toContain("6. Six");
115
+ expect(parseMarkdownDocument(serialized)).toEqual(parsed);
116
+ });
117
+
118
+ it("round-trips table, image, link, and footnote integrations", () => {
119
+ const markdown = [
120
+ "[Source](https://example.com)",
121
+ "",
122
+ "| Name | Value |",
123
+ "| --- | --- |",
124
+ "| Jant | 1 |",
125
+ "",
126
+ '![Alt text](https://example.com/image.png "Title")',
127
+ "",
128
+ "Body[^note]",
129
+ "",
130
+ "[^note]: Footnote body",
131
+ ].join("\n");
132
+
133
+ const parsed = parseMarkdownDocument(markdown) as TiptapNode;
134
+ expect(findNode(parsed, "table")).not.toBeNull();
135
+ expect(findNode(parsed, "image")?.attrs).toMatchObject({
136
+ src: "https://example.com/image.png",
137
+ alt: "Alt text",
138
+ title: "Title",
139
+ });
140
+ expect(findNode(parsed, "footnoteReference")?.attrs?.label).toBe("note");
141
+ expect(findNode(parsed, "footnoteDefinition")?.attrs?.label).toBe("note");
142
+ const linkedText = findNode(parsed, "text");
143
+ expect(linkedText?.marks?.[0]?.type).toBe("link");
144
+ expect(linkedText?.marks?.[0]?.attrs?.href).toBe("https://example.com");
145
+
146
+ const serialized = serializeMarkdownDocument(parsed);
147
+ expect(parseMarkdownDocument(serialized)).toEqual(parsed);
148
+ });
149
+ });
@@ -128,6 +128,29 @@ describe("markdownToTiptapJson", () => {
128
128
  expect(doc.content[1].content[0].type).toBe("paragraph");
129
129
  expect(doc.content[1].content[0].content[0].text).toBe("Footnote body");
130
130
  });
131
+
132
+ it("normalizes inline footnotes into paired structural nodes", () => {
133
+ const doc = parse("Body^[Inline **bold** note.]");
134
+
135
+ expect(doc.content[0].content).toEqual([
136
+ { type: "text", text: "Body" },
137
+ { type: "footnoteReference", attrs: { label: "1" } },
138
+ ]);
139
+ expect(doc.content[1]).toMatchObject({
140
+ type: "footnoteDefinition",
141
+ attrs: { label: "1" },
142
+ content: [
143
+ {
144
+ type: "paragraph",
145
+ content: [
146
+ { type: "text", text: "Inline " },
147
+ { type: "text", text: "bold", marks: [{ type: "bold" }] },
148
+ { type: "text", text: " note." },
149
+ ],
150
+ },
151
+ ],
152
+ });
153
+ });
131
154
  });
132
155
 
133
156
  describe("inline elements", () => {
@@ -398,15 +421,20 @@ describe("end-to-end: Markdown → markdownToTiptapJson → renderTiptapJson", (
398
421
  expect(html).toContain("<figcaption>Caption</figcaption>");
399
422
  });
400
423
 
401
- it("renders footnotes as inline sidenotes through the shared document renderer", () => {
424
+ it("renders semantic footnotes through the shared document renderer", () => {
402
425
  const json = markdownToTiptapJson("Body[^1]\n\n[^1]: Footnote body");
403
426
  const html = renderTiptapJson(json);
404
427
 
428
+ expect(html).toContain('role="doc-noteref"');
405
429
  expect(html).toContain(
406
- '<label for="sn-1" class="margin-toggle sidenote-number footref">',
430
+ '<section class="footnote-endnotes" role="doc-endnotes">',
407
431
  );
408
- expect(html).toContain('<span class="sidenote">Footnote body</span>');
409
- expect(html).not.toContain('<section class="footnotes"');
432
+ expect(html).toContain('<ol class="footnote-list">');
433
+ expect(html).toContain('<li id="fn-1" class="footnote">');
434
+ expect(html).toMatch(/<p>Footnote body <span class="footnote-backlinks">/);
435
+ expect(html).toContain('role="doc-backlink"');
436
+ expect(html).not.toMatch(/footnote-document|footnote-main|footnote-body/);
437
+ expect(html).not.toMatch(/data-footnote-|--footnote-|tabindex=/);
410
438
  });
411
439
 
412
440
  it("renders a complex document", () => {
@@ -26,6 +26,22 @@ describe("render", () => {
26
26
  expect(html).toContain(">link</a>");
27
27
  });
28
28
 
29
+ it("renders phone and messaging action links", () => {
30
+ expect(render("[Call](tel:+15551234567)")).toContain(
31
+ 'href="tel:+15551234567"',
32
+ );
33
+ expect(render("[Message](sms:xxxxx@xx.com)")).toContain(
34
+ 'href="sms:xxxxx@xx.com"',
35
+ );
36
+ });
37
+
38
+ it("strips executable link destinations", () => {
39
+ const html = render("[unsafe](javascript:alert(1))");
40
+
41
+ expect(html).not.toContain("javascript:");
42
+ expect(html).toContain('href=""');
43
+ });
44
+
29
45
  it("renders code blocks", () => {
30
46
  const html = render("```\nconst x = 1;\n```");
31
47
  expect(html).toContain("<code>");
@@ -62,14 +78,16 @@ describe("render", () => {
62
78
  expect(html).toBe("<p>Intro</p><!--more--><p>Rest</p>");
63
79
  });
64
80
 
65
- it("renders footnote references as inline sidenotes", () => {
81
+ it("renders semantic footnote references and definitions", () => {
66
82
  const html = render("Body copy[^1]\n\n[^1]: Footnote body");
67
83
 
68
- expect(html).toContain(
69
- '<label for="sn-1" class="margin-toggle sidenote-number footref">',
70
- );
71
- expect(html).toContain('<span class="sidenote">Footnote body</span>');
72
- expect(html).not.toContain('<section class="footnotes"');
84
+ expect(html).toContain('role="doc-noteref"');
85
+ expect(html).toContain('role="doc-endnotes"');
86
+ expect(html).toContain('class="footnote-list"');
87
+ expect(html).toContain("Footnote body");
88
+ expect(html).toContain('role="doc-backlink"');
89
+ expect(html.match(/<ol class="footnote-list">/g)).toHaveLength(1);
90
+ expect(html).not.toMatch(/footnote-document|data-footnote-|--footnote-/);
73
91
  });
74
92
 
75
93
  it("escapes raw HTML outside the supported markdown schema", () => {
@@ -7,6 +7,7 @@ describe("getNavigationData", () => {
7
7
  const context = {
8
8
  var: {
9
9
  publicPath: "/",
10
+ currentSite: { id: "sit_navigation" },
10
11
  appConfig: {
11
12
  siteName: "Jant",
12
13
  sitePathPrefix: "",
@@ -34,8 +35,9 @@ describe("getNavigationData", () => {
34
35
 
35
36
  expect(result.isAuthenticated).toBe(false);
36
37
  expect(result.collections).toEqual([]);
37
- expect(result.siteFooterHtml).toContain('<label for="sn-');
38
- expect(result.siteFooterHtml).toContain('<span class="sidenote">');
38
+ expect(result.siteFooterHtml).toContain('role="doc-noteref"');
39
+ expect(result.siteFooterHtml).toContain('role="doc-endnotes"');
40
+ expect(result.siteFooterHtml).toContain("fn-2wpij13sz70fe-1");
39
41
  expect(result.siteFooterHtml).toContain("<strong>note</strong>");
40
42
  expect(result.siteFooterHtml).toContain(
41
43
  "&lt;script&gt;alert(1)&lt;/script&gt;",
@@ -96,4 +98,59 @@ describe("getNavigationData", () => {
96
98
  expect(result.links[0]?.url).toBe("/featured");
97
99
  expect(result.links[1]?.url).toBe("/");
98
100
  });
101
+
102
+ it("hides only the built-in RSS item when feeds are disabled", async () => {
103
+ const context = {
104
+ var: {
105
+ publicPath: "/",
106
+ appConfig: {
107
+ siteName: "Jant",
108
+ sitePathPrefix: "",
109
+ siteOrigin: "https://example.com",
110
+ siteDescription: "",
111
+ siteDescriptionExplicit: false,
112
+ siteAvatarUrl: "",
113
+ showHeaderAvatar: false,
114
+ siteFooter: "",
115
+ rssFeedsEnabled: false,
116
+ },
117
+ services: {
118
+ navItems: {
119
+ list: async () => [
120
+ {
121
+ id: "nav_rss",
122
+ type: "system",
123
+ systemKey: "rss",
124
+ label: "Feed",
125
+ url: "/feed",
126
+ placement: "header",
127
+ position: "a0",
128
+ createdAt: 1,
129
+ updatedAt: 1,
130
+ },
131
+ {
132
+ id: "nav_custom",
133
+ type: "link",
134
+ label: "Custom feed link",
135
+ url: "/feed",
136
+ placement: "header",
137
+ position: "a1",
138
+ createdAt: 1,
139
+ updatedAt: 1,
140
+ },
141
+ ],
142
+ },
143
+ collections: {
144
+ listByRecentActivity: async () => [],
145
+ },
146
+ },
147
+ isAuthenticated: false,
148
+ session: null,
149
+ },
150
+ } as unknown as Context;
151
+
152
+ const result = await getNavigationData(context);
153
+
154
+ expect(result.links.map((link) => link.id)).toEqual(["nav_custom"]);
155
+ });
99
156
  });