livemark 0.25.0 → 0.27.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 (427) hide show
  1. package/README.md +3 -2
  2. package/build/actions/config/define.d.ts +12 -0
  3. package/build/actions/config/define.unit.js +8 -1
  4. package/build/actions/escape/copy.d.ts +11 -0
  5. package/build/actions/escape/copy.js +29 -0
  6. package/build/actions/escape/copy.unit.d.ts +1 -0
  7. package/build/actions/escape/copy.unit.js +37 -0
  8. package/build/actions/escape/list.d.ts +12 -0
  9. package/build/actions/escape/list.js +27 -0
  10. package/build/actions/escape/list.unit.d.ts +1 -0
  11. package/build/actions/escape/list.unit.js +36 -0
  12. package/build/actions/target/build.d.ts +9 -26
  13. package/build/actions/target/build.js +50 -71
  14. package/build/actions/target/build.unit.d.ts +1 -0
  15. package/build/actions/target/build.unit.js +76 -0
  16. package/build/actions/target/clear.d.ts +4 -0
  17. package/build/actions/target/clear.js +14 -0
  18. package/build/actions/target/clear.unit.d.ts +1 -0
  19. package/build/actions/target/clear.unit.js +23 -0
  20. package/build/commands/build.js +1 -2
  21. package/build/commands/clear.d.ts +5 -0
  22. package/build/commands/clear.js +15 -0
  23. package/build/commands/escape.d.ts +4 -3
  24. package/build/commands/escape.js +45 -48
  25. package/build/commands/preview.js +1 -2
  26. package/build/commands/start.js +1 -2
  27. package/build/components/Abbr.d.ts +1 -1
  28. package/build/components/Abbr.js +2 -4
  29. package/build/components/AnsiCode.d.ts +1 -1
  30. package/build/components/AnsiCode.js +2 -2
  31. package/build/components/Article.d.ts +2 -1
  32. package/build/components/Article.js +7 -14
  33. package/build/components/BackToTop.d.ts +1 -1
  34. package/build/components/Banner.d.ts +2 -2
  35. package/build/components/Banner.js +7 -3
  36. package/build/components/BlogIndex.d.ts +1 -1
  37. package/build/components/BlogIndex.js +2 -11
  38. package/build/components/Breadcrumbs.d.ts +1 -1
  39. package/build/components/Breadcrumbs.js +2 -3
  40. package/build/components/Callout.d.ts +1 -1
  41. package/build/components/Callout.js +5 -9
  42. package/build/components/Card.d.ts +1 -1
  43. package/build/components/Card.js +2 -3
  44. package/build/components/Cards.d.ts +1 -1
  45. package/build/components/ChangelogIndex.d.ts +1 -1
  46. package/build/components/ChangelogIndex.js +2 -11
  47. package/build/components/ChoiceList.d.ts +15 -0
  48. package/build/components/ChoiceList.js +13 -0
  49. package/build/components/CodeBlock.d.ts +1 -1
  50. package/build/components/CodeBlock.js +3 -16
  51. package/build/components/CodeTabs.d.ts +1 -1
  52. package/build/components/CodeTabs.js +2 -3
  53. package/build/components/Column.d.ts +1 -1
  54. package/build/components/Columns.d.ts +1 -1
  55. package/build/components/Content.d.ts +1 -1
  56. package/build/components/ContentTab.d.ts +1 -1
  57. package/build/components/ContentTabs.d.ts +1 -1
  58. package/build/components/ContentTabs.js +2 -3
  59. package/build/components/DefaultCatchBoundary.d.ts +1 -1
  60. package/build/components/DefaultCatchBoundary.js +5 -7
  61. package/build/components/DefinitionList.d.ts +3 -3
  62. package/build/components/Details.d.ts +1 -1
  63. package/build/components/Details.js +2 -5
  64. package/build/components/FileTree.d.ts +1 -1
  65. package/build/components/FileTree.js +3 -5
  66. package/build/components/Footer.d.ts +1 -1
  67. package/build/components/Footer.js +2 -5
  68. package/build/components/FootnoteRef.d.ts +1 -1
  69. package/build/components/FootnoteRef.js +2 -4
  70. package/build/components/Header.d.ts +6 -0
  71. package/build/components/Header.js +26 -0
  72. package/build/components/HeaderMenu.d.ts +2 -0
  73. package/build/components/HeaderMenu.js +27 -0
  74. package/build/components/HeaderNav.d.ts +6 -0
  75. package/build/components/HeaderNav.js +74 -0
  76. package/build/components/HeaderSearch.d.ts +4 -0
  77. package/build/components/HeaderSearch.js +33 -0
  78. package/build/components/Heading.d.ts +6 -6
  79. package/build/components/InlineBadge.d.ts +2 -2
  80. package/build/components/InlineBadge.js +1 -1
  81. package/build/components/InlineIcon.d.ts +1 -1
  82. package/build/components/InlineToc.d.ts +1 -1
  83. package/build/components/InlineToc.js +2 -4
  84. package/build/components/Layout.d.ts +2 -1
  85. package/build/components/Layout.js +5 -58
  86. package/build/components/LinkButton.d.ts +2 -2
  87. package/build/components/LinkButton.js +1 -1
  88. package/build/components/MenuSheet.d.ts +22 -0
  89. package/build/components/MenuSheet.js +27 -0
  90. package/build/components/Mermaid.d.ts +1 -1
  91. package/build/components/MermaidRenderer.d.ts +1 -1
  92. package/build/components/MermaidRenderer.js +20 -8
  93. package/build/components/NotFound.d.ts +1 -1
  94. package/build/components/NotFound.js +3 -6
  95. package/build/components/PackageTabs.d.ts +1 -1
  96. package/build/components/PackageTabs.js +3 -7
  97. package/build/components/PageToolbar.d.ts +1 -1
  98. package/build/components/PageToolbar.js +2 -10
  99. package/build/components/PrevNext.d.ts +1 -1
  100. package/build/components/PrevNext.js +2 -10
  101. package/build/components/Root.d.ts +4 -0
  102. package/build/components/Root.js +8 -0
  103. package/build/components/Search.d.ts +5 -2
  104. package/build/components/Search.js +11 -9
  105. package/build/components/SearchDialog.d.ts +1 -1
  106. package/build/components/SearchDialog.js +10 -133
  107. package/build/components/Section.d.ts +1 -1
  108. package/build/components/SectionHeader.d.ts +1 -1
  109. package/build/components/SheetPanel.d.ts +12 -0
  110. package/build/components/SheetPanel.js +19 -0
  111. package/build/components/Sidebar.d.ts +1 -1
  112. package/build/components/Sidebar.js +12 -14
  113. package/build/components/SidebarArticles.d.ts +4 -1
  114. package/build/components/SidebarArticles.js +5 -9
  115. package/build/components/SidebarLinks.d.ts +4 -1
  116. package/build/components/SidebarLinks.js +6 -9
  117. package/build/components/SidebarPosts.d.ts +4 -1
  118. package/build/components/SidebarPosts.js +7 -23
  119. package/build/components/SidebarReleases.d.ts +4 -1
  120. package/build/components/SidebarReleases.js +5 -15
  121. package/build/components/SidebarSectionPicker.d.ts +8 -0
  122. package/build/components/SidebarSectionPicker.js +31 -0
  123. package/build/components/SidebarSections.d.ts +1 -1
  124. package/build/components/SidebarSections.js +5 -9
  125. package/build/components/SiteLink.d.ts +1 -1
  126. package/build/components/SiteTitle.d.ts +1 -1
  127. package/build/components/SiteTitle.js +2 -6
  128. package/build/components/SortableTable.d.ts +1 -1
  129. package/build/components/SoundCloud.d.ts +1 -1
  130. package/build/components/SoundCloud.js +2 -2
  131. package/build/components/TagIndex.d.ts +1 -1
  132. package/build/components/TagIndex.js +2 -11
  133. package/build/components/Theme.d.ts +2 -1
  134. package/build/components/Theme.js +35 -15
  135. package/build/components/Toc.d.ts +5 -4
  136. package/build/components/Toc.js +35 -35
  137. package/build/components/YouTube.d.ts +1 -1
  138. package/build/components/YouTube.js +2 -2
  139. package/build/components/ZoomImage.d.ts +1 -1
  140. package/build/content/article.js +4 -11
  141. package/build/content-collections.js +6 -4
  142. package/build/elements/accordion.d.ts +4 -4
  143. package/build/elements/accordion.js +2 -3
  144. package/build/elements/alert-dialog.d.ts +12 -12
  145. package/build/elements/alert-dialog.js +2 -4
  146. package/build/elements/alert.d.ts +4 -4
  147. package/build/elements/alert.js +2 -2
  148. package/build/elements/aspect-ratio.d.ts +1 -1
  149. package/build/elements/avatar.d.ts +6 -6
  150. package/build/elements/badge.d.ts +1 -1
  151. package/build/elements/badge.js +3 -1
  152. package/build/elements/breadcrumb.d.ts +6 -6
  153. package/build/elements/breadcrumb.js +2 -4
  154. package/build/elements/button-group.d.ts +2 -2
  155. package/build/elements/button-group.js +5 -5
  156. package/build/elements/button.d.ts +2 -2
  157. package/build/elements/button.js +9 -7
  158. package/build/elements/calendar.d.ts +2 -2
  159. package/build/elements/calendar.js +2 -2
  160. package/build/elements/card.d.ts +7 -7
  161. package/build/elements/card.js +4 -4
  162. package/build/elements/carousel.d.ts +5 -5
  163. package/build/elements/carousel.js +3 -7
  164. package/build/elements/chart.d.ts +6 -6
  165. package/build/elements/chart.js +5 -11
  166. package/build/elements/checkbox.d.ts +1 -1
  167. package/build/elements/collapsible.d.ts +3 -3
  168. package/build/elements/command.d.ts +9 -9
  169. package/build/elements/command.js +5 -11
  170. package/build/elements/context-menu.d.ts +15 -15
  171. package/build/elements/context-menu.js +6 -9
  172. package/build/elements/dialog.d.ts +10 -10
  173. package/build/elements/dialog.js +2 -6
  174. package/build/elements/drawer.d.ts +14 -12
  175. package/build/elements/drawer.js +14 -15
  176. package/build/elements/dropdown-menu.d.ts +15 -15
  177. package/build/elements/dropdown-menu.js +7 -10
  178. package/build/elements/empty.d.ts +6 -6
  179. package/build/elements/empty.js +2 -2
  180. package/build/elements/field.d.ts +10 -10
  181. package/build/elements/field.js +3 -4
  182. package/build/elements/hover-card.d.ts +3 -3
  183. package/build/elements/hover-card.js +2 -2
  184. package/build/elements/input-group.d.ts +7 -7
  185. package/build/elements/input-group.js +5 -5
  186. package/build/elements/input-otp.d.ts +4 -4
  187. package/build/elements/input.d.ts +1 -1
  188. package/build/elements/input.js +2 -2
  189. package/build/elements/item.d.ts +9 -9
  190. package/build/elements/item.js +2 -2
  191. package/build/elements/kbd.d.ts +2 -2
  192. package/build/elements/label.d.ts +1 -1
  193. package/build/elements/menubar.d.ts +16 -16
  194. package/build/elements/menubar.js +9 -11
  195. package/build/elements/native-select.d.ts +3 -3
  196. package/build/elements/native-select.js +2 -4
  197. package/build/elements/navigation-menu.d.ts +8 -8
  198. package/build/elements/navigation-menu.js +3 -5
  199. package/build/elements/pagination.d.ts +7 -7
  200. package/build/elements/pagination.js +4 -10
  201. package/build/elements/popover.d.ts +6 -6
  202. package/build/elements/popover.js +2 -2
  203. package/build/elements/progress.d.ts +5 -5
  204. package/build/elements/progress.js +2 -3
  205. package/build/elements/radio-group.d.ts +2 -2
  206. package/build/elements/resizable.d.ts +3 -3
  207. package/build/elements/scroll-area.d.ts +2 -2
  208. package/build/elements/scroll-area.js +2 -4
  209. package/build/elements/select.d.ts +9 -9
  210. package/build/elements/select.js +4 -9
  211. package/build/elements/separator.d.ts +1 -1
  212. package/build/elements/sheet.d.ts +8 -8
  213. package/build/elements/sheet.js +2 -6
  214. package/build/elements/sidebar.d.ts +19 -19
  215. package/build/elements/sidebar.js +15 -37
  216. package/build/elements/skeleton.d.ts +1 -1
  217. package/build/elements/slider.d.ts +2 -1
  218. package/build/elements/slider.js +2 -3
  219. package/build/elements/sonner.d.ts +1 -1
  220. package/build/elements/spinner.d.ts +1 -1
  221. package/build/elements/switch.d.ts +1 -1
  222. package/build/elements/table.d.ts +8 -8
  223. package/build/elements/tabs.d.ts +4 -4
  224. package/build/elements/tabs.js +3 -3
  225. package/build/elements/textarea.d.ts +1 -1
  226. package/build/elements/textarea.js +2 -2
  227. package/build/elements/toggle-group.d.ts +3 -2
  228. package/build/elements/toggle-group.js +3 -3
  229. package/build/elements/toggle.d.ts +1 -1
  230. package/build/elements/toggle.js +2 -2
  231. package/build/elements/tooltip.d.ts +4 -4
  232. package/build/elements/tooltip.js +2 -3
  233. package/build/head.d.ts +4 -0
  234. package/build/head.js +6 -0
  235. package/build/helpers/agent-skills.d.ts +22 -0
  236. package/build/helpers/agent-skills.js +46 -0
  237. package/build/helpers/agent-skills.unit.d.ts +1 -0
  238. package/build/helpers/agent-skills.unit.js +49 -0
  239. package/build/helpers/article-search.d.ts +19 -0
  240. package/build/helpers/article-search.js +134 -0
  241. package/build/helpers/article.d.ts +13 -0
  242. package/build/helpers/article.js +17 -1
  243. package/build/helpers/dependency-dir.d.ts +5 -0
  244. package/build/helpers/dependency-dir.js +20 -0
  245. package/build/helpers/dependency-dir.unit.d.ts +1 -0
  246. package/build/helpers/dependency-dir.unit.js +25 -0
  247. package/build/helpers/dynamic-icon-resolver.d.ts +1 -1
  248. package/build/helpers/dynamic-icon.d.ts +1 -1
  249. package/build/helpers/head.d.ts +58 -0
  250. package/build/helpers/head.js +80 -0
  251. package/build/helpers/head.unit.d.ts +1 -0
  252. package/build/helpers/head.unit.js +110 -0
  253. package/build/helpers/llms.d.ts +33 -0
  254. package/build/helpers/llms.js +72 -0
  255. package/build/helpers/llms.unit.d.ts +1 -0
  256. package/build/helpers/llms.unit.js +98 -0
  257. package/build/helpers/nav-items.d.ts +11 -0
  258. package/build/helpers/nav-items.js +65 -0
  259. package/build/helpers/render-inline-markdown.d.ts +1 -0
  260. package/build/helpers/render-inline-markdown.js +34 -0
  261. package/build/helpers/render-inline-markdown.unit.d.ts +1 -0
  262. package/build/helpers/render-inline-markdown.unit.js +20 -0
  263. package/build/helpers/search-highlight.d.ts +8 -0
  264. package/build/helpers/search-highlight.js +59 -0
  265. package/build/helpers/search-highlight.unit.d.ts +1 -0
  266. package/build/helpers/search-highlight.unit.js +36 -0
  267. package/build/helpers/target-export.d.ts +9 -0
  268. package/build/helpers/target-export.js +24 -0
  269. package/build/helpers/target-export.unit.d.ts +1 -0
  270. package/build/helpers/target-export.unit.js +25 -0
  271. package/build/helpers/theme.d.ts +6 -2
  272. package/build/helpers/theme.js +40 -7
  273. package/build/helpers/theme.unit.d.ts +1 -0
  274. package/build/helpers/theme.unit.js +30 -0
  275. package/build/hooks/hydrated.d.ts +2 -0
  276. package/build/hooks/hydrated.js +15 -0
  277. package/build/index.d.ts +1 -0
  278. package/build/index.js +1 -1
  279. package/build/models/config.d.ts +52 -6
  280. package/build/models/config.js +17 -1
  281. package/build/models/section.d.ts +4 -2
  282. package/build/models/section.js +1 -1
  283. package/build/plugins/vite-agents.d.ts +17 -0
  284. package/build/plugins/vite-agents.js +109 -0
  285. package/build/plugins/vite-agents.unit.d.ts +1 -0
  286. package/build/plugins/vite-agents.unit.js +20 -0
  287. package/build/plugins/vite-livemark.d.ts +4 -5
  288. package/build/plugins/vite-livemark.js +56 -90
  289. package/build/plugins/vite-robots.d.ts +4 -2
  290. package/build/plugins/vite-robots.js +15 -2
  291. package/build/plugins/vite-robots.unit.js +5 -1
  292. package/build/program.js +4 -2
  293. package/build/routes/$.d.ts +4 -1
  294. package/build/routes/$.js +20 -9
  295. package/build/routes/__root.js +29 -63
  296. package/build/settings.js +2 -1
  297. package/build/vite.config.js +3 -1
  298. package/package.json +69 -66
  299. package/{.agents/skills → skills}/livemark-authoring/SKILL.md +2 -2
  300. package/{.agents/skills → skills}/livemark-configuration/SKILL.md +16 -5
  301. package/skills/livemark-customization/SKILL.md +146 -0
  302. package/source/actions/config/define.unit.ts +9 -0
  303. package/source/actions/escape/copy.ts +35 -0
  304. package/source/actions/escape/copy.unit.ts +51 -0
  305. package/source/actions/escape/list.ts +30 -0
  306. package/source/actions/escape/list.unit.ts +42 -0
  307. package/source/actions/target/build.ts +49 -69
  308. package/source/actions/target/build.unit.ts +95 -0
  309. package/source/actions/target/clear.ts +14 -0
  310. package/source/actions/target/clear.unit.ts +25 -0
  311. package/source/commands/build.ts +0 -1
  312. package/source/commands/clear.ts +15 -0
  313. package/source/commands/escape.ts +42 -59
  314. package/source/commands/preview.ts +0 -1
  315. package/source/commands/start.ts +0 -1
  316. package/source/components/AnsiCode.tsx +1 -1
  317. package/source/components/Article.tsx +8 -4
  318. package/source/components/Banner.tsx +12 -2
  319. package/source/components/BlogIndex.tsx +1 -1
  320. package/source/components/Callout.tsx +4 -6
  321. package/source/components/ChangelogIndex.tsx +1 -1
  322. package/source/components/ChoiceList.tsx +69 -0
  323. package/source/components/CodeBlock.tsx +6 -6
  324. package/source/components/CodeTabs.tsx +1 -1
  325. package/source/components/ContentTabs.tsx +1 -1
  326. package/source/components/DefaultCatchBoundary.tsx +4 -6
  327. package/source/components/Details.tsx +1 -1
  328. package/source/components/FileTree.tsx +1 -1
  329. package/source/components/Header.tsx +52 -0
  330. package/source/components/HeaderMenu.tsx +41 -0
  331. package/source/components/HeaderNav.tsx +145 -0
  332. package/source/components/HeaderSearch.tsx +77 -0
  333. package/source/components/InlineBadge.tsx +1 -1
  334. package/source/components/InlineToc.tsx +1 -1
  335. package/source/components/Layout.tsx +16 -160
  336. package/source/components/LinkButton.tsx +9 -1
  337. package/source/components/MenuSheet.tsx +92 -0
  338. package/source/components/MermaidRenderer.tsx +19 -8
  339. package/source/components/NotFound.tsx +3 -5
  340. package/source/components/PackageTabs.tsx +2 -2
  341. package/source/components/PageToolbar.tsx +11 -3
  342. package/source/components/PrevNext.tsx +2 -2
  343. package/source/components/Root.tsx +13 -0
  344. package/source/components/Search.tsx +28 -8
  345. package/source/components/SearchDialog.tsx +15 -189
  346. package/source/components/SheetPanel.tsx +69 -0
  347. package/source/components/Sidebar.tsx +27 -14
  348. package/source/components/SidebarArticles.tsx +4 -3
  349. package/source/components/SidebarLinks.tsx +3 -2
  350. package/source/components/SidebarPosts.tsx +3 -2
  351. package/source/components/SidebarReleases.tsx +3 -2
  352. package/source/components/SidebarSectionPicker.tsx +70 -0
  353. package/source/components/SiteTitle.tsx +5 -5
  354. package/source/components/SoundCloud.tsx +1 -1
  355. package/source/components/TagIndex.tsx +1 -1
  356. package/source/components/Theme.tsx +55 -29
  357. package/source/components/Toc.tsx +76 -55
  358. package/source/components/YouTube.tsx +1 -1
  359. package/source/content/article.ts +8 -13
  360. package/source/content-collections.ts +5 -4
  361. package/source/elements/alert-dialog.tsx +1 -1
  362. package/source/elements/alert.tsx +1 -1
  363. package/source/elements/badge.tsx +4 -0
  364. package/source/elements/button-group.tsx +4 -4
  365. package/source/elements/button.tsx +10 -6
  366. package/source/elements/calendar.tsx +1 -1
  367. package/source/elements/card.tsx +3 -3
  368. package/source/elements/command.tsx +7 -4
  369. package/source/elements/context-menu.tsx +5 -5
  370. package/source/elements/dialog.tsx +1 -1
  371. package/source/elements/drawer.tsx +59 -43
  372. package/source/elements/dropdown-menu.tsx +6 -6
  373. package/source/elements/empty.tsx +1 -1
  374. package/source/elements/field.tsx +1 -1
  375. package/source/elements/hover-card.tsx +1 -1
  376. package/source/elements/input-group.tsx +4 -4
  377. package/source/elements/input.tsx +1 -1
  378. package/source/elements/item.tsx +1 -1
  379. package/source/elements/menubar.tsx +8 -8
  380. package/source/elements/native-select.tsx +1 -1
  381. package/source/elements/popover.tsx +1 -1
  382. package/source/elements/select.tsx +3 -3
  383. package/source/elements/sidebar.tsx +8 -21
  384. package/source/elements/tabs.tsx +2 -2
  385. package/source/elements/textarea.tsx +1 -1
  386. package/source/elements/toggle-group.tsx +2 -2
  387. package/source/elements/toggle.tsx +1 -1
  388. package/source/env.d.ts +1 -0
  389. package/source/head.ts +7 -0
  390. package/source/helpers/agent-skills.ts +59 -0
  391. package/source/helpers/agent-skills.unit.ts +56 -0
  392. package/source/helpers/article-search.tsx +192 -0
  393. package/source/helpers/article.ts +17 -0
  394. package/source/helpers/dependency-dir.ts +20 -0
  395. package/source/helpers/dependency-dir.unit.ts +39 -0
  396. package/source/helpers/head.ts +105 -0
  397. package/source/helpers/head.unit.ts +118 -0
  398. package/source/helpers/llms.ts +109 -0
  399. package/source/helpers/llms.unit.ts +115 -0
  400. package/source/helpers/nav-items.tsx +102 -0
  401. package/source/helpers/render-inline-markdown.ts +44 -0
  402. package/source/helpers/render-inline-markdown.unit.ts +30 -0
  403. package/source/helpers/search-highlight.ts +73 -0
  404. package/source/helpers/search-highlight.unit.ts +42 -0
  405. package/source/helpers/target-export.ts +28 -0
  406. package/source/helpers/target-export.unit.ts +37 -0
  407. package/source/helpers/theme.ts +40 -6
  408. package/source/helpers/theme.unit.ts +32 -0
  409. package/source/hooks/hydrated.ts +18 -0
  410. package/source/index.ts +1 -0
  411. package/source/models/config.ts +24 -0
  412. package/source/models/section.ts +3 -0
  413. package/source/plugins/vite-agents.ts +135 -0
  414. package/source/plugins/vite-agents.unit.ts +26 -0
  415. package/source/plugins/vite-livemark.ts +54 -95
  416. package/source/plugins/vite-robots.ts +18 -3
  417. package/source/plugins/vite-robots.unit.ts +7 -0
  418. package/source/program.ts +2 -0
  419. package/source/routes/$.tsx +27 -9
  420. package/source/routes/__root.tsx +51 -71
  421. package/source/settings.ts +1 -0
  422. package/source/styles/general.css +59 -11
  423. package/source/styles/markdown.css +13 -7
  424. package/source/styles/shape.css +14 -0
  425. package/source/vite.config.ts +2 -0
  426. package/.agents/skills/change-review/SKILL.md +0 -47
  427. package/.agents/skills/livemark-customization/SKILL.md +0 -118
@@ -0,0 +1,18 @@
1
+ import { useSyncExternalStore } from "react"
2
+
3
+ /** False during SSR and the hydrating render, true from the first client render on. */
4
+ export function useHydrated() {
5
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
6
+ }
7
+
8
+ function subscribe() {
9
+ return () => {}
10
+ }
11
+
12
+ function getSnapshot() {
13
+ return true
14
+ }
15
+
16
+ function getServerSnapshot() {
17
+ return false
18
+ }
package/source/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { defineConfig } from "./actions/config/define.ts"
2
+ export type { RootHead } from "./helpers/head.ts"
@@ -22,6 +22,24 @@ export type ViteOverride =
22
22
  * resolves it to a string before any runtime sees it. */
23
23
  export type Base = string | ((command: "build" | "serve") => string | undefined)
24
24
 
25
+ /** Extra site-wide `<head>` tags, appended after Livemark's defaults. A meta tag
26
+ * sharing a default's `name` or `property` replaces it. */
27
+ export type Head = z.infer<typeof Head>
28
+ export const Head = z.object({
29
+ meta: z.array(z.record(z.string(), z.string())).optional(),
30
+ links: z.array(z.record(z.string(), z.string())).optional(),
31
+ scripts: z.array(z.record(z.string(), z.string())).optional(),
32
+ })
33
+
34
+ /** How automated systems may use the site's content, published as a
35
+ * `Content-Signal` line in the generated `robots.txt`. */
36
+ export type ContentSignals = z.infer<typeof ContentSignals>
37
+ export const ContentSignals = z.object({
38
+ search: z.boolean().default(true),
39
+ aiInput: z.boolean().default(true),
40
+ aiTrain: z.boolean().default(true),
41
+ })
42
+
25
43
  /**
26
44
  * User-provided configuration for Livemark.
27
45
  *
@@ -46,6 +64,7 @@ export const UserConfig = z.object({
46
64
  logo: z.string().optional(),
47
65
  title: z.string().default("Livemark"),
48
66
  description: z.string().default("Markdown site generator"),
67
+ banner: z.string().optional(),
49
68
  include: z.union([z.string(), z.array(z.string())]).default("**/*.md"),
50
69
  exclude: z.union([z.string(), z.array(z.string())]).optional(),
51
70
  codeThemeLight: CodeThemeLight.default("catppuccin-latte"),
@@ -53,6 +72,9 @@ export const UserConfig = z.object({
53
72
  links: z.array(Link).optional(),
54
73
  sections: z.array(Section).optional(),
55
74
  patches: z.array(Patch).optional(),
75
+ head: Head.optional(),
76
+ contentSignals: ContentSignals.prefault({}),
77
+ skills: z.union([z.string(), z.array(z.string())]).optional(),
56
78
  // Stored as `unknown` in the schema (zod can't type a Vite UserConfig
57
79
  // or a function returning one meaningfully); the public TypeScript
58
80
  // surface above lifts it back to ViteOverride for editor support.
@@ -69,8 +91,10 @@ export const WebsiteConfig = UserConfig.pick({
69
91
  site: true,
70
92
  favicon: true,
71
93
  logo: true,
94
+ banner: true,
72
95
  links: true,
73
96
  sections: true,
97
+ head: true,
74
98
  }).extend({
75
99
  base: z.string().optional(),
76
100
  })
@@ -62,3 +62,6 @@ export const Section = z.preprocess(
62
62
  CustomSection,
63
63
  ]),
64
64
  )
65
+
66
+ /** A section that owns a URL prefix — every type except `custom`. */
67
+ export type RoutedSection = Exclude<Section, CustomSection>
@@ -0,0 +1,135 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { join, resolve } from "node:path"
3
+ import { pathToFileURL } from "node:url"
4
+ import type { Plugin } from "vite"
5
+ import { z } from "zod"
6
+ import {
7
+ buildSkillFileName,
8
+ loadAgentSkills,
9
+ renderSkillsIndex,
10
+ } from "../helpers/agent-skills.ts"
11
+ import {
12
+ buildTwinFileName,
13
+ renderLlmsText,
14
+ renderMarkdownTwin,
15
+ renderSectionIndex,
16
+ } from "../helpers/llms.ts"
17
+ import { resolveIncludes } from "../helpers/resolve-includes.ts"
18
+ import type { Config } from "../models/config.ts"
19
+
20
+ export interface AgentsOptions {
21
+ config: Config
22
+ }
23
+
24
+ const GeneratedArticles = z.array(
25
+ z.object({
26
+ path: z.string(),
27
+ title: z.string(),
28
+ description: z.string().optional(),
29
+ order: z.number().optional(),
30
+ date: z.string().optional(),
31
+ file: z.string(),
32
+ content: z.string(),
33
+ }),
34
+ )
35
+
36
+ /** Emit what AI agents look for into the client build: a Markdown twin of
37
+ * every article and section listing at `<path>index.md`, `llms.txt`, the Agent Skills index for
38
+ * `config.skills`, and a `_headers` file whose `Link` points agents at them. */
39
+ export function agents(opts: AgentsOptions): Plugin {
40
+ const publicDir = join(opts.config.root, ".livemark", "public")
41
+ let root = ""
42
+ return {
43
+ name: "livemark:agents",
44
+ applyToEnvironment(env) {
45
+ return env.name === "client"
46
+ },
47
+ configResolved(resolved) {
48
+ root = resolved.root
49
+ },
50
+ async generateBundle() {
51
+ // Vite copies the publicDir before writeBundle, so emitting a file the
52
+ // project ships under `.livemark/public/` would silently replace it.
53
+ const emit = (fileName: string, source: string | Buffer) => {
54
+ if (existsSync(join(publicDir, fileName))) return
55
+ this.emitFile({ type: "asset", fileName, source })
56
+ }
57
+
58
+ const articles = await loadGeneratedArticles(root)
59
+ for (const article of articles) {
60
+ const content = resolveIncludes(
61
+ article.content,
62
+ resolve(opts.config.root, article.file),
63
+ )
64
+ emit(buildTwinFileName(article.path), renderMarkdownTwin({ article, content }))
65
+ }
66
+
67
+ const llms = renderLlmsText({ config: opts.config, articles })
68
+ emit("llms.txt", llms)
69
+ const hasTwin = (path: string) => articles.some(a => a.path === path)
70
+ if (!hasTwin("/")) emit("index.md", llms)
71
+ // A rewrite to a missing twin is a 404, so every listing page needs one too.
72
+ for (const section of opts.config.sections ?? []) {
73
+ if (section.type === "custom" || section.prefix === "/") continue
74
+ if (hasTwin(section.prefix)) continue
75
+ emit(
76
+ buildTwinFileName(section.prefix),
77
+ renderSectionIndex({ config: opts.config, section, articles }),
78
+ )
79
+ }
80
+
81
+ const patterns = opts.config.skills ?? []
82
+ const skills = loadAgentSkills({
83
+ root: opts.config.root,
84
+ patterns: Array.isArray(patterns) ? patterns : [patterns],
85
+ })
86
+ for (const skill of skills) emit(buildSkillFileName(skill.name), skill.source)
87
+ if (skills.length) {
88
+ emit(
89
+ ".well-known/agent-skills/index.json",
90
+ renderSkillsIndex({ skills, base: opts.config.base }),
91
+ )
92
+ }
93
+
94
+ const headersFile = join(publicDir, "_headers")
95
+ this.emitFile({
96
+ type: "asset",
97
+ fileName: "_headers",
98
+ source: renderHeaders({
99
+ base: opts.config.base,
100
+ hasSkills: skills.length > 0,
101
+ existing: existsSync(headersFile) ? readFileSync(headersFile, "utf-8") : "",
102
+ }),
103
+ })
104
+ },
105
+ }
106
+ }
107
+
108
+ /** Render `_headers` (Cloudflare, Netlify): a `Link` on the home page naming
109
+ * `llms.txt`, its Markdown twin and the skills index, after any rules the
110
+ * project ships itself. */
111
+ export function renderHeaders(props: {
112
+ base?: string
113
+ hasSkills: boolean
114
+ existing?: string
115
+ }) {
116
+ const base = props.base ?? ""
117
+ const links = [
118
+ `<${base}/llms.txt>; rel="describedby"; type="text/plain"`,
119
+ `<${base}/index.md>; rel="alternate"; type="text/markdown"`,
120
+ ]
121
+ if (props.hasSkills) {
122
+ links.push(
123
+ `<${base}/.well-known/agent-skills/index.json>; rel="describedby"; type="application/json"`,
124
+ )
125
+ }
126
+ const rule = `${base}/\n Link: ${links.join(", ")}\n`
127
+ const existing = props.existing?.trimEnd()
128
+ return existing ? `${existing}\n\n${rule}` : rule
129
+ }
130
+
131
+ async function loadGeneratedArticles(root: string) {
132
+ const file = join(root, ".content-collections", "generated", "allArticles.js")
133
+ const module: { default: unknown } = await import(pathToFileURL(file).href)
134
+ return GeneratedArticles.parse(module.default)
135
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { renderHeaders } from "./vite-agents.ts"
3
+
4
+ describe("renderHeaders", () => {
5
+ it("links the home page to llms.txt and its markdown twin", () => {
6
+ expect(renderHeaders({ hasSkills: false })).toBe(
7
+ '/\n Link: </llms.txt>; rel="describedby"; type="text/plain", </index.md>; rel="alternate"; type="text/markdown"\n',
8
+ )
9
+ })
10
+
11
+ it("adds the skills index and mounts everything under base", () => {
12
+ const headers = renderHeaders({ base: "/repo", hasSkills: true })
13
+ expect(headers.startsWith("/repo/\n Link: </repo/llms.txt>")).toBe(true)
14
+ expect(headers).toContain(
15
+ '</repo/.well-known/agent-skills/index.json>; rel="describedby"; type="application/json"',
16
+ )
17
+ })
18
+
19
+ it("keeps the project's own rules ahead of the generated one", () => {
20
+ const headers = renderHeaders({
21
+ hasSkills: false,
22
+ existing: "/*\n X-Frame-Options: DENY\n\n",
23
+ })
24
+ expect(headers.startsWith("/*\n X-Frame-Options: DENY\n\n/\n Link: ")).toBe(true)
25
+ })
26
+ })
@@ -1,9 +1,11 @@
1
- import { cpSync, existsSync, rmSync } from "node:fs"
1
+ import { cpSync, existsSync, rmSync, statSync } from "node:fs"
2
2
  import { join, sep } from "node:path"
3
- import { watch as chokidarWatch } from "chokidar"
4
- import type { ConfigEnv, Plugin, UserConfig as ViteUserConfig, ViteDevServer } from "vite"
3
+ import type { ConfigEnv, Plugin, UserConfig as ViteUserConfig } from "vite"
5
4
  import { mergeConfig } from "vite"
5
+ import packageJson from "../../package.json" with { type: "json" }
6
6
  import { buildTarget } from "../actions/target/build.ts"
7
+ import { renderInlineMarkdown } from "../helpers/render-inline-markdown.ts"
8
+ import { resolveTargetExport } from "../helpers/target-export.ts"
7
9
  import type { Config } from "../models/config.ts"
8
10
  import { WebsiteConfig } from "../models/config.ts"
9
11
  import { ESCAPABLE_FILES, ESCAPABLE_FOLDERS } from "../settings.ts"
@@ -13,13 +15,15 @@ export interface LivemarkOptions {
13
15
  }
14
16
 
15
17
  const SOURCE_DIR = join(import.meta.dirname, "..")
18
+ const IS_INSTALLED = SOURCE_DIR.includes(`${sep}node_modules${sep}`)
16
19
 
17
20
  /** Single virtual module exposing livemark's runtime values to consumer
18
21
  * override files via named exports:
19
22
  *
20
23
  * import { config } from "livemark:virtual"
21
24
  *
22
- * Currently exports `config` (validated `WebsiteConfig`); future
25
+ * Currently exports `config` (validated `WebsiteConfig`) and `bannerHtml`
26
+ * (`config.banner` rendered from inline Markdown at build time); future
23
27
  * additions (e.g. `articles`, `sections`) land here as new named
24
28
  * exports without consumers changing their import statements.
25
29
  *
@@ -29,33 +33,36 @@ const SOURCE_DIR = join(import.meta.dirname, "..")
29
33
  const VIRTUAL_ID = "livemark:virtual"
30
34
  const VIRTUAL_RESOLVED_ID = `\0${VIRTUAL_ID}`
31
35
 
32
- /** Owns the per-consumer `targets/<hash>/` mount: synthesizes it via
36
+ /** Owns the project's `.livemark/.target/` mount: synthesizes it via
33
37
  * `buildTarget()`, returns the Vite config that anchors there, and
34
- * watches the consumer's `.livemark/` (plus livemark's `source/` in
35
- * dev) for changes that need to be re-copied into the target. The
36
- * override mechanism is pure file-system (full copies) — no resolveId
37
- * redirection, no realpath surprises. */
38
+ * copies `.livemark/` edits (plus livemark's `source/` in its own dev)
39
+ * into the target. Vite watches the target natively, so its own HMR
40
+ * decides between Fast Refresh, CSS updates and a page reload. */
38
41
  export function livemark(opts: LivemarkOptions): Plugin {
39
42
  const overridesRoot = join(opts.config.root, ".livemark")
43
+ const outDir = join(overridesRoot, "build")
40
44
  let targetDir = ""
41
45
  return {
42
46
  name: "livemark",
43
47
  enforce: "pre",
44
48
  async config(_, env) {
45
- targetDir = buildTarget(opts.config.configPath)
49
+ targetDir = buildTarget({
50
+ configPath: opts.config.configPath,
51
+ sourceDir: SOURCE_DIR,
52
+ })
46
53
  const base: ViteUserConfig = {
47
54
  root: targetDir,
55
+ // The default `<root>/node_modules/.vite` would land inside the
56
+ // dependency link, i.e. in livemark's shared install.
57
+ cacheDir: join(targetDir, ".vite"),
48
58
  // Mount everything (asset URLs, modulepreloads, dev server) under
49
59
  // `config.base` so the same build works at root or at a sub-path
50
60
  // (e.g. GitHub Pages `https://user.github.io/repo/`). The router
51
61
  // and asset-path resolver mirror this at runtime.
52
62
  base: opts.config.base ? `${opts.config.base}/` : "/",
53
- // outDir lives outside the target root (in the consumer's
54
- // .livemark/) so the consumer can find their deployable output
55
- // without digging into node_modules. Vite's default refusal to
56
- // empty out-of-root directories doesn't apply here because the
57
- // target/ root is livemark's, not the consumer's.
58
- build: { outDir: join(overridesRoot, "build"), emptyOutDir: true },
63
+ // outDir lives outside the target root, so Vite's refusal to
64
+ // empty out-of-root directories needs `emptyOutDir`.
65
+ build: { outDir, emptyOutDir: true },
59
66
  resolve: { dedupe: ["react", "react-dom"] },
60
67
  // For the SSR build only, inline every node_modules dep into
61
68
  // server.js. The prerender step (which runs server.js with
@@ -72,62 +79,44 @@ export function livemark(opts: LivemarkOptions): Plugin {
72
79
  },
73
80
  resolveId(id) {
74
81
  if (id === VIRTUAL_ID) return VIRTUAL_RESOLVED_ID
75
- return null
82
+ return resolveTargetExport({ id, targetDir, exports: packageJson.exports }) ?? null
76
83
  },
77
84
  load(id) {
78
85
  if (id !== VIRTUAL_RESOLVED_ID) return null
79
86
  const config = WebsiteConfig.parse(opts.config)
80
- return `export const config = ${JSON.stringify(config)}`
87
+ const bannerHtml = config.banner
88
+ ? renderInlineMarkdown(config.banner, config.base)
89
+ : undefined
90
+ return [
91
+ `export const config = ${JSON.stringify(config)}`,
92
+ `export const bannerHtml = ${JSON.stringify(bannerHtml)}`,
93
+ ].join("\n")
81
94
  },
82
95
  configureServer(server) {
83
- // Two reasons we don't use Vite's own `server.watcher`:
84
- // 1. Vite 8 bundles chokidar 3 with `disableGlobbing: true`, and
85
- // paths added via `server.watcher.add()` for surfaces outside
86
- // Vite's root never fire — getWatched() reports them but no
87
- // events arrive for external editors.
88
- // 2. Vite's chokidar ignores `**\/node_modules/**` by default, so
89
- // even if (1) were fixed, our targetDir (which lives inside
90
- // livemark's own node_modules in consumer projects) would be
91
- // silently filtered. We emit the change event manually onto
92
- // `server.watcher` after cpSync — see {@link notifyVite}.
93
- // A dedicated chokidar 5 watcher set up directly on these paths
94
- // fires reliably; we forward events into Vite's HMR pipeline below.
95
- const overlayPaths: string[] = []
96
- for (const folder of ESCAPABLE_FOLDERS) {
97
- overlayPaths.push(join(overridesRoot, folder))
98
- }
99
- for (const file of ESCAPABLE_FILES) {
100
- overlayPaths.push(join(overridesRoot, file))
101
- }
102
- const watcher = chokidarWatch(
103
- [opts.config.configPath, ...overlayPaths, SOURCE_DIR],
104
- { ignoreInitial: true, ignorePermissionErrors: true },
105
- )
106
- watcher.on("change", file => onChange(file, "change"))
107
- watcher.on("add", file => onChange(file, "add"))
108
- watcher.on("unlink", file => onChange(file, "unlink"))
109
- const closeWatcher = () => {
110
- watcher.close()
111
- }
112
- server.httpServer?.once("close", closeWatcher)
96
+ // Vite watches only its root (the target); the files the target is
97
+ // copied from join its watcher here. `.livemark/` always exists
98
+ // because the target lives inside it.
99
+ server.watcher.add([opts.config.configPath, overridesRoot])
100
+ if (!IS_INSTALLED) server.watcher.add(SOURCE_DIR)
101
+ server.watcher.on("change", file => onChange(file, "change"))
102
+ server.watcher.on("add", file => onChange(file, "add"))
103
+ server.watcher.on("unlink", file => onChange(file, "unlink"))
104
+ server.watcher.on("unlinkDir", file => onChange(file, "unlink"))
113
105
 
114
106
  function onChange(file: string, kind: "change" | "add" | "unlink") {
115
107
  if (file === opts.config.configPath) {
116
108
  server.restart()
117
109
  return
118
110
  }
119
- const overlayHit = matchOverlay(file, overridesRoot)
111
+ if (isWithin(file, targetDir) || isWithin(file, outDir)) return
112
+ const overlayHit = matchPath(file, overridesRoot)
120
113
  if (overlayHit) {
121
114
  syncOverlay(overlayHit, kind, targetDir, overridesRoot)
122
- notifyVite(server, join(targetDir, overlayHit.rel), kind)
123
- if (kind !== "change") fullReload(server)
124
115
  return
125
116
  }
126
- const sourceHit = matchSource(file)
127
- if (sourceHit) {
117
+ const sourceHit = matchPath(file, SOURCE_DIR)
118
+ if (sourceHit && !sourceHit.rel.includes("routeTree.gen.ts")) {
128
119
  syncSource(sourceHit, kind, targetDir, overridesRoot)
129
- notifyVite(server, join(targetDir, sourceHit.rel), kind)
130
- if (kind !== "change") fullReload(server)
131
120
  }
132
121
  }
133
122
  },
@@ -139,21 +128,14 @@ interface Hit {
139
128
  category: "folder" | "file" | null
140
129
  }
141
130
 
142
- function matchOverlay(file: string, overridesRoot: string): Hit | null {
143
- const prefix = `${overridesRoot}${sep}`
144
- if (!file.startsWith(prefix)) return null
145
- const rel = file.slice(prefix.length)
131
+ function matchPath(file: string, root: string): Hit | null {
132
+ if (!isWithin(file, root) || file === root) return null
133
+ const rel = file.slice(root.length + sep.length)
146
134
  return { rel, category: classify(rel) }
147
135
  }
148
136
 
149
- function matchSource(file: string): Hit | null {
150
- const prefix = `${SOURCE_DIR}${sep}`
151
- if (!file.startsWith(prefix)) return null
152
- // Skip the targets/ output dir itself (sibling of source/) and any
153
- // `.gen.ts` files that downstream tools regenerate inside the target.
154
- const rel = file.slice(prefix.length)
155
- if (rel === "" || rel.includes("routeTree.gen.ts")) return null
156
- return { rel, category: classify(rel) }
137
+ function isWithin(file: string, dir: string) {
138
+ return file === dir || file.startsWith(`${dir}${sep}`)
157
139
  }
158
140
 
159
141
  function classify(rel: string): "folder" | "file" | null {
@@ -175,7 +157,11 @@ function syncOverlay(
175
157
  const dest = join(targetDir, hit.rel)
176
158
  const source = join(SOURCE_DIR, hit.rel)
177
159
  if (kind === "unlink") {
178
- rmSync(dest, { force: true, recursive: true })
160
+ // Copy a file's default over the escape in one write: an unlink then
161
+ // an add reaches Vite as two events and costs a page reload.
162
+ if (!existsSync(source) || statSync(source).isDirectory()) {
163
+ rmSync(dest, { force: true, recursive: true })
164
+ }
179
165
  if (existsSync(source)) cpSync(source, dest, { recursive: true, force: true })
180
166
  return
181
167
  }
@@ -201,33 +187,6 @@ function syncSource(
201
187
  if (existsSync(source)) cpSync(source, dest, { recursive: true, force: true })
202
188
  }
203
189
 
204
- function fullReload(server: ViteDevServer) {
205
- server.ws.send({ type: "full-reload" })
206
- }
207
-
208
- /** Drive Vite's HMR pipeline manually for a target file we just synced.
209
- * Vite's chokidar ignores `**\/node_modules/**` by default and our
210
- * targetDir lives inside livemark's own node_modules in consumer
211
- * projects, so cpSync writes there are silently filtered out.
212
- *
213
- * Emitting on `server.watcher` runs Vite's HMR analysis per environment
214
- * (CSS gets `hmr update`, route modules trigger `(ssr) page reload`).
215
- * But `(ssr) page reload` only sends `full-reload` on the SSR
216
- * environment's hot channel — the browser is connected to the *client*
217
- * channel and never sees it. So we also fire `full-reload` on the
218
- * client hot directly, which is what TanStack Start needs for route
219
- * edits to actually refresh the page. CSS HMR still works because
220
- * Vite's analysis runs first and updates already-flushed before our
221
- * reload. */
222
- function notifyVite(
223
- server: ViteDevServer,
224
- targetFile: string,
225
- kind: "change" | "add" | "unlink",
226
- ) {
227
- server.watcher.emit(kind, targetFile)
228
- server.environments.client.hot.send({ type: "full-reload" })
229
- }
230
-
231
190
  /** Resolve livemark.config.ts's `vite` field — either a config object
232
191
  * or a function `(env) => config` (matching Vite's own `defineConfig`).
233
192
  * Schema stores it as `unknown`; typing for the consumer's editor
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { join } from "node:path"
3
3
  import type { Plugin } from "vite"
4
- import type { Config } from "../models/config.ts"
4
+ import type { Config, ContentSignals } from "../models/config.ts"
5
5
 
6
6
  export interface RobotsOptions {
7
7
  config: Config
@@ -35,8 +35,13 @@ export function robots(opts: RobotsOptions): Plugin {
35
35
  * line is appended only when `site` is set, because the directive
36
36
  * requires a fully-qualified URL; `base` is included so the URL matches
37
37
  * where the build (and the `sitemap.xml` inside it) is actually mounted. */
38
- export function renderRobots(config: Pick<Config, "site" | "base">) {
39
- const lines = ["User-agent: *", "Allow: /"]
38
+ export function renderRobots(
39
+ config: Pick<Config, "site" | "base"> & { contentSignals?: ContentSignals },
40
+ ) {
41
+ const lines = ["User-agent: *"]
42
+ // Agents read the signal as part of a user-agent group, so it must sit inside one.
43
+ if (config.contentSignals) lines.push(renderContentSignal(config.contentSignals))
44
+ lines.push("Allow: /")
40
45
  if (config.site) {
41
46
  const host = config.site.replace(/\/+$/, "")
42
47
  const base = config.base ?? ""
@@ -44,3 +49,13 @@ export function renderRobots(config: Pick<Config, "site" | "base">) {
44
49
  }
45
50
  return `${lines.join("\n")}\n`
46
51
  }
52
+
53
+ function renderContentSignal(signals: ContentSignals) {
54
+ const flag = (value: boolean) => (value ? "yes" : "no")
55
+ const pairs = [
56
+ `search=${flag(signals.search)}`,
57
+ `ai-input=${flag(signals.aiInput)}`,
58
+ `ai-train=${flag(signals.aiTrain)}`,
59
+ ]
60
+ return `Content-Signal: ${pairs.join(", ")}`
61
+ }
@@ -27,4 +27,11 @@ describe("renderRobots", () => {
27
27
  it("ignores base when site is not set", () => {
28
28
  expect(renderRobots({ base: "/repo" })).toBe("User-agent: *\nAllow: /\n")
29
29
  })
30
+
31
+ it("puts the content signals inside the wildcard group", () => {
32
+ const contentSignals = { search: true, aiInput: true, aiTrain: false }
33
+ expect(renderRobots({ contentSignals })).toBe(
34
+ "User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=no\nAllow: /\n",
35
+ )
36
+ })
30
37
  })
package/source/program.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander"
2
2
  import { build } from "./commands/build.ts"
3
+ import { clear } from "./commands/clear.ts"
3
4
  import { escape } from "./commands/escape.ts"
4
5
  import { preview } from "./commands/preview.ts"
5
6
  import { start } from "./commands/start.ts"
@@ -16,3 +17,4 @@ export const program = new Command()
16
17
  .addCommand(build)
17
18
  .addCommand(preview)
18
19
  .addCommand(escape)
20
+ .addCommand(clear)
@@ -1,4 +1,6 @@
1
+ import { config } from "livemark:virtual"
1
2
  import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
3
+ import { z } from "zod"
2
4
  import {
3
5
  currentSection,
4
6
  firstArticle,
@@ -6,12 +8,16 @@ import {
6
8
  sortedArticles,
7
9
  } from "../content/article.ts"
8
10
  import { sectionTags } from "../content/tag.ts"
11
+ import { buildPageHead } from "../helpers/head.ts"
9
12
  import { Article } from "../components/Article.tsx"
10
13
  import { BlogIndex } from "../components/BlogIndex.tsx"
11
14
  import { ChangelogIndex } from "../components/ChangelogIndex.tsx"
15
+ import { Layout } from "../components/Layout.tsx"
12
16
  import { TagIndex } from "../components/TagIndex.tsx"
13
17
 
14
18
  export const Route = createFileRoute("/$")({
19
+ // Loose, so keys this route does not own (UTM tags, devtools flags) survive.
20
+ validateSearch: z.looseObject({ highlight: z.string().optional().catch(undefined) }),
15
21
  loader: ({ params }) => {
16
22
  if (!params._splat) {
17
23
  if (homeArticle) return { ...homeArticle, sidebar: homeArticle.sidebar }
@@ -62,19 +68,31 @@ export const Route = createFileRoute("/$")({
62
68
 
63
69
  throw notFound()
64
70
  },
65
- head: ({ loaderData }) => ({
66
- meta: [
67
- ...(loaderData ? [{ title: loaderData.title }] : []),
68
- ...("description" in loaderData && loaderData.description
69
- ? [{ name: "description", content: loaderData.description }]
70
- : []),
71
- ],
72
- }),
71
+ head: ({ loaderData, params }) =>
72
+ buildPageHead({
73
+ config,
74
+ path: params._splat ? `/${params._splat}/` : "/",
75
+ title: loaderData?.title,
76
+ description:
77
+ loaderData && "description" in loaderData ? loaderData.description : undefined,
78
+ image: loaderData && "image" in loaderData ? loaderData.image : undefined,
79
+ isArticle: loaderData && "file" in loaderData,
80
+ }),
73
81
  component: Component,
74
82
  })
75
83
 
76
84
  function Component() {
77
85
  const data = Route.useLoaderData()
86
+ return (
87
+ <Layout withSidebar={data.sidebar}>
88
+ <Page />
89
+ </Layout>
90
+ )
91
+ }
92
+
93
+ function Page() {
94
+ const data = Route.useLoaderData()
95
+ const search = Route.useSearch()
78
96
  if ("blogIndex" in data) {
79
97
  return <BlogIndex sectionPrefix={data.sectionPrefix} />
80
98
  }
@@ -84,5 +102,5 @@ function Component() {
84
102
  if ("tagPage" in data) {
85
103
  return <TagIndex sectionPrefix={data.sectionPrefix} tag={data.tag} />
86
104
  }
87
- return <Article article={data} />
105
+ return <Article article={data} highlight={search.highlight} />
88
106
  }