nuxt-ai-ready 3.0.4 → 3.1.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.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { realpath, readFile, readdir } from 'node:fs/promises';
3
3
  import { join, resolve, isAbsolute, relative, sep } from 'node:path';
4
4
  import { parseDocument } from 'yaml';
5
- import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.iq1L3kll.mjs';
5
+ import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.Dmvy7CIJ.mjs';
6
6
  import 'node:module';
7
7
  import 'node:url';
8
8
  import '@nuxt/kit';
@@ -6,7 +6,7 @@ import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
6
6
  import { createPrerenderFetch } from 'nuxtseo-shared/prerender';
7
7
  import { collectSitemap } from 'sitemapd/parse';
8
8
  import { withLeadingSlash, joinURL, withBase } from 'ufo';
9
- import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.iq1L3kll.mjs';
9
+ import { l as logger, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.Dmvy7CIJ.mjs';
10
10
  import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
11
11
  import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
12
12
  import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
@@ -4,7 +4,7 @@ export interface DevtoolsConfig {
4
4
  sitemapPrerendered: boolean
5
5
  markdownCacheHeaders: { maxAge: number, swr: boolean }
6
6
  llmsTxtCacheSeconds: number
7
- contentSignal: false | { aiTrain: boolean, search: boolean, aiInput: boolean }
7
+ contentSignal: false | { aiTrain?: boolean, search?: boolean, aiInput?: boolean }
8
8
  mcp: { enabled: boolean, tools: boolean, resources: boolean }
9
9
  cron: boolean
10
10
  }
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.6.0 || ^5.0.0-0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "3.0.4",
7
+ "version": "3.1.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ import '@nuxt/kit';
7
7
  import 'defu';
8
8
  import 'nuxt-site-config/kit';
9
9
  import 'nuxtseo-shared/kit';
10
- export { m as default } from './shared/nuxt-ai-ready.iq1L3kll.mjs';
10
+ export { m as default } from './shared/nuxt-ai-ready.Dmvy7CIJ.mjs';
11
11
  import './shared/nuxt-ai-ready.B3hAuOLQ.mjs';
12
12
  import '../dist/runtime/route-path.js';
13
13
  import '../dist/runtime/server/utils/sitemap-md.js';
@@ -200,7 +200,12 @@ export interface ModuleOptions {
200
200
  */
201
201
  describedby?: boolean;
202
202
  /**
203
- * Content Signal Directives
203
+ * Content Signal directives for robots.txt.
204
+ *
205
+ * Unset, robots.txt gets `Content-Signal: search=yes, ai-input=yes` and no
206
+ * `ai-train` preference, unless a robots group already sets one. An object
207
+ * states the whole policy: each permission left out is sent as `no`.
208
+ * `false` sends no Content-Signal line.
204
209
  */
205
210
  contentSignal?: false | {
206
211
  /**
@@ -748,6 +748,58 @@ function resolveConfigurationWarnings(input) {
748
748
  return warnings;
749
749
  }
750
750
 
751
+ const DEFAULT_CONTENT_SIGNAL = "search=yes, ai-input=yes";
752
+ function resolveContentSignalPolicy(option) {
753
+ if (option === false)
754
+ return { _tag: "Disabled" };
755
+ if (option === void 0)
756
+ return { _tag: "Default" };
757
+ const value = (allowed) => allowed ? "yes" : "no";
758
+ return {
759
+ _tag: "Configured",
760
+ // One comma-separated line: validators reject one line per signal.
761
+ contentSignal: `ai-train=${value(option.aiTrain)}, search=${value(option.search)}, ai-input=${value(option.aiInput)}`,
762
+ ...option.contentUsage !== false && { contentUsage: `train-ai=${option.aiTrain ? "y" : "n"}` }
763
+ };
764
+ }
765
+ function hasContentSignal(value) {
766
+ if (Array.isArray(value))
767
+ return value.some(Boolean);
768
+ if (value && typeof value === "object")
769
+ return Object.keys(value).length > 0;
770
+ return Boolean(value);
771
+ }
772
+ function isWildcardGroup(group) {
773
+ const userAgents = Array.isArray(group.userAgent) ? group.userAgent : [group.userAgent ?? "*"];
774
+ return userAgents.length === 1 && userAgents[0] === "*";
775
+ }
776
+ function wildcardGroup(groups) {
777
+ const existing = groups.find(isWildcardGroup);
778
+ if (existing)
779
+ return existing;
780
+ const group = { userAgent: ["*"], disallow: [""] };
781
+ groups.push(group);
782
+ return group;
783
+ }
784
+ function applyContentSignalPolicy(groups, policy) {
785
+ switch (policy._tag) {
786
+ case "Disabled":
787
+ return;
788
+ case "Default": {
789
+ if (groups.some((group) => hasContentSignal(group.contentSignal)))
790
+ return;
791
+ wildcardGroup(groups).contentSignal = [DEFAULT_CONTENT_SIGNAL];
792
+ return;
793
+ }
794
+ case "Configured": {
795
+ const group = wildcardGroup(groups);
796
+ group.contentSignal = [policy.contentSignal];
797
+ if (policy.contentUsage)
798
+ group.contentUsage = [policy.contentUsage];
799
+ }
800
+ }
801
+ }
802
+
751
803
  const DATABASE_TYPE_NONE = "none";
752
804
  const DATABASE_DEPENDENT_OPTIONS = ["runtimeSync", "cron"];
753
805
  const DATABASE_CONSUMER_LABELS = {
@@ -1071,6 +1123,7 @@ function parseMcpServerCardConfig(input) {
1071
1123
  }
1072
1124
  };
1073
1125
  }
1126
+ const MCP_SERVER_CARD_WELL_KNOWN_PATHS = ["/.well-known/mcp/server-card.json", "/.well-known/mcp.json"];
1074
1127
  function resolveMcpServerCardRoute(mcpRoute) {
1075
1128
  return `${mcpRoute.replace(/\/+$/, "") || "/mcp"}/server-card`;
1076
1129
  }
@@ -1744,25 +1797,11 @@ ${details}`);
1744
1797
  (paths.resources ||= []).push(`${mcpRuntimeDir}/resources`);
1745
1798
  });
1746
1799
  const contentSource = await resolveContentSource(config.contentSource !== false);
1747
- if (typeof config.contentSignal === "object") {
1748
- const robotsOpts = nuxt.options.robots || {};
1749
- nuxt.options.robots = robotsOpts;
1750
- const groups = robotsOpts.groups || [];
1751
- robotsOpts.groups = groups;
1752
- const group = {
1753
- userAgent: "*",
1754
- // Preserve nuxt-robots' default wildcard rule so the injected group remains valid.
1755
- disallow: [""],
1756
- // Object form renders one comma-separated line; an array renders one line per entry, which validators reject.
1757
- contentSignal: {
1758
- "ai-train": config.contentSignal.aiTrain ? "yes" : "no",
1759
- "search": config.contentSignal.search ? "yes" : "no",
1760
- "ai-input": config.contentSignal.aiInput ? "yes" : "no"
1761
- }
1762
- };
1763
- if (config.contentSignal.contentUsage !== false)
1764
- group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
1765
- groups.push(group);
1800
+ const contentSignalPolicy = resolveContentSignalPolicy(config.contentSignal);
1801
+ if (contentSignalPolicy._tag !== "Disabled") {
1802
+ nuxt.hook("robots:config", (robotsConfig) => {
1803
+ applyContentSignalPolicy(robotsConfig.groups, contentSignalPolicy);
1804
+ });
1766
1805
  }
1767
1806
  registerTypeTemplates({ config, nitroCompatibility });
1768
1807
  const defaultLlmsTxtSections = [];
@@ -1876,18 +1915,21 @@ ${details}`);
1876
1915
  updateSeoProFeatures(seoProModules);
1877
1916
  return;
1878
1917
  }
1879
- const mcpLink = {
1880
- title: "MCP",
1881
- href: withSiteUrl(finalMcpToolkitState.route, { withBase: true }),
1882
- description: config.mcp?.tools !== false && hasMcpSiteTools ? "Use this MCP endpoint when you need information from this site's indexed pages." : "Model Context Protocol server endpoint for AI agent integration."
1883
- };
1884
- const firstSection = mergedLlmsTxt.sections?.[0];
1885
- if (firstSection) {
1886
- firstSection.links ||= [];
1887
- if (!firstSection.links.some((link) => link.title === mcpLink.title))
1888
- firstSection.links.push(mcpLink);
1889
- } else {
1890
- mergedLlmsTxt.sections = [{ title: "LLM Tools", links: [mcpLink] }];
1918
+ if (mcpServerCardResult._tag === "Enabled") {
1919
+ const endpoint = withSiteUrl(finalMcpToolkitState.route, { withBase: true });
1920
+ const mcpLink = {
1921
+ title: "MCP server card",
1922
+ href: withSiteUrl(MCP_SERVER_CARD_WELL_KNOWN_PATHS[0].slice(1), { withBase: true }),
1923
+ description: config.mcp?.tools !== false && hasMcpSiteTools ? `Connect an MCP client to ${endpoint} when you need information from this site's indexed pages.` : `Connect an MCP client to ${endpoint} to use this site's Model Context Protocol server.`
1924
+ };
1925
+ const firstSection = mergedLlmsTxt.sections?.[0];
1926
+ if (firstSection) {
1927
+ firstSection.links ||= [];
1928
+ if (!firstSection.links.some((link) => link.title === mcpLink.title))
1929
+ firstSection.links.push(mcpLink);
1930
+ } else {
1931
+ mergedLlmsTxt.sections = [{ title: "LLM Tools", links: [mcpLink] }];
1932
+ }
1891
1933
  }
1892
1934
  const mcpServerCardRoute = resolveMcpServerCardRoute(finalMcpToolkitState.route);
1893
1935
  if (siteConfig.url && config.apiCatalog !== false) {
@@ -1964,21 +2006,23 @@ ${details}`);
1964
2006
  etag
1965
2007
  };
1966
2008
  const handler = resolve$1("./runtime/server/routes/mcp-server-card");
1967
- addServerHandler({ route: mcpServerCardRoute, method: "get", handler, lazy: true });
1968
- addServerHandler({ route: mcpServerCardRoute, method: "head", handler, lazy: true });
1969
- addServerHandler({ route: mcpServerCardRoute, method: "options", handler, lazy: true });
1970
- extendRouteRules(mcpServerCardRoute, {
1971
- sitemap: false,
1972
- headers: {
1973
- "Access-Control-Allow-Headers": "Content-Type, If-None-Match",
1974
- "Access-Control-Allow-Methods": "GET, HEAD",
1975
- "Access-Control-Allow-Origin": "*",
1976
- "Access-Control-Expose-Headers": "ETag",
1977
- "Cache-Control": `public, max-age=${mcpServerCardResult.config.cacheMaxAge}`,
1978
- "Content-Type": MCP_SERVER_CARD_MEDIA_TYPE,
1979
- "ETag": etag
1980
- }
1981
- });
2009
+ for (const route of [mcpServerCardRoute, ...MCP_SERVER_CARD_WELL_KNOWN_PATHS]) {
2010
+ addServerHandler({ route, method: "get", handler, lazy: true });
2011
+ addServerHandler({ route, method: "head", handler, lazy: true });
2012
+ addServerHandler({ route, method: "options", handler, lazy: true });
2013
+ extendRouteRules(route, {
2014
+ sitemap: false,
2015
+ headers: {
2016
+ "Access-Control-Allow-Headers": "Content-Type, If-None-Match",
2017
+ "Access-Control-Allow-Methods": "GET, HEAD",
2018
+ "Access-Control-Allow-Origin": "*",
2019
+ "Access-Control-Expose-Headers": "ETag",
2020
+ "Cache-Control": `public, max-age=${mcpServerCardResult.config.cacheMaxAge}`,
2021
+ "Content-Type": MCP_SERVER_CARD_MEDIA_TYPE,
2022
+ "ETag": etag
2023
+ }
2024
+ });
2025
+ }
1982
2026
  if (siteConfig.url) {
1983
2027
  const document = resolveAiCatalog({
1984
2028
  siteUrl: siteConfig.url,
@@ -2188,7 +2232,7 @@ export function trackDrizzleWork(event, work) { return work }
2188
2232
  const schemaPath = database._tag === "Enabled" && (database.type === "neon" || database.type === "postgres") ? "#ai-ready/server/db/schema/postgres" : "#ai-ready/server/db/schema/sqlite";
2189
2233
  nitroConfig.virtual["#ai-ready-virtual/db-schema.mjs"] = database._tag === "Enabled" ? `export * from '${schemaPath}'` : "export {}";
2190
2234
  nitroConfig.virtual["#ai-ready-virtual/devtools-meta.mjs"] = `export const devtoolsMeta = ${JSON.stringify({
2191
- contentSignal: config.contentSignal || false,
2235
+ contentSignal: config.contentSignal ?? { search: true, aiInput: true },
2192
2236
  mcp: { enabled: mcpAvailable, tools: mcpAvailable && config.mcp?.tools !== false && hasMcpSiteTools, resources: mcpAvailable && config.mcp?.resources !== false },
2193
2237
  cron: !!config.cron
2194
2238
  })}`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "3.0.4",
4
+ "version": "3.1.0",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -39,8 +39,9 @@ With no `aiReady` config, the module adds:
39
39
  - `/sitemap.md`, and a `## Sitemap` footer on every generated `.md` page. Set `sitemapMd: false` to remove both.
40
40
  - Nuxt Content v3 `page` collections: the `.md` route serves the source Markdown, not converted HTML. Set `contentSource: false` to convert HTML.
41
41
  - Agent skills: `public/skills/<name>/SKILL.md` in the project or a local layer is published at `/.well-known/agent-skills/`, `/skills/<name>/SKILL.md`, and in `llms.txt`. Custom public directories are supported. One local skill is also served at `/SKILL.md`. Set `agentSkills: false` to publish nothing.
42
+ - `Content-Signal: search=yes, ai-input=yes` in `robots.txt`. Set `contentSignal: false` to remove it.
42
43
 
43
- Off by default: Content Signals, MCP, WebMCP, runtime sync, cron, and the database.
44
+ Off by default: MCP, WebMCP, runtime sync, cron, and the database.
44
45
 
45
46
  ## Common tasks
46
47
 
@@ -76,14 +77,14 @@ export default defineNitroPlugin((nitroApp) => {
76
77
  })
77
78
  ```
78
79
 
79
- Allow Content Signals. An omitted permission renders as `no`:
80
+ Content Signals. Without config, robots.txt gets `Content-Signal: search=yes, ai-input=yes` and no `ai-train`. A robots group or merged robots.txt that already sets `Content-Signal` replaces the default. `contentSignal: false` removes the line. An object states the whole policy, so an omitted permission renders as `no`:
80
81
 
81
82
  ```ts
82
- export default defineNuxtConfig({ aiReady: { contentSignal: { search: true } } })
83
- // robots.txt: Content-Signal: ai-train=no, search=yes, ai-input=no
83
+ export default defineNuxtConfig({ aiReady: { contentSignal: { search: true, aiInput: true, aiTrain: false } } })
84
+ // robots.txt: Content-Signal: ai-train=no, search=yes, ai-input=yes
84
85
  ```
85
86
 
86
- MCP: install `@nuxtjs/mcp-toolkit` and add it to `modules`. The module then registers `list_pages`, `search_pages`, and `get_page_markdown` at `/mcp`. These tools need the database, so also run `npx nypm add drizzle-orm@1.0.0-rc.4`. MCP needs a running server; `nuxt generate` output cannot host it.
87
+ MCP: install `@nuxtjs/mcp-toolkit` and add it to `modules`. The module then registers `list_pages`, `search_pages`, and `get_page_markdown` at `/mcp`. These tools need the database, so also run `npx nypm add drizzle-orm@1.0.0-rc.4`. MCP needs a running server; `nuxt generate` output cannot host it. The server card is served at `/mcp/server-card`, `/.well-known/mcp/server-card.json`, and `/.well-known/mcp.json`. llms.txt links the card, never the endpoint, because the endpoint answers GET with 405 or 406. `mcpServerCard: false` removes the card routes and the llms.txt entry.
87
88
 
88
89
  WebMCP: set `webmcp: true`. This registers the same three tools in the browser and auto-imports `useWebMcpTool()` and `useWebMcpSupported()`. The tools need the database and `drizzle-orm`. Without `webmcp`, those composables are not defined.
89
90