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.
- package/build/actions/config/define.d.ts +6 -0
- package/build/content/article.js +4 -11
- package/build/content-collections.js +2 -4
- package/build/helpers/agent-skills.d.ts +22 -0
- package/build/helpers/agent-skills.js +46 -0
- package/build/helpers/agent-skills.unit.d.ts +1 -0
- package/build/helpers/agent-skills.unit.js +49 -0
- package/build/helpers/article.d.ts +13 -0
- package/build/helpers/article.js +17 -1
- package/build/helpers/head.d.ts +7 -2
- package/build/helpers/head.js +13 -2
- package/build/helpers/head.unit.js +4 -2
- package/build/helpers/llms.d.ts +33 -0
- package/build/helpers/llms.js +72 -0
- package/build/helpers/llms.unit.d.ts +1 -0
- package/build/helpers/llms.unit.js +98 -0
- package/build/models/config.d.ts +20 -0
- package/build/models/config.js +8 -1
- package/build/plugins/vite-agents.d.ts +17 -0
- package/build/plugins/vite-agents.js +109 -0
- package/build/plugins/vite-agents.unit.d.ts +1 -0
- package/build/plugins/vite-agents.unit.js +20 -0
- package/build/plugins/vite-robots.d.ts +4 -2
- package/build/plugins/vite-robots.js +15 -2
- package/build/plugins/vite-robots.unit.js +5 -1
- package/build/vite.config.js +3 -1
- package/package.json +3 -2
- package/skills/livemark-configuration/SKILL.md +4 -0
- package/skills/livemark-customization/SKILL.md +1 -0
- package/source/content/article.ts +8 -13
- package/source/content-collections.ts +1 -4
- package/source/helpers/agent-skills.ts +59 -0
- package/source/helpers/agent-skills.unit.ts +56 -0
- package/source/helpers/article.ts +17 -0
- package/source/helpers/head.ts +12 -1
- package/source/helpers/head.unit.ts +3 -1
- package/source/helpers/llms.ts +109 -0
- package/source/helpers/llms.unit.ts +115 -0
- package/source/models/config.ts +11 -0
- package/source/plugins/vite-agents.ts +135 -0
- package/source/plugins/vite-agents.unit.ts +26 -0
- package/source/plugins/vite-robots.ts +18 -3
- package/source/plugins/vite-robots.unit.ts +7 -0
- package/source/vite.config.ts +2 -0
|
@@ -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,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(
|
|
39
|
-
|
|
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/vite.config.ts
CHANGED
|
@@ -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.ts"
|
|
9
|
+
import { agents } from "./plugins/vite-agents.ts"
|
|
9
10
|
import { livemark } from "./plugins/vite-livemark.ts"
|
|
10
11
|
import { robots } from "./plugins/vite-robots.ts"
|
|
11
12
|
|
|
@@ -28,6 +29,7 @@ export default defineConfig({
|
|
|
28
29
|
plugins: [
|
|
29
30
|
livemark({ config }),
|
|
30
31
|
robots({ config }),
|
|
32
|
+
agents({ config }),
|
|
31
33
|
devtools(),
|
|
32
34
|
tailwind(),
|
|
33
35
|
contentCollections({ configPath: "content-collections.ts" }),
|