livemark 0.26.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 (44) hide show
  1. package/build/actions/config/define.d.ts +6 -0
  2. package/build/content/article.js +4 -11
  3. package/build/content-collections.js +2 -4
  4. package/build/helpers/agent-skills.d.ts +22 -0
  5. package/build/helpers/agent-skills.js +46 -0
  6. package/build/helpers/agent-skills.unit.d.ts +1 -0
  7. package/build/helpers/agent-skills.unit.js +49 -0
  8. package/build/helpers/article.d.ts +13 -0
  9. package/build/helpers/article.js +17 -1
  10. package/build/helpers/head.d.ts +7 -2
  11. package/build/helpers/head.js +13 -2
  12. package/build/helpers/head.unit.js +4 -2
  13. package/build/helpers/llms.d.ts +33 -0
  14. package/build/helpers/llms.js +72 -0
  15. package/build/helpers/llms.unit.d.ts +1 -0
  16. package/build/helpers/llms.unit.js +98 -0
  17. package/build/models/config.d.ts +20 -0
  18. package/build/models/config.js +8 -1
  19. package/build/plugins/vite-agents.d.ts +17 -0
  20. package/build/plugins/vite-agents.js +109 -0
  21. package/build/plugins/vite-agents.unit.d.ts +1 -0
  22. package/build/plugins/vite-agents.unit.js +20 -0
  23. package/build/plugins/vite-robots.d.ts +4 -2
  24. package/build/plugins/vite-robots.js +15 -2
  25. package/build/plugins/vite-robots.unit.js +5 -1
  26. package/build/vite.config.js +3 -1
  27. package/package.json +3 -2
  28. package/skills/livemark-configuration/SKILL.md +4 -0
  29. package/skills/livemark-customization/SKILL.md +1 -0
  30. package/source/content/article.ts +8 -13
  31. package/source/content-collections.ts +1 -4
  32. package/source/helpers/agent-skills.ts +59 -0
  33. package/source/helpers/agent-skills.unit.ts +56 -0
  34. package/source/helpers/article.ts +17 -0
  35. package/source/helpers/head.ts +12 -1
  36. package/source/helpers/head.unit.ts +3 -1
  37. package/source/helpers/llms.ts +109 -0
  38. package/source/helpers/llms.unit.ts +115 -0
  39. package/source/models/config.ts +11 -0
  40. package/source/plugins/vite-agents.ts +135 -0
  41. package/source/plugins/vite-agents.unit.ts +26 -0
  42. package/source/plugins/vite-robots.ts +18 -3
  43. package/source/plugins/vite-robots.unit.ts +7 -0
  44. package/source/vite.config.ts +2 -0
@@ -6,6 +6,7 @@ import react from "@vitejs/plugin-react";
6
6
  import { defineConfig } from "vite";
7
7
  import svgr from "vite-plugin-svgr";
8
8
  import { loadConfig } from "./actions/config/load.js";
9
+ import { agents } from "./plugins/vite-agents.js";
9
10
  import { livemark } from "./plugins/vite-livemark.js";
10
11
  import { robots } from "./plugins/vite-robots.js";
11
12
  const config = await loadConfig();
@@ -25,6 +26,7 @@ export default defineConfig({
25
26
  plugins: [
26
27
  livemark({ config }),
27
28
  robots({ config }),
29
+ agents({ config }),
28
30
  devtools(),
29
31
  tailwind(),
30
32
  contentCollections({ configPath: "content-collections.ts" }),
@@ -53,4 +55,4 @@ export default defineConfig({
53
55
  svgr(),
54
56
  ],
55
57
  });
56
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvdml0ZS5jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxrQkFBa0IsTUFBTSwyQkFBMkIsQ0FBQTtBQUMxRCxPQUFPLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQTtBQUN4QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUE7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG1DQUFtQyxDQUFBO0FBQ2pFLE9BQU8sS0FBSyxNQUFNLHNCQUFzQixDQUFBO0FBQ3hDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxNQUFNLENBQUE7QUFDbkMsT0FBTyxJQUFJLE1BQU0sa0JBQWtCLENBQUE7QUFDbkMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3JELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQTtBQUNyRCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFFakQsTUFBTSxNQUFNLEdBQUcsTUFBTSxVQUFVLEVBQUUsQ0FBQTtBQUVqQyx1RUFBdUU7QUFDdkUsd0VBQXdFO0FBQ3hFLHlFQUF5RTtBQUN6RSxtREFBbUQ7QUFDbkQsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLElBQUk7SUFDL0IsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLFFBQVEsSUFBSSxFQUFFLENBQUM7U0FDcEIsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksS0FBSyxNQUFNLENBQUM7U0FDOUIsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNULElBQUksRUFBRSxHQUFHLE1BQU0sQ0FBQyxJQUFJLElBQUksRUFBRSxHQUFHLENBQUMsQ0FBQyxNQUFNLFNBQVM7UUFDOUMsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRTtLQUMzQixDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsRUFBRSxDQUFBO0FBRU4sZUFBZSxZQUFZLENBQUM7SUFDMUIsT0FBTyxFQUFFO1FBQ1AsUUFBUSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDcEIsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDbEIsUUFBUSxFQUFFO1FBQ1YsUUFBUSxFQUFFO1FBQ1Ysa0JBQWtCLENBQUMsRUFBRSxVQUFVLEVBQUUsd0JBQXdCLEVBQUUsQ0FBQztRQUM1RCxhQUFhLENBQUM7WUFDWixZQUFZLEVBQUUsR0FBRztZQUNqQixpRUFBaUU7WUFDakUsZ0VBQWdFO1lBQ2hFLHlEQUF5RDtZQUN6RCxLQUFLLEVBQUUsQ0FBQyxFQUFFLElBQUksRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsR0FBRyxhQUFhLENBQUM7WUFDMUUsZ0VBQWdFO1lBQ2hFLG9FQUFvRTtZQUNwRSxrRUFBa0U7WUFDbEUsNkJBQTZCO1lBQzdCLFNBQVMsRUFBRTtnQkFDVCxPQUFPLEVBQUUsSUFBSTtnQkFDYixVQUFVLEVBQUUsSUFBSTtnQkFDaEIsd0JBQXdCLEVBQUUsS0FBSzthQUNoQztZQUNELE9BQU8sRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1lBQ2hGLE1BQU0sRUFBRTtnQkFDTixrQkFBa0IsRUFBRSxrQkFBa0I7Z0JBQ3RDLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSTthQUN0QjtTQUNGLENBQUM7UUFDRixLQUFLLEVBQUU7UUFDUCxJQUFJLEVBQUU7S0FDUDtDQUNGLENBQUMsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBjb250ZW50Q29sbGVjdGlvbnMgZnJvbSBcIkBjb250ZW50LWNvbGxlY3Rpb25zL3ZpdGVcIlxuaW1wb3J0IHRhaWx3aW5kIGZyb20gXCJAdGFpbHdpbmRjc3Mvdml0ZVwiXG5pbXBvcnQgeyBkZXZ0b29scyB9IGZyb20gXCJAdGFuc3RhY2svZGV2dG9vbHMtdml0ZVwiXG5pbXBvcnQgeyB0YW5zdGFja1N0YXJ0IH0gZnJvbSBcIkB0YW5zdGFjay9yZWFjdC1zdGFydC9wbHVnaW4vdml0ZVwiXG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCJcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBzdmdyIGZyb20gXCJ2aXRlLXBsdWdpbi1zdmdyXCJcbmltcG9ydCB7IGxvYWRDb25maWcgfSBmcm9tIFwiLi9hY3Rpb25zL2NvbmZpZy9sb2FkLnRzXCJcbmltcG9ydCB7IGxpdmVtYXJrIH0gZnJvbSBcIi4vcGx1Z2lucy92aXRlLWxpdmVtYXJrLnRzXCJcbmltcG9ydCB7IHJvYm90cyB9IGZyb20gXCIuL3BsdWdpbnMvdml0ZS1yb2JvdHMudHNcIlxuXG5jb25zdCBjb25maWcgPSBhd2FpdCBsb2FkQ29uZmlnKClcblxuLy8gUGVyLWJsb2ctc2VjdGlvbiBSU1MgZmVlZCBwYXRocyB0byBwcmVyZW5kZXIuIEdhdGVkIG9uIGBjb25maWcuc2l0ZWBcbi8vIGJlY2F1c2UgZmVlZCBVUkxzIGFyZSBhYnNvbHV0ZSAobWF0Y2hpbmcgdGhlIHNpdGVtYXAvcm9ib3RzLnR4dCBnYXRlKVxuLy8gYW5kIHRoZSByb3V0ZSByZXR1cm5zIDQwNCB3aXRob3V0IGl0IGFueXdheS4gRXhjbHVkZWQgZnJvbSB0aGUgc2l0ZW1hcFxuLy8gc2luY2UgYC54bWxgIHBheWxvYWRzIGRvbid0IGJlbG9uZyBpbiBhIHNpdGVtYXAuXG5jb25zdCBibG9nRmVlZFBhZ2VzID0gY29uZmlnLnNpdGVcbiAgPyAoY29uZmlnLnNlY3Rpb25zID8/IFtdKVxuICAgICAgLmZpbHRlcihzID0+IHMudHlwZSA9PT0gXCJibG9nXCIpXG4gICAgICAubWFwKHMgPT4gKHtcbiAgICAgICAgcGF0aDogYCR7Y29uZmlnLmJhc2UgPz8gXCJcIn0ke3MucHJlZml4fXJzcy54bWxgLFxuICAgICAgICBzaXRlbWFwOiB7IGV4Y2x1ZGU6IHRydWUgfSxcbiAgICAgIH0pKVxuICA6IFtdXG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtcbiAgICBsaXZlbWFyayh7IGNvbmZpZyB9KSxcbiAgICByb2JvdHMoeyBjb25maWcgfSksXG4gICAgZGV2dG9vbHMoKSxcbiAgICB0YWlsd2luZCgpLFxuICAgIGNvbnRlbnRDb2xsZWN0aW9ucyh7IGNvbmZpZ1BhdGg6IFwiY29udGVudC1jb2xsZWN0aW9ucy50c1wiIH0pLFxuICAgIHRhbnN0YWNrU3RhcnQoe1xuICAgICAgc3JjRGlyZWN0b3J5OiBcIi5cIixcbiAgICAgIC8vIFNlZWQgcGF0aCBtdXN0IG1hdGNoIHdoYXQgdGhlIGNyYXdsZXIgcHJvZHVjZXMgKHJvdXRlci1lbWl0dGVkXG4gICAgICAvLyBocmVmcyBpbmNsdWRlIHRoZSBiYXNlcGF0aCksIG90aGVyd2lzZSBwcmVyZW5kZXIgcmVjb3JkcyBib3RoXG4gICAgICAvLyBzaGFwZXMgYW5kIHRoZSBzaXRlbWFwIGVuZHMgdXAgd2l0aCBkdXBsaWNhdGUgZW50cmllcy5cbiAgICAgIHBhZ2VzOiBbeyBwYXRoOiBjb25maWcuYmFzZSA/IGAke2NvbmZpZy5iYXNlfS9gIDogXCIvXCIgfSwgLi4uYmxvZ0ZlZWRQYWdlc10sXG4gICAgICAvLyBEaXNhYmxlIHJvdXRlLXRyZWUgYXV0by1kaXNjb3Zlcnk6IGl0IHByb2R1Y2VzIGJhc2VwYXRoLW5haXZlXG4gICAgICAvLyBwYXRocyAoZS5nLiBgL2ApIHRoYXQgZHVwbGljYXRlIHRoZSBjcmF3bC1kZXJpdmVkIHByZWZpeGVkIHBhdGhzLFxuICAgICAgLy8gYmxvYXRpbmcgcGFnZXMuanNvbiBhbmQgc2l0ZW1hcC4gQ3Jhd2xpbmcgZnJvbSB0aGUgc2VlZCBhbHJlYWR5XG4gICAgICAvLyByZWFjaGVzIGV2ZXJ5IGxpbmtlZCBwYWdlLlxuICAgICAgcHJlcmVuZGVyOiB7XG4gICAgICAgIGVuYWJsZWQ6IHRydWUsXG4gICAgICAgIGNyYXdsTGlua3M6IHRydWUsXG4gICAgICAgIGF1dG9TdGF0aWNQYXRoc0Rpc2NvdmVyeTogZmFsc2UsXG4gICAgICB9LFxuICAgICAgc2l0ZW1hcDogY29uZmlnLnNpdGUgPyB7IGVuYWJsZWQ6IHRydWUsIGhvc3Q6IGNvbmZpZy5zaXRlIH0gOiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gICAgICByb3V0ZXI6IHtcbiAgICAgICAgZ2VuZXJhdGVkUm91dGVUcmVlOiBcInJvdXRlVHJlZS5nZW4udHNcIixcbiAgICAgICAgYmFzZXBhdGg6IGNvbmZpZy5iYXNlLFxuICAgICAgfSxcbiAgICB9KSxcbiAgICByZWFjdCgpLFxuICAgIHN2Z3IoKSxcbiAgXSxcbn0pXG4iXX0=
58
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvdml0ZS5jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxrQkFBa0IsTUFBTSwyQkFBMkIsQ0FBQTtBQUMxRCxPQUFPLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQTtBQUN4QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUE7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG1DQUFtQyxDQUFBO0FBQ2pFLE9BQU8sS0FBSyxNQUFNLHNCQUFzQixDQUFBO0FBQ3hDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxNQUFNLENBQUE7QUFDbkMsT0FBTyxJQUFJLE1BQU0sa0JBQWtCLENBQUE7QUFDbkMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3JELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQTtBQUNqRCxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sNEJBQTRCLENBQUE7QUFDckQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBRWpELE1BQU0sTUFBTSxHQUFHLE1BQU0sVUFBVSxFQUFFLENBQUE7QUFFakMsdUVBQXVFO0FBQ3ZFLHdFQUF3RTtBQUN4RSx5RUFBeUU7QUFDekUsbURBQW1EO0FBQ25ELE1BQU0sYUFBYSxHQUFHLE1BQU0sQ0FBQyxJQUFJO0lBQy9CLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxRQUFRLElBQUksRUFBRSxDQUFDO1NBQ3BCLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssTUFBTSxDQUFDO1NBQzlCLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDVCxJQUFJLEVBQUUsR0FBRyxNQUFNLENBQUMsSUFBSSxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUMsTUFBTSxTQUFTO1FBQzlDLE9BQU8sRUFBRSxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUU7S0FDM0IsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtBQUVOLGVBQWUsWUFBWSxDQUFDO0lBQzFCLE9BQU8sRUFBRTtRQUNQLFFBQVEsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ3BCLE1BQU0sQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ2xCLE1BQU0sQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ2xCLFFBQVEsRUFBRTtRQUNWLFFBQVEsRUFBRTtRQUNWLGtCQUFrQixDQUFDLEVBQUUsVUFBVSxFQUFFLHdCQUF3QixFQUFFLENBQUM7UUFDNUQsYUFBYSxDQUFDO1lBQ1osWUFBWSxFQUFFLEdBQUc7WUFDakIsaUVBQWlFO1lBQ2pFLGdFQUFnRTtZQUNoRSx5REFBeUQ7WUFDekQsS0FBSyxFQUFFLENBQUMsRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLEdBQUcsYUFBYSxDQUFDO1lBQzFFLGdFQUFnRTtZQUNoRSxvRUFBb0U7WUFDcEUsa0VBQWtFO1lBQ2xFLDZCQUE2QjtZQUM3QixTQUFTLEVBQUU7Z0JBQ1QsT0FBTyxFQUFFLElBQUk7Z0JBQ2IsVUFBVSxFQUFFLElBQUk7Z0JBQ2hCLHdCQUF3QixFQUFFLEtBQUs7YUFDaEM7WUFDRCxPQUFPLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRTtZQUNoRixNQUFNLEVBQUU7Z0JBQ04sa0JBQWtCLEVBQUUsa0JBQWtCO2dCQUN0QyxRQUFRLEVBQUUsTUFBTSxDQUFDLElBQUk7YUFDdEI7U0FDRixDQUFDO1FBQ0YsS0FBSyxFQUFFO1FBQ1AsSUFBSSxFQUFFO0tBQ1A7Q0FDRixDQUFDLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgY29udGVudENvbGxlY3Rpb25zIGZyb20gXCJAY29udGVudC1jb2xsZWN0aW9ucy92aXRlXCJcbmltcG9ydCB0YWlsd2luZCBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIlxuaW1wb3J0IHsgZGV2dG9vbHMgfSBmcm9tIFwiQHRhbnN0YWNrL2RldnRvb2xzLXZpdGVcIlxuaW1wb3J0IHsgdGFuc3RhY2tTdGFydCB9IGZyb20gXCJAdGFuc3RhY2svcmVhY3Qtc3RhcnQvcGx1Z2luL3ZpdGVcIlxuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdFwiXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgc3ZnciBmcm9tIFwidml0ZS1wbHVnaW4tc3ZnclwiXG5pbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSBcIi4vYWN0aW9ucy9jb25maWcvbG9hZC50c1wiXG5pbXBvcnQgeyBhZ2VudHMgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtYWdlbnRzLnRzXCJcbmltcG9ydCB7IGxpdmVtYXJrIH0gZnJvbSBcIi4vcGx1Z2lucy92aXRlLWxpdmVtYXJrLnRzXCJcbmltcG9ydCB7IHJvYm90cyB9IGZyb20gXCIuL3BsdWdpbnMvdml0ZS1yb2JvdHMudHNcIlxuXG5jb25zdCBjb25maWcgPSBhd2FpdCBsb2FkQ29uZmlnKClcblxuLy8gUGVyLWJsb2ctc2VjdGlvbiBSU1MgZmVlZCBwYXRocyB0byBwcmVyZW5kZXIuIEdhdGVkIG9uIGBjb25maWcuc2l0ZWBcbi8vIGJlY2F1c2UgZmVlZCBVUkxzIGFyZSBhYnNvbHV0ZSAobWF0Y2hpbmcgdGhlIHNpdGVtYXAvcm9ib3RzLnR4dCBnYXRlKVxuLy8gYW5kIHRoZSByb3V0ZSByZXR1cm5zIDQwNCB3aXRob3V0IGl0IGFueXdheS4gRXhjbHVkZWQgZnJvbSB0aGUgc2l0ZW1hcFxuLy8gc2luY2UgYC54bWxgIHBheWxvYWRzIGRvbid0IGJlbG9uZyBpbiBhIHNpdGVtYXAuXG5jb25zdCBibG9nRmVlZFBhZ2VzID0gY29uZmlnLnNpdGVcbiAgPyAoY29uZmlnLnNlY3Rpb25zID8/IFtdKVxuICAgICAgLmZpbHRlcihzID0+IHMudHlwZSA9PT0gXCJibG9nXCIpXG4gICAgICAubWFwKHMgPT4gKHtcbiAgICAgICAgcGF0aDogYCR7Y29uZmlnLmJhc2UgPz8gXCJcIn0ke3MucHJlZml4fXJzcy54bWxgLFxuICAgICAgICBzaXRlbWFwOiB7IGV4Y2x1ZGU6IHRydWUgfSxcbiAgICAgIH0pKVxuICA6IFtdXG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtcbiAgICBsaXZlbWFyayh7IGNvbmZpZyB9KSxcbiAgICByb2JvdHMoeyBjb25maWcgfSksXG4gICAgYWdlbnRzKHsgY29uZmlnIH0pLFxuICAgIGRldnRvb2xzKCksXG4gICAgdGFpbHdpbmQoKSxcbiAgICBjb250ZW50Q29sbGVjdGlvbnMoeyBjb25maWdQYXRoOiBcImNvbnRlbnQtY29sbGVjdGlvbnMudHNcIiB9KSxcbiAgICB0YW5zdGFja1N0YXJ0KHtcbiAgICAgIHNyY0RpcmVjdG9yeTogXCIuXCIsXG4gICAgICAvLyBTZWVkIHBhdGggbXVzdCBtYXRjaCB3aGF0IHRoZSBjcmF3bGVyIHByb2R1Y2VzIChyb3V0ZXItZW1pdHRlZFxuICAgICAgLy8gaHJlZnMgaW5jbHVkZSB0aGUgYmFzZXBhdGgpLCBvdGhlcndpc2UgcHJlcmVuZGVyIHJlY29yZHMgYm90aFxuICAgICAgLy8gc2hhcGVzIGFuZCB0aGUgc2l0ZW1hcCBlbmRzIHVwIHdpdGggZHVwbGljYXRlIGVudHJpZXMuXG4gICAgICBwYWdlczogW3sgcGF0aDogY29uZmlnLmJhc2UgPyBgJHtjb25maWcuYmFzZX0vYCA6IFwiL1wiIH0sIC4uLmJsb2dGZWVkUGFnZXNdLFxuICAgICAgLy8gRGlzYWJsZSByb3V0ZS10cmVlIGF1dG8tZGlzY292ZXJ5OiBpdCBwcm9kdWNlcyBiYXNlcGF0aC1uYWl2ZVxuICAgICAgLy8gcGF0aHMgKGUuZy4gYC9gKSB0aGF0IGR1cGxpY2F0ZSB0aGUgY3Jhd2wtZGVyaXZlZCBwcmVmaXhlZCBwYXRocyxcbiAgICAgIC8vIGJsb2F0aW5nIHBhZ2VzLmpzb24gYW5kIHNpdGVtYXAuIENyYXdsaW5nIGZyb20gdGhlIHNlZWQgYWxyZWFkeVxuICAgICAgLy8gcmVhY2hlcyBldmVyeSBsaW5rZWQgcGFnZS5cbiAgICAgIHByZXJlbmRlcjoge1xuICAgICAgICBlbmFibGVkOiB0cnVlLFxuICAgICAgICBjcmF3bExpbmtzOiB0cnVlLFxuICAgICAgICBhdXRvU3RhdGljUGF0aHNEaXNjb3Zlcnk6IGZhbHNlLFxuICAgICAgfSxcbiAgICAgIHNpdGVtYXA6IGNvbmZpZy5zaXRlID8geyBlbmFibGVkOiB0cnVlLCBob3N0OiBjb25maWcuc2l0ZSB9IDogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgICAgcm91dGVyOiB7XG4gICAgICAgIGdlbmVyYXRlZFJvdXRlVHJlZTogXCJyb3V0ZVRyZWUuZ2VuLnRzXCIsXG4gICAgICAgIGJhc2VwYXRoOiBjb25maWcuYmFzZSxcbiAgICAgIH0sXG4gICAgfSksXG4gICAgcmVhY3QoKSxcbiAgICBzdmdyKCksXG4gIF0sXG59KVxuIl19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livemark",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Livemark description",
5
5
  "license": "MIT",
6
6
  "author": "Evgeny Karev",
@@ -91,6 +91,7 @@
91
91
  "unist-util-visit": "^5.1.0",
92
92
  "vite": "8.3.4",
93
93
  "vite-plugin-svgr": "5.2.0",
94
+ "yaml": "^2.9.1",
94
95
  "zod": "4.6.6"
95
96
  },
96
97
  "devDependencies": {
@@ -127,7 +128,7 @@
127
128
  "scan": "pnpm scan:leaks && pnpm scan:vulns",
128
129
  "scan:leaks": "gitleaks git --config=.gitleaks.toml --verbose --redact",
129
130
  "scan:vulns": "semgrep --config=p/typescript --config=p/nodejs --config=p/owasp-top-ten --config=p/secrets --exclude=\"**/fixtures/**\" --metrics=off",
130
- "squash": "node .scripts/squash-branch.ts",
131
+ "squash": "node scripts/squash-branch.ts",
131
132
  "start": "node source/entrypoints/main.ts start",
132
133
  "test": "pnpm check && pnpm test:unit",
133
134
  "test:unit": "vp test",
@@ -42,6 +42,10 @@ export default defineConfig({
42
42
  scripts: [{ src: "https://example.com/analytics.js", defer: "" }],
43
43
  },
44
44
 
45
+ // Optional: AI agents (llms.txt and Markdown twins are always emitted)
46
+ contentSignals: { search: true, aiInput: true, aiTrain: true }, // robots.txt Content-Signal; all default true
47
+ skills: "skills/*/SKILL.md", // publish skills at /.well-known/agent-skills/
48
+
45
49
  // Optional: content selection
46
50
  exclude: "docs/drafts/**", // string | string[]
47
51
 
@@ -25,6 +25,7 @@ Invoke when the user asks to:
25
25
  | Replace a default component/style | Drop a file at `.livemark/components/<Name>.tsx` etc. |
26
26
  | Add a new page at an exact URL | Create `.livemark/routes/<path>.tsx` |
27
27
  | Serve a static asset at `/<path>` | Put the file in `.livemark/public/<path>` |
28
+ | Extend the generated `_headers` | `.livemark/public/_headers` — kept, `Link` appended |
28
29
  | Rewrite site-wide `<head>` tags | `.livemark/head.ts` exporting `customizeHead(head)` |
29
30
 
30
31
  None require config changes. The Vite plugin (`source/plugins/vite-livemark.ts`) watches `.livemark/**` and hot-reloads.
@@ -1,7 +1,12 @@
1
1
  import { config } from "livemark:virtual"
2
2
  import { allArticles } from "content-collections"
3
3
  import type { ArticleNode } from "../models/article.ts"
4
- import { buildArticleTree, flattenArticleTree } from "../helpers/article.ts"
4
+ import {
5
+ buildArticleTree,
6
+ compareArticleDate,
7
+ compareArticleOrder,
8
+ flattenArticleTree,
9
+ } from "../helpers/article.ts"
5
10
  import { matchSection, partitionBySection } from "../helpers/section.ts"
6
11
 
7
12
  /**
@@ -9,9 +14,7 @@ import { matchSection, partitionBySection } from "../helpers/section.ts"
9
14
  * positive values first (ascending), then unordered, then negative values
10
15
  * (so `order: -1` lands at the very end, `-2` just before it, and so on).
11
16
  */
12
- export const sortedArticles = [...allArticles].sort(
13
- (a, b) => orderKey(a.order) - orderKey(b.order),
14
- )
17
+ export const sortedArticles = [...allArticles].sort(compareArticleOrder)
15
18
 
16
19
  /** The article whose path is `/`, if any (rendered in place at the root) */
17
20
  export const homeArticle = sortedArticles.find(a => a.path === "/")
@@ -25,12 +28,6 @@ export const flatArticles = flattenArticleTree(articleTree)
25
28
  /** The first article in sidebar order, used as the default landing page */
26
29
  export const firstArticle = sortedArticles.find(a => a.path === flatArticles[0])
27
30
 
28
- function orderKey(order?: number) {
29
- if (order === undefined) return Number.MAX_SAFE_INTEGER / 2
30
- if (order < 0) return Number.MAX_SAFE_INTEGER + order
31
- return order
32
- }
33
-
34
31
  const configSections = config.sections
35
32
 
36
33
  /** Route-bound sections (everything except `custom`). Custom sections
@@ -53,9 +50,7 @@ if (routedSections.length) {
53
50
  const section = sectionByPrefix.get(key)
54
51
  const sorted =
55
52
  section?.type === "blog" || section?.type === "changelog"
56
- ? [...bucket].sort(
57
- (a, b) => new Date(b.date ?? 0).getTime() - new Date(a.date ?? 0).getTime(),
58
- )
53
+ ? [...bucket].sort(compareArticleDate)
59
54
  : bucket
60
55
  const tree = buildArticleTree(sorted)
61
56
  const flat = flattenArticleTree(tree)
@@ -49,6 +49,7 @@ import { transformerIcon } from "./plugins/shiki-icon.ts"
49
49
  import { resolveAssetPath } from "./helpers/resolve-asset-path.ts"
50
50
  import { resolveIncludes } from "./helpers/resolve-includes.ts"
51
51
  import { extractSearchText } from "./helpers/search-text.ts"
52
+ import { stripLeadingH1 } from "./helpers/llms.ts"
52
53
  import { extractToc } from "./helpers/toc.ts"
53
54
 
54
55
  const config = await loadConfig()
@@ -98,10 +99,6 @@ function getLastUpdated(filePath: string) {
98
99
  }
99
100
  }
100
101
 
101
- function stripLeadingH1(content: string) {
102
- return content.replace(/^[ \t]*#\s+[^\n]*\n+/, "")
103
- }
104
-
105
102
  function extractTitle(content: string, path: string) {
106
103
  const match = /^#\s+(.+)$/m.exec(content)
107
104
  if (match) return match[1]!
@@ -0,0 +1,59 @@
1
+ import { createHash } from "node:crypto"
2
+ import { globSync, readFileSync } from "node:fs"
3
+ import { resolve, sep } from "node:path"
4
+ import { parse } from "yaml"
5
+ import { z } from "zod"
6
+
7
+ const SKILLS_SCHEMA = "https://schemas.agentskills.io/discovery/0.2.0/schema.json"
8
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
9
+
10
+ const SkillFrontmatter = z.object({
11
+ name: z
12
+ .string()
13
+ .max(64)
14
+ .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
15
+ description: z.string().min(1).max(1024),
16
+ })
17
+
18
+ /** A `SKILL.md` published for agents, with the raw bytes its digest covers. */
19
+ export interface AgentSkill {
20
+ name: string
21
+ description: string
22
+ source: Buffer
23
+ }
24
+
25
+ /** Read every `SKILL.md` the `skills` globs match, in a stable order. */
26
+ export function loadAgentSkills(props: { root: string; patterns: string[] }) {
27
+ // Windows globs with `\`, which would leak into messages and change the sort.
28
+ return globSync(props.patterns, { cwd: props.root })
29
+ .map(file => file.split(sep).join("/"))
30
+ .sort()
31
+ .map(file => {
32
+ const source = readFileSync(resolve(props.root, file))
33
+ const frontmatter = FRONTMATTER_RE.exec(source.toString("utf-8"))?.[1] ?? ""
34
+ const parsed = SkillFrontmatter.safeParse(parse(frontmatter))
35
+ if (!parsed.success) {
36
+ throw new Error(
37
+ `${file}: a published skill needs a frontmatter \`name\` (lowercase, hyphenated) and \`description\``,
38
+ )
39
+ }
40
+ return { ...parsed.data, source }
41
+ })
42
+ }
43
+
44
+ /** Where a skill is published inside the build output (no base). */
45
+ export function buildSkillFileName(name: string) {
46
+ return `.well-known/agent-skills/${name}/SKILL.md`
47
+ }
48
+
49
+ /** Render the Agent Skills discovery index (v0.2.0) for the published skills. */
50
+ export function renderSkillsIndex(props: { skills: AgentSkill[]; base?: string }) {
51
+ const skills = props.skills.map(skill => ({
52
+ name: skill.name,
53
+ type: "skill-md",
54
+ description: skill.description,
55
+ url: `${props.base ?? ""}/${buildSkillFileName(skill.name)}`,
56
+ digest: `sha256:${createHash("sha256").update(skill.source).digest("hex")}`,
57
+ }))
58
+ return `${JSON.stringify({ $schema: SKILLS_SCHEMA, skills }, null, 2)}\n`
59
+ }
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto"
2
+ import { mkdirSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { describe, expect, it } from "vite-plus/test"
5
+ import { temporaryDirectory } from "tempy"
6
+ import { loadAgentSkills, renderSkillsIndex } from "./agent-skills.ts"
7
+
8
+ function writeSkill(root: string, dir: string, source: string) {
9
+ mkdirSync(join(root, "skills", dir), { recursive: true })
10
+ writeFileSync(join(root, "skills", dir, "SKILL.md"), source)
11
+ }
12
+
13
+ describe("loadAgentSkills", () => {
14
+ it("reads name and description from each matched SKILL.md", () => {
15
+ const root = temporaryDirectory()
16
+ writeSkill(root, "b", "---\nname: beta\ndescription: >\n Folded\n text\n---\n# B\n")
17
+ writeSkill(root, "a", "---\nname: alpha\ndescription: First\n---\n# A\n")
18
+ const skills = loadAgentSkills({ root, patterns: ["skills/*/SKILL.md"] })
19
+ expect(skills.map(s => [s.name, s.description])).toEqual([
20
+ ["alpha", "First"],
21
+ ["beta", "Folded text\n"],
22
+ ])
23
+ })
24
+
25
+ it("rejects a skill whose name agents cannot address", () => {
26
+ const root = temporaryDirectory()
27
+ writeSkill(root, "a", "---\nname: Not Valid\ndescription: First\n---\n")
28
+ expect(() => loadAgentSkills({ root, patterns: ["skills/*/SKILL.md"] })).toThrow(
29
+ "skills/a/SKILL.md",
30
+ )
31
+ })
32
+ })
33
+
34
+ describe("renderSkillsIndex", () => {
35
+ it("publishes each skill with a digest of its exact bytes", () => {
36
+ const source = Buffer.from("---\nname: alpha\ndescription: First\n---\n")
37
+ const index = JSON.parse(
38
+ renderSkillsIndex({
39
+ skills: [{ name: "alpha", description: "First", source }],
40
+ base: "/repo",
41
+ }),
42
+ )
43
+ expect(index).toEqual({
44
+ $schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
45
+ skills: [
46
+ {
47
+ name: "alpha",
48
+ type: "skill-md",
49
+ description: "First",
50
+ url: "/repo/.well-known/agent-skills/alpha/SKILL.md",
51
+ digest: `sha256:${createHash("sha256").update(source).digest("hex")}`,
52
+ },
53
+ ],
54
+ })
55
+ })
56
+ })
@@ -50,3 +50,20 @@ export function flattenArticleTree(nodes: ArticleNode[]) {
50
50
  }
51
51
  return result
52
52
  }
53
+
54
+ /** Sort comparator on frontmatter `order`: positive ascending, then unordered,
55
+ * then negative (so `order: -1` lands last) */
56
+ export function compareArticleOrder(a: { order?: number }, b: { order?: number }) {
57
+ return orderKey(a.order) - orderKey(b.order)
58
+ }
59
+
60
+ /** Sort comparator putting the newest `date` first, as blog and changelog list */
61
+ export function compareArticleDate(a: { date?: string }, b: { date?: string }) {
62
+ return new Date(b.date ?? 0).getTime() - new Date(a.date ?? 0).getTime()
63
+ }
64
+
65
+ function orderKey(order?: number) {
66
+ if (order === undefined) return Number.MAX_SAFE_INTEGER / 2
67
+ if (order < 0) return Number.MAX_SAFE_INTEGER + order
68
+ return order
69
+ }
@@ -83,7 +83,18 @@ export function buildPageHead(props: {
83
83
  ...(url ? [{ property: "og:url", content: url }] : []),
84
84
  ...(image ? [{ property: "og:image", content: image }] : []),
85
85
  ],
86
- links: url ? [{ rel: "canonical", href: url }] : [],
86
+ links: [
87
+ ...(url ? [{ rel: "canonical", href: url }] : []),
88
+ ...(props.isArticle
89
+ ? [
90
+ {
91
+ rel: "alternate",
92
+ type: "text/markdown",
93
+ href: prefixUrl(`${props.path}index.md`, props.config.base),
94
+ },
95
+ ]
96
+ : []),
97
+ ],
87
98
  }
88
99
  }
89
100
 
@@ -79,7 +79,9 @@ describe("buildPageHead", () => {
79
79
  { property: "og:description", content: "How to" },
80
80
  { property: "og:type", content: "article" },
81
81
  ])
82
- expect(head.links).toEqual([])
82
+ expect(head.links).toEqual([
83
+ { rel: "alternate", type: "text/markdown", href: "/guide/index.md" },
84
+ ])
83
85
  })
84
86
 
85
87
  it("adds absolute url, canonical and image when site is set", () => {
@@ -0,0 +1,109 @@
1
+ import type { Config } from "../models/config.ts"
2
+ import type { RoutedSection } from "../models/section.ts"
3
+ import {
4
+ buildArticleTree,
5
+ compareArticleDate,
6
+ compareArticleOrder,
7
+ flattenArticleTree,
8
+ } from "./article.ts"
9
+ import { partitionBySection } from "./section.ts"
10
+
11
+ /** The slice of a built article that agent-facing files are rendered from. */
12
+ export interface LlmsArticle {
13
+ path: string
14
+ title: string
15
+ description?: string
16
+ order?: number
17
+ date?: string
18
+ }
19
+
20
+ /** Render `llms.txt`: the site title and description, then one Markdown link
21
+ * per article twin, grouped under each routed section in sidebar order. */
22
+ export function renderLlmsText(props: {
23
+ config: Pick<Config, "title" | "description" | "site" | "base" | "sections">
24
+ articles: LlmsArticle[]
25
+ }) {
26
+ const lines = [`# ${props.config.title}`, "", `> ${props.config.description}`]
27
+ for (const group of groupArticles(props)) {
28
+ lines.push("", `## ${group.title}`, "", ...renderLinks(props.config, group.articles))
29
+ }
30
+ return `${lines.join("\n")}\n`
31
+ }
32
+
33
+ /** Render the Markdown twin of a section's index page (a blog or changelog
34
+ * listing), which has no article of its own: links to the section's articles. */
35
+ export function renderSectionIndex(props: {
36
+ config: Pick<Config, "site" | "base" | "sections">
37
+ section: RoutedSection
38
+ articles: LlmsArticle[]
39
+ }) {
40
+ const group = groupArticles(props).find(g => g.prefix === props.section.prefix)
41
+ const lines = [
42
+ `# ${props.section.title}`,
43
+ "",
44
+ ...renderLinks(props.config, group?.articles ?? []),
45
+ ]
46
+ return `${lines.join("\n")}\n`
47
+ }
48
+
49
+ /** Render the Markdown twin of an article: its title and description as the
50
+ * rendered page shows them, then the body without its own leading h1. */
51
+ export function renderMarkdownTwin(props: { article: LlmsArticle; content: string }) {
52
+ const article = props.article
53
+ const parts = [`# ${article.title}`]
54
+ if (article.description) parts.push(`> ${article.description}`)
55
+ parts.push(stripLeadingH1(props.content).trim())
56
+ return `${parts.join("\n\n")}\n`
57
+ }
58
+
59
+ /** Path of an article's Markdown twin inside the build output (no base). */
60
+ export function buildTwinFileName(path: string) {
61
+ return `${path.replace(/^\//, "")}index.md`
62
+ }
63
+
64
+ /** Drop a leading h1 so a title set by frontmatter or a patch is not shown twice */
65
+ export function stripLeadingH1(content: string) {
66
+ return content.replace(/^[ \t]*#\s+[^\n]*\n+/, "")
67
+ }
68
+
69
+ function buildTwinUrl(config: Pick<Config, "site" | "base">, path: string) {
70
+ const host = config.site?.replace(/\/+$/, "") ?? ""
71
+ return `${host}${config.base ?? ""}${path}index.md`
72
+ }
73
+
74
+ function groupArticles(props: {
75
+ config: Pick<Config, "sections">
76
+ articles: LlmsArticle[]
77
+ }) {
78
+ const sections =
79
+ props.config.sections?.filter((s): s is RoutedSection => s.type !== "custom") ?? []
80
+ const buckets = partitionBySection(
81
+ [...props.articles].sort(compareArticleOrder),
82
+ sections,
83
+ )
84
+ const groups = [
85
+ ...sections.map(s => ({
86
+ prefix: s.prefix,
87
+ title: s.title,
88
+ dated: s.type !== "article",
89
+ })),
90
+ { prefix: "__default__", title: sections.length ? "Other" : "Pages", dated: false },
91
+ ]
92
+ return groups.flatMap(group => {
93
+ const bucket = buckets.get(group.prefix)
94
+ if (!bucket?.length) return []
95
+ const articles = group.dated
96
+ ? [...bucket].sort(compareArticleDate)
97
+ : flattenArticleTree(buildArticleTree(bucket)).flatMap(
98
+ path => bucket.find(a => a.path === path) ?? [],
99
+ )
100
+ return [{ prefix: group.prefix, title: group.title, articles }]
101
+ })
102
+ }
103
+
104
+ function renderLinks(config: Pick<Config, "site" | "base">, articles: LlmsArticle[]) {
105
+ return articles.map(article => {
106
+ const link = `[${article.title}](${buildTwinUrl(config, article.path)})`
107
+ return article.description ? `- ${link}: ${article.description}` : `- ${link}`
108
+ })
109
+ }
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import type { RoutedSection } from "../models/section.ts"
3
+ import {
4
+ buildTwinFileName,
5
+ renderLlmsText,
6
+ renderMarkdownTwin,
7
+ renderSectionIndex,
8
+ } from "./llms.ts"
9
+
10
+ const config = { title: "Docs", description: "Blurb" }
11
+
12
+ describe("renderLlmsText", () => {
13
+ it("lists articles in sidebar order under a single heading without sections", () => {
14
+ const text = renderLlmsText({
15
+ config,
16
+ articles: [
17
+ { path: "/guide/", title: "Guide", order: 2 },
18
+ { path: "/intro/", title: "Intro", description: "Start here", order: 1 },
19
+ { path: "/guide/deep/", title: "Deep" },
20
+ ],
21
+ })
22
+ expect(text).toBe(
23
+ [
24
+ "# Docs",
25
+ "",
26
+ "> Blurb",
27
+ "",
28
+ "## Pages",
29
+ "",
30
+ "- [Intro](/intro/index.md): Start here",
31
+ "- [Guide](/guide/index.md)",
32
+ "- [Deep](/guide/deep/index.md)",
33
+ "",
34
+ ].join("\n"),
35
+ )
36
+ })
37
+
38
+ it("groups by routed section, newest first in a blog, with absolute urls", () => {
39
+ const text = renderLlmsText({
40
+ config: {
41
+ ...config,
42
+ site: "https://example.com/",
43
+ base: "/repo",
44
+ sections: [
45
+ { title: "Docs", prefix: "/", type: "article", position: "header" },
46
+ { title: "Blog", prefix: "/blog/", type: "blog", position: "header" },
47
+ {
48
+ title: "GitHub",
49
+ url: "https://github.com",
50
+ type: "custom",
51
+ position: "header",
52
+ },
53
+ ],
54
+ },
55
+ articles: [
56
+ { path: "/blog/old/", title: "Old", date: "2026-01-01" },
57
+ { path: "/intro/", title: "Intro" },
58
+ { path: "/blog/new/", title: "New", date: "2026-02-01" },
59
+ ],
60
+ })
61
+ expect(text).toContain(
62
+ [
63
+ "## Docs",
64
+ "",
65
+ "- [Intro](https://example.com/repo/intro/index.md)",
66
+ "",
67
+ "## Blog",
68
+ "",
69
+ "- [New](https://example.com/repo/blog/new/index.md)",
70
+ "- [Old](https://example.com/repo/blog/old/index.md)",
71
+ ].join("\n"),
72
+ )
73
+ expect(text).not.toContain("GitHub")
74
+ })
75
+ })
76
+
77
+ describe("renderSectionIndex", () => {
78
+ it("lists only the section's articles, newest first", () => {
79
+ const blog: RoutedSection = {
80
+ title: "Blog",
81
+ prefix: "/blog/",
82
+ type: "blog",
83
+ position: "header",
84
+ }
85
+ const text = renderSectionIndex({
86
+ config: { sections: [blog] },
87
+ section: blog,
88
+ articles: [
89
+ { path: "/intro/", title: "Intro" },
90
+ { path: "/blog/old/", title: "Old", date: "2026-01-01" },
91
+ { path: "/blog/new/", title: "New", date: "2026-02-01", description: "Fresh" },
92
+ ],
93
+ })
94
+ expect(text).toBe(
95
+ "# Blog\n\n- [New](/blog/new/index.md): Fresh\n- [Old](/blog/old/index.md)\n",
96
+ )
97
+ })
98
+ })
99
+
100
+ describe("renderMarkdownTwin", () => {
101
+ it("leads with the title and description instead of the body's own h1", () => {
102
+ const twin = renderMarkdownTwin({
103
+ article: { path: "/intro/", title: "Introduction", description: "Start here" },
104
+ content: "# Livemark\n\nBody text\n",
105
+ })
106
+ expect(twin).toBe("# Introduction\n\n> Start here\n\nBody text\n")
107
+ })
108
+ })
109
+
110
+ describe("buildTwinFileName", () => {
111
+ it("places the twin beside the page's index.html", () => {
112
+ expect(buildTwinFileName("/")).toBe("index.md")
113
+ expect(buildTwinFileName("/guide/deep/")).toBe("guide/deep/index.md")
114
+ })
115
+ })
@@ -31,6 +31,15 @@ export const Head = z.object({
31
31
  scripts: z.array(z.record(z.string(), z.string())).optional(),
32
32
  })
33
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
+
34
43
  /**
35
44
  * User-provided configuration for Livemark.
36
45
  *
@@ -64,6 +73,8 @@ export const UserConfig = z.object({
64
73
  sections: z.array(Section).optional(),
65
74
  patches: z.array(Patch).optional(),
66
75
  head: Head.optional(),
76
+ contentSignals: ContentSignals.prefault({}),
77
+ skills: z.union([z.string(), z.array(z.string())]).optional(),
67
78
  // Stored as `unknown` in the schema (zod can't type a Vite UserConfig
68
79
  // or a function returning one meaningfully); the public TypeScript
69
80
  // surface above lifts it back to ViteOverride for editor support.