@agentmarkup/astro 0.3.4 → 0.5.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/README.md +6 -1
- package/dist/index.js +142 -6
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @agentmarkup/astro
|
|
2
2
|
|
|
3
|
-
Build-time `llms.txt`, optional `llms-full.txt`, JSON-LD, markdown mirrors, AI crawler `robots.txt`, headers, and validation for Astro websites.
|
|
3
|
+
Build-time `llms.txt`, optional `llms-full.txt`, optional A2A Agent Cards, JSON-LD, markdown mirrors, AI crawler `robots.txt`, headers, and validation for Astro websites.
|
|
4
|
+
|
|
5
|
+
`@agentmarkup/astro` is the Astro adapter in the `agentmarkup` package family. Framework-agnostic helpers live in `@agentmarkup/core`, Vite sites use `@agentmarkup/vite`, and Next.js sites use `@agentmarkup/next`.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -89,6 +91,7 @@ export default defineConfig({
|
|
|
89
91
|
|
|
90
92
|
## What It Does
|
|
91
93
|
|
|
94
|
+
- Generates optional `/.well-known/agent-card.json` for existing A2A services
|
|
92
95
|
- Injects JSON-LD into built HTML pages during the Astro build
|
|
93
96
|
- Generates `/llms.txt` from config
|
|
94
97
|
- Generates optional `/llms-full.txt` with inlined same-site markdown content
|
|
@@ -112,6 +115,8 @@ When markdown mirrors are enabled, same-site page entries in `llms.txt` automati
|
|
|
112
115
|
|
|
113
116
|
Enable `llmsFullTxt` when you want a richer companion file for agents that can consume more than the compact `llms.txt` manifest. The generated `llms-full.txt` keeps the same section structure but inlines same-site markdown mirror content when those mirrors exist.
|
|
114
117
|
|
|
118
|
+
Enable `agentCard` when you already run a real A2A-compatible agent service and want Astro builds to publish its discovery file at `/.well-known/agent-card.json`. agentmarkup only emits and validates the static Agent Card. It does not implement the A2A runtime server or transport endpoints for you. When enabled, provide a `version`, at least one `supportedInterfaces` entry, and a non-empty description through either the top-level `description` or `agentCard.description`.
|
|
119
|
+
|
|
115
120
|
## Maintainer
|
|
116
121
|
|
|
117
122
|
Copyright (c) 2026 [Sebastian Cochinescu](https://www.cochinescu.com). MIT License.
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
|
-
import { readdir, readFile, writeFile } from "fs/promises";
|
|
4
|
-
import { join, relative } from "path";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join, relative } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import {
|
|
7
|
+
A2A_AGENT_CARD_FILE_NAME,
|
|
7
8
|
collectSchemasForPage,
|
|
8
9
|
filterJsonLdByExistingTypes,
|
|
10
|
+
generateAgentCard,
|
|
11
|
+
generateLlmsFullTxt,
|
|
12
|
+
generateLlmsTxtDiscoveryLink,
|
|
9
13
|
generateMarkdownAlternateLink,
|
|
10
14
|
generatePageMarkdown,
|
|
11
15
|
generateJsonLdTags,
|
|
12
16
|
generateLlmsTxt,
|
|
17
|
+
hasLlmsTxtDiscoveryLink,
|
|
13
18
|
hasExistingJsonLdScripts,
|
|
14
19
|
injectHeadContent,
|
|
15
20
|
injectJsonLdTags,
|
|
@@ -20,18 +25,27 @@ import {
|
|
|
20
25
|
patchRobotsTxt,
|
|
21
26
|
presetToJsonLd,
|
|
22
27
|
printReport,
|
|
28
|
+
resolveLlmsTxtSections,
|
|
29
|
+
validateAgentCardConfig,
|
|
30
|
+
validateAgentCardJson,
|
|
23
31
|
validateExistingJsonLd,
|
|
24
32
|
validateHtmlContent,
|
|
25
33
|
validateLlmsTxt,
|
|
34
|
+
validateLlmsTxtMarkdownCoverage,
|
|
35
|
+
validateMarkdownAlternateLink,
|
|
36
|
+
validateMarkdownContent,
|
|
26
37
|
validateRobotsTxt,
|
|
27
38
|
validateSchema
|
|
28
39
|
} from "@agentmarkup/core";
|
|
29
40
|
export * from "@agentmarkup/core";
|
|
30
41
|
function agentmarkup(config) {
|
|
31
42
|
const validationResults = [];
|
|
43
|
+
let agentCardStatus = "none";
|
|
32
44
|
let llmsTxtEntries = 0;
|
|
33
45
|
let llmsTxtSections = 0;
|
|
34
46
|
let llmsTxtStatus = "none";
|
|
47
|
+
let llmsFullTxtEntries = 0;
|
|
48
|
+
let llmsFullTxtStatus = "none";
|
|
35
49
|
let jsonLdPages = 0;
|
|
36
50
|
let markdownPages = 0;
|
|
37
51
|
let markdownPagesStatus = "none";
|
|
@@ -50,6 +64,12 @@ function agentmarkup(config) {
|
|
|
50
64
|
"astro:build:done": async ({ dir }) => {
|
|
51
65
|
const outDir = fileURLToPath(dir);
|
|
52
66
|
const htmlFiles = await findHtmlFiles(outDir);
|
|
67
|
+
const resolvedLlmsSections = resolveLlmsTxtSections(config);
|
|
68
|
+
const markdownByUrl = {};
|
|
69
|
+
const availableMarkdownUrls = /* @__PURE__ */ new Set();
|
|
70
|
+
const finalHtmlByFile = /* @__PURE__ */ new Map();
|
|
71
|
+
const shouldManageAgentCard = Boolean(config.agentCard) && config.agentCard?.enabled !== false;
|
|
72
|
+
const advertiseLlmsTxt = Boolean(config.llmsTxt) || Boolean(publicDir && existsSync(join(publicDir, "llms.txt")));
|
|
53
73
|
for (const htmlFile of htmlFiles) {
|
|
54
74
|
const pagePath = pagePathFromOutputFile(outDir, htmlFile);
|
|
55
75
|
const html = await readFile(htmlFile, "utf8");
|
|
@@ -60,12 +80,18 @@ function agentmarkup(config) {
|
|
|
60
80
|
validationResults.push(...validateHtmlContent(nextHtml, pagePath));
|
|
61
81
|
validationResults.push(...validateExistingJsonLd(nextHtml, pagePath));
|
|
62
82
|
}
|
|
83
|
+
if (advertiseLlmsTxt && !hasLlmsTxtDiscoveryLink(nextHtml)) {
|
|
84
|
+
nextHtml = injectHeadContent(nextHtml, generateLlmsTxtDiscoveryLink());
|
|
85
|
+
}
|
|
63
86
|
if (isFeatureEnabled(config.markdownPages) && pagePath && !hasMarkdownAlternateLink(nextHtml)) {
|
|
64
87
|
nextHtml = injectHeadContent(
|
|
65
88
|
nextHtml,
|
|
66
89
|
generateMarkdownAlternateLink(pagePath)
|
|
67
90
|
);
|
|
68
91
|
}
|
|
92
|
+
if (isFeatureEnabled(config.markdownPages) && !config.validation?.disabled) {
|
|
93
|
+
validationResults.push(...validateMarkdownAlternateLink(nextHtml, pagePath));
|
|
94
|
+
}
|
|
69
95
|
if (schemas.length === 0) {
|
|
70
96
|
if (pagePath && config.validation?.warnOnMissingSchema && !config.validation.disabled && !hasExistingJsonLd) {
|
|
71
97
|
validationResults.push({
|
|
@@ -77,6 +103,7 @@ function agentmarkup(config) {
|
|
|
77
103
|
if (nextHtml !== html) {
|
|
78
104
|
await writeFile(htmlFile, nextHtml, "utf8");
|
|
79
105
|
}
|
|
106
|
+
finalHtmlByFile.set(htmlFile, nextHtml);
|
|
80
107
|
continue;
|
|
81
108
|
}
|
|
82
109
|
const jsonLdObjects = schemas.map(presetToJsonLd);
|
|
@@ -90,11 +117,13 @@ function agentmarkup(config) {
|
|
|
90
117
|
if (nextHtml !== html) {
|
|
91
118
|
await writeFile(htmlFile, nextHtml, "utf8");
|
|
92
119
|
}
|
|
120
|
+
finalHtmlByFile.set(htmlFile, nextHtml);
|
|
93
121
|
continue;
|
|
94
122
|
}
|
|
95
123
|
const tags = generateJsonLdTags(injectables);
|
|
96
124
|
nextHtml = injectJsonLdTags(nextHtml, tags);
|
|
97
125
|
await writeFile(htmlFile, nextHtml, "utf8");
|
|
126
|
+
finalHtmlByFile.set(htmlFile, nextHtml);
|
|
98
127
|
jsonLdPages += 1;
|
|
99
128
|
}
|
|
100
129
|
if (isFeatureEnabled(config.markdownPages)) {
|
|
@@ -104,8 +133,9 @@ function agentmarkup(config) {
|
|
|
104
133
|
const relativeHtmlPath = relative(outDir, htmlFile).replace(/\\/g, "/");
|
|
105
134
|
const markdownFileName = markdownFileNameFromHtmlFile(relativeHtmlPath);
|
|
106
135
|
const outputMarkdownPath = join(outDir, markdownFileName);
|
|
107
|
-
const html = await readFile(htmlFile, "utf8");
|
|
136
|
+
const html = finalHtmlByFile.get(htmlFile) ?? await readFile(htmlFile, "utf8");
|
|
108
137
|
const pagePath = pagePathFromOutputFile(outDir, htmlFile);
|
|
138
|
+
const markdownAbsoluteUrl = buildAbsoluteMarkdownUrl(config.site, pagePath);
|
|
109
139
|
const markdown = generatePageMarkdown({
|
|
110
140
|
html,
|
|
111
141
|
pagePath,
|
|
@@ -118,20 +148,32 @@ function agentmarkup(config) {
|
|
|
118
148
|
const existingMarkdown = existingOutputMarkdown ?? (publicDir ? await readTextFileIfExists(join(publicDir, markdownFileName)) : null);
|
|
119
149
|
if (existingMarkdown && !config.markdownPages?.replaceExisting) {
|
|
120
150
|
preservedMarkdownPages += 1;
|
|
151
|
+
markdownByUrl[markdownAbsoluteUrl] = existingMarkdown;
|
|
152
|
+
availableMarkdownUrls.add(markdownAbsoluteUrl);
|
|
121
153
|
markdownCanonicalEntries.push({
|
|
122
154
|
markdownPath: `/${markdownFileName}`,
|
|
123
155
|
canonicalUrl: buildCanonicalUrl(config.site, pagePath)
|
|
124
156
|
});
|
|
157
|
+
if (!config.validation?.disabled) {
|
|
158
|
+
validationResults.push(
|
|
159
|
+
...validateMarkdownContent(existingMarkdown, pagePath)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
125
162
|
if (!existingOutputMarkdown) {
|
|
126
163
|
await writeFile(outputMarkdownPath, existingMarkdown, "utf8");
|
|
127
164
|
}
|
|
128
165
|
continue;
|
|
129
166
|
}
|
|
130
167
|
await writeFile(outputMarkdownPath, markdown, "utf8");
|
|
168
|
+
markdownByUrl[markdownAbsoluteUrl] = markdown;
|
|
169
|
+
availableMarkdownUrls.add(markdownAbsoluteUrl);
|
|
131
170
|
markdownCanonicalEntries.push({
|
|
132
171
|
markdownPath: `/${markdownFileName}`,
|
|
133
172
|
canonicalUrl: buildCanonicalUrl(config.site, pagePath)
|
|
134
173
|
});
|
|
174
|
+
if (!config.validation?.disabled) {
|
|
175
|
+
validationResults.push(...validateMarkdownContent(markdown, pagePath));
|
|
176
|
+
}
|
|
135
177
|
markdownPages += 1;
|
|
136
178
|
}
|
|
137
179
|
markdownPagesStatus = markdownPages > 0 ? "generated" : preservedMarkdownPages > 0 ? "preserved" : "none";
|
|
@@ -150,6 +192,47 @@ function agentmarkup(config) {
|
|
|
150
192
|
await writeFile(outputHeadersPath, patchedHeaders, "utf8");
|
|
151
193
|
}
|
|
152
194
|
}
|
|
195
|
+
if (!config.validation?.disabled && resolvedLlmsSections.length > 0) {
|
|
196
|
+
validationResults.push(
|
|
197
|
+
...validateLlmsTxtMarkdownCoverage(
|
|
198
|
+
resolvedLlmsSections,
|
|
199
|
+
availableMarkdownUrls
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (shouldManageAgentCard) {
|
|
205
|
+
const outputAgentCardPath = join(outDir, A2A_AGENT_CARD_FILE_NAME);
|
|
206
|
+
const existingOutputAgentCard = await readTextFileIfExists(outputAgentCardPath);
|
|
207
|
+
const existingAgentCard = existingOutputAgentCard ?? (publicDir ? await readTextFileIfExists(join(publicDir, A2A_AGENT_CARD_FILE_NAME)) : null);
|
|
208
|
+
if (existingAgentCard && !config.agentCard?.replaceExisting) {
|
|
209
|
+
agentCardStatus = "preserved";
|
|
210
|
+
if (!existingOutputAgentCard) {
|
|
211
|
+
await writeTextFile(outputAgentCardPath, existingAgentCard);
|
|
212
|
+
}
|
|
213
|
+
if (!config.validation?.disabled) {
|
|
214
|
+
validationResults.push(...validateAgentCardJson(existingAgentCard));
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const agentCardConfigIssues = validateAgentCardConfig(
|
|
218
|
+
config,
|
|
219
|
+
`/${A2A_AGENT_CARD_FILE_NAME}`
|
|
220
|
+
);
|
|
221
|
+
if (!config.validation?.disabled) {
|
|
222
|
+
validationResults.push(...agentCardConfigIssues);
|
|
223
|
+
}
|
|
224
|
+
if (!agentCardConfigIssues.some((result) => result.severity === "error")) {
|
|
225
|
+
const agentCardContent = generateAgentCard(config);
|
|
226
|
+
if (!agentCardContent) {
|
|
227
|
+
throw new Error("Agent Card generation returned no output for a valid config");
|
|
228
|
+
}
|
|
229
|
+
agentCardStatus = "generated";
|
|
230
|
+
await writeTextFile(outputAgentCardPath, agentCardContent);
|
|
231
|
+
if (!config.validation?.disabled) {
|
|
232
|
+
validationResults.push(...validateAgentCardJson(agentCardContent));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
153
236
|
}
|
|
154
237
|
const llmsTxtContent = generateLlmsTxt(config);
|
|
155
238
|
if (llmsTxtContent) {
|
|
@@ -177,6 +260,36 @@ function agentmarkup(config) {
|
|
|
177
260
|
}
|
|
178
261
|
}
|
|
179
262
|
}
|
|
263
|
+
if (config.llmsFullTxt?.enabled) {
|
|
264
|
+
const llmsFullTxtContent = generateLlmsFullTxt(config, {
|
|
265
|
+
contentByUrl: markdownByUrl
|
|
266
|
+
});
|
|
267
|
+
if (llmsFullTxtContent) {
|
|
268
|
+
const outputLlmsFullPath = join(outDir, "llms-full.txt");
|
|
269
|
+
const inlineEntries = countInlinedLlmsFullEntries(
|
|
270
|
+
resolvedLlmsSections,
|
|
271
|
+
markdownByUrl
|
|
272
|
+
);
|
|
273
|
+
const existingOutputLlmsFull = await readTextFileIfExists(outputLlmsFullPath);
|
|
274
|
+
const existingLlmsFull = existingOutputLlmsFull ?? (publicDir ? await readTextFileIfExists(join(publicDir, "llms-full.txt")) : null);
|
|
275
|
+
if (existingLlmsFull && !config.llmsFullTxt.replaceExisting) {
|
|
276
|
+
llmsFullTxtStatus = "preserved";
|
|
277
|
+
if (!existingOutputLlmsFull) {
|
|
278
|
+
await writeFile(outputLlmsFullPath, existingLlmsFull, "utf8");
|
|
279
|
+
}
|
|
280
|
+
if (!config.validation?.disabled) {
|
|
281
|
+
validationResults.push(...validateLlmsTxt(existingLlmsFull));
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
llmsFullTxtStatus = "generated";
|
|
285
|
+
llmsFullTxtEntries = inlineEntries;
|
|
286
|
+
await writeFile(outputLlmsFullPath, llmsFullTxtContent, "utf8");
|
|
287
|
+
if (!config.validation?.disabled) {
|
|
288
|
+
validationResults.push(...validateLlmsTxt(llmsFullTxtContent));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
180
293
|
if (config.aiCrawlers) {
|
|
181
294
|
const crawlerEntries = Object.entries(config.aiCrawlers).filter(
|
|
182
295
|
([, value]) => value !== void 0
|
|
@@ -215,9 +328,12 @@ function agentmarkup(config) {
|
|
|
215
328
|
}
|
|
216
329
|
printReport({
|
|
217
330
|
label: "@agentmarkup/astro",
|
|
331
|
+
agentCardStatus,
|
|
218
332
|
llmsTxtEntries,
|
|
219
333
|
llmsTxtSections,
|
|
220
334
|
llmsTxtStatus,
|
|
335
|
+
llmsFullTxtEntries,
|
|
336
|
+
llmsFullTxtStatus,
|
|
221
337
|
jsonLdPages,
|
|
222
338
|
markdownPages,
|
|
223
339
|
markdownPagesStatus,
|
|
@@ -233,10 +349,18 @@ function agentmarkup(config) {
|
|
|
233
349
|
};
|
|
234
350
|
}
|
|
235
351
|
async function readTextFileIfExists(filePath) {
|
|
236
|
-
|
|
237
|
-
return
|
|
352
|
+
try {
|
|
353
|
+
return await readFile(filePath, "utf8");
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
238
359
|
}
|
|
239
|
-
|
|
360
|
+
}
|
|
361
|
+
async function writeTextFile(filePath, content) {
|
|
362
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
363
|
+
await writeFile(filePath, ensureTrailingNewline(content), "utf8");
|
|
240
364
|
}
|
|
241
365
|
async function findHtmlFiles(rootDir) {
|
|
242
366
|
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
@@ -262,6 +386,18 @@ function buildCanonicalUrl(siteUrl, pagePath) {
|
|
|
262
386
|
const base = siteUrl.replace(/\/$/, "");
|
|
263
387
|
return pagePath === "/" ? `${base}/` : `${base}${pagePath}`;
|
|
264
388
|
}
|
|
389
|
+
function buildAbsoluteMarkdownUrl(siteUrl, pagePath) {
|
|
390
|
+
const base = siteUrl.replace(/\/$/, "");
|
|
391
|
+
return pagePath === "/" ? `${base}/index.md` : `${base}${pagePath}.md`;
|
|
392
|
+
}
|
|
393
|
+
function countInlinedLlmsFullEntries(sections, markdownByUrl) {
|
|
394
|
+
return sections.reduce(
|
|
395
|
+
(sum, section) => sum + section.entries.filter(
|
|
396
|
+
(entry) => Boolean(entry.markdownUrl && markdownByUrl[entry.markdownUrl])
|
|
397
|
+
).length,
|
|
398
|
+
0
|
|
399
|
+
);
|
|
400
|
+
}
|
|
265
401
|
function existingOutputFileExists(filePath) {
|
|
266
402
|
return existsSync(filePath);
|
|
267
403
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentmarkup/astro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Build-time llms.txt, llms-full.txt, JSON-LD, markdown mirrors, headers, AI crawler controls, and validation for Astro",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Build-time llms.txt, llms-full.txt, A2A Agent Cards, JSON-LD, markdown mirrors, headers, AI crawler controls, and validation for Astro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Sebastian Cochinescu <hello@animafelix.com> (https://animafelix.com)",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"astro",
|
|
19
19
|
"llms-txt",
|
|
20
20
|
"llms-full",
|
|
21
|
+
"a2a",
|
|
22
|
+
"agent-card",
|
|
21
23
|
"json-ld",
|
|
22
24
|
"markdown",
|
|
23
25
|
"schema-org",
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
"headers",
|
|
28
30
|
"ai-crawler",
|
|
29
31
|
"machine-readable",
|
|
32
|
+
"seo",
|
|
30
33
|
"validation",
|
|
31
34
|
"geo"
|
|
32
35
|
],
|
|
@@ -42,7 +45,7 @@
|
|
|
42
45
|
"dist"
|
|
43
46
|
],
|
|
44
47
|
"dependencies": {
|
|
45
|
-
"@agentmarkup/core": "0.
|
|
48
|
+
"@agentmarkup/core": "0.5.0"
|
|
46
49
|
},
|
|
47
50
|
"peerDependencies": {
|
|
48
51
|
"astro": ">=4.0.0"
|