@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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +142 -6
  3. 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
- if (!existsSync(filePath)) {
237
- return null;
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
- return readFile(filePath, "utf8");
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.3.4",
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.3.4"
48
+ "@agentmarkup/core": "0.5.0"
46
49
  },
47
50
  "peerDependencies": {
48
51
  "astro": ">=4.0.0"