nuxt-ai-ready 2.1.0 → 2.2.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/dist/chunks/agent-skills.mjs +4 -1
- package/dist/chunks/prerender.mjs +10 -3
- package/dist/cli.mjs +67 -11
- package/dist/module.d.mts +2 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -1
- package/dist/runtime/app/plugins/md-alternate.server.js +6 -2
- package/dist/runtime/llms-txt-utils.js +2 -2
- package/dist/runtime/markdown-path.d.ts +2 -0
- package/dist/runtime/markdown-path.js +4 -0
- package/dist/runtime/server/db/drizzle/index.d.ts +1 -1
- package/dist/runtime/server/db/drizzle/index.js +0 -1
- package/dist/runtime/server/db/drizzle/queries.d.ts +0 -4
- package/dist/runtime/server/db/drizzle/queries.js +22 -16
- package/dist/runtime/server/db/drizzle/raw.js +1 -4
- package/dist/runtime/server/db/queries.d.ts +22 -21
- package/dist/runtime/server/db/queries.js +89 -66
- package/dist/runtime/server/db/shared.d.ts +16 -0
- package/dist/runtime/server/db/shared.js +12 -0
- package/dist/runtime/server/middleware/markdown.js +10 -5
- package/dist/runtime/server/middleware/markdown.prerender.js +4 -1
- package/dist/runtime/server/plugins/sitemap-seeder.js +5 -2
- package/dist/runtime/server/routes/__ai-ready/cron.get.js +10 -2
- package/dist/runtime/server/routes/__ai-ready/poll.post.js +30 -14
- package/dist/runtime/server/routes/__ai-ready/reindex.post.d.ts +2 -0
- package/dist/runtime/server/routes/__ai-ready/reindex.post.js +23 -0
- package/dist/runtime/server/routes/api-catalog.js +11 -2
- package/dist/runtime/server/routes/llms.txt.get.js +17 -12
- package/dist/runtime/server/routes/sitemap.md.get.d.ts +2 -0
- package/dist/runtime/server/routes/sitemap.md.get.js +51 -0
- package/dist/runtime/server/tasks/ai-ready-cron.js +3 -0
- package/dist/runtime/server/utils/i18n.d.ts +4 -0
- package/dist/runtime/server/utils/i18n.js +13 -0
- package/dist/runtime/server/utils/indexPage.js +7 -1
- package/dist/runtime/server/utils/link-header.d.ts +2 -0
- package/dist/runtime/server/utils/link-header.js +6 -0
- package/dist/runtime/server/utils/markdown-request.js +3 -1
- package/dist/runtime/server/utils/runCron.d.ts +24 -0
- package/dist/runtime/server/utils/runCron.js +21 -4
- package/dist/runtime/server/utils/sitemap-md.d.ts +13 -0
- package/dist/runtime/server/utils/sitemap-md.js +65 -0
- package/dist/runtime/types.d.ts +16 -3
- package/dist/shared/{nuxt-ai-ready.BTQAkSYt.mjs → nuxt-ai-ready.D5URd0TD.mjs} +103 -36
- package/package.json +17 -17
|
@@ -7,3 +7,16 @@ export {
|
|
|
7
7
|
export function getRuntimeI18n(aiReadyConfig) {
|
|
8
8
|
return aiReadyConfig.i18n || null;
|
|
9
9
|
}
|
|
10
|
+
export function normalizeHost(value) {
|
|
11
|
+
return value.trim().toLowerCase().replace(/^[a-z][a-z\d+.-]*:\/\//, "").split("/")[0];
|
|
12
|
+
}
|
|
13
|
+
export function hostMatchesLocaleDomain(host, i18n) {
|
|
14
|
+
if (!host)
|
|
15
|
+
return false;
|
|
16
|
+
const normalized = normalizeHost(host);
|
|
17
|
+
if (!normalized)
|
|
18
|
+
return false;
|
|
19
|
+
return i18n.locales.some(
|
|
20
|
+
(locale) => [...locale.domains ?? [], locale.domain, ...locale.defaultForDomains ?? []].some((domain) => domain && normalizeHost(domain) === normalized)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -98,5 +98,11 @@ export async function indexPageByRoute(route, event, options = {}) {
|
|
|
98
98
|
}
|
|
99
99
|
return { success: false, error: `Failed to fetch HTML for ${route}` };
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
try {
|
|
102
|
+
return await indexPage(route, html, options, event);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
logger.warn(`[indexPageByRoute] Failed to index ${route}:`, message);
|
|
106
|
+
return { success: false, error: message || `Failed to index ${route}` };
|
|
107
|
+
}
|
|
102
108
|
}
|
|
@@ -8,10 +8,12 @@ import type { RuntimeI18nConfig, RuntimeRouteContext } from './i18n.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function encodePathForHeader(path: string): string;
|
|
10
10
|
type LinkUrlResolver = (path: string) => string;
|
|
11
|
+
export declare const LLMS_TXT_PATH = "/llms.txt";
|
|
11
12
|
interface LinkHeaderConfig {
|
|
12
13
|
apiCatalog?: {
|
|
13
14
|
href: string;
|
|
14
15
|
};
|
|
16
|
+
describedby?: boolean;
|
|
15
17
|
i18n?: RuntimeI18nConfig | null;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
@@ -4,6 +4,7 @@ import { computeLocaleAlternates } from "./i18n.js";
|
|
|
4
4
|
export function encodePathForHeader(path) {
|
|
5
5
|
return encodeURI(path);
|
|
6
6
|
}
|
|
7
|
+
export const LLMS_TXT_PATH = "/llms.txt";
|
|
7
8
|
function resolveHeaderUrl(path, resolveUrl) {
|
|
8
9
|
if (!resolveUrl)
|
|
9
10
|
return path;
|
|
@@ -21,6 +22,11 @@ export function buildLinkHeader(path, variant, config, resolveUrl, routeContext
|
|
|
21
22
|
} else {
|
|
22
23
|
const href = resolveHeaderUrl(path, resolveUrl);
|
|
23
24
|
parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/html"`);
|
|
25
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="canonical"`);
|
|
26
|
+
}
|
|
27
|
+
if (config.describedby !== false) {
|
|
28
|
+
const href = resolveHeaderUrl(LLMS_TXT_PATH, resolveUrl);
|
|
29
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="describedby"`);
|
|
24
30
|
}
|
|
25
31
|
if (config.i18n) {
|
|
26
32
|
const alternates = computeLocaleAlternates(path, config.i18n, routeContext);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { negotiateContent } from "@mdream/js/negotiate";
|
|
2
2
|
import { getBotInfo } from "@nuxtjs/robots/util";
|
|
3
3
|
import { getHeaders } from "#nuxtseo/h3";
|
|
4
|
+
import { isReservedPath } from "../../markdown-path.js";
|
|
5
|
+
const RE_MD_EXT = /\.md$/;
|
|
4
6
|
export function toMarkdownRequest(event, isPrerender = !!import.meta.prerender) {
|
|
5
7
|
return { path: event.path, headers: getHeaders(event), isPrerender };
|
|
6
8
|
}
|
|
@@ -25,7 +27,7 @@ export function getRequestRenderInfo(request, mode = { _tag: "runtime", contentN
|
|
|
25
27
|
const queryIndex = request.path.indexOf("?");
|
|
26
28
|
const originalPath = queryIndex === -1 ? request.path : request.path.slice(0, queryIndex);
|
|
27
29
|
const isPrerender = mode._tag === "prerender";
|
|
28
|
-
if (
|
|
30
|
+
if (isReservedPath(originalPath.replace(RE_MD_EXT, "")))
|
|
29
31
|
return null;
|
|
30
32
|
const accept = request.headers.accept || "";
|
|
31
33
|
if (!originalPath.endsWith(".md") && accept && /\b(?:application\/json|text\/event-stream)\b/i.test(accept) && !/text\/(?:html|markdown|plain)\b|\*\/\*/i.test(accept)) {
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import type { H3Event } from '#nuxtseo/h3';
|
|
2
2
|
import type { StaleCheckResult } from './checkStale.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why a run could not finish.
|
|
5
|
+
*
|
|
6
|
+
* `lock` means the run never started, so nothing was changed. `run` means it
|
|
7
|
+
* started and stopped part way, so some work may already be committed.
|
|
8
|
+
*/
|
|
9
|
+
export interface CronFailure {
|
|
10
|
+
stage: 'lock' | 'run';
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
3
13
|
export interface CronResult {
|
|
14
|
+
/**
|
|
15
|
+
* Set when the run failed.
|
|
16
|
+
*
|
|
17
|
+
* A scheduled task that throws reaches Cloudflare as `scriptThrewException`,
|
|
18
|
+
* which carries no stage, no message, and nothing a host can report. The
|
|
19
|
+
* failure travels back as a value instead, so the caller decides what to do
|
|
20
|
+
* with it.
|
|
21
|
+
*/
|
|
22
|
+
failed?: CronFailure;
|
|
4
23
|
runId?: number | null;
|
|
5
24
|
stale?: StaleCheckResult;
|
|
6
25
|
sitemap?: {
|
|
@@ -19,6 +38,11 @@ export interface CronResult {
|
|
|
19
38
|
}
|
|
20
39
|
/**
|
|
21
40
|
* Run cron job logic - shared between scheduled task and HTTP endpoint
|
|
41
|
+
*
|
|
42
|
+
* This is the error boundary for the whole run. Nothing throws past it: the
|
|
43
|
+
* scheduled task has no request context and no handler above it, so a throw
|
|
44
|
+
* here becomes an opaque platform exception rather than something the host can
|
|
45
|
+
* report. Every failure comes back as `CronResult.failed`.
|
|
22
46
|
*/
|
|
23
47
|
export declare function runCron(event: H3Event | undefined, options?: {
|
|
24
48
|
batchSize?: number;
|
|
@@ -6,17 +6,34 @@ import { checkAndHandleStale, STALE_CHECK_INTERVAL_MS } from "./checkStale.js";
|
|
|
6
6
|
import { resolveCronPlan, resolveSitemapIntervalMinutes } from "./cron-plan.js";
|
|
7
7
|
import { crawlSitemapByRoute, getSitemapsFromConfig } from "./sitemap.js";
|
|
8
8
|
import { mapSitemapRoutes } from "./sitemap-routes.js";
|
|
9
|
+
function describeFailure(error) {
|
|
10
|
+
if (error instanceof Error)
|
|
11
|
+
return error.message;
|
|
12
|
+
return String(error);
|
|
13
|
+
}
|
|
9
14
|
export async function runCron(event, options) {
|
|
10
15
|
if (import.meta.dev)
|
|
11
16
|
return {};
|
|
17
|
+
return executeCron(event, options).catch((error) => {
|
|
18
|
+
const message = describeFailure(error);
|
|
19
|
+
logger.error(`[cron] Run failed: ${message}`);
|
|
20
|
+
return { failed: { stage: "run", message } };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function executeCron(event, options) {
|
|
12
24
|
const config = useRuntimeConfig()["nuxt-ai-ready"];
|
|
13
25
|
const debug = config.debug;
|
|
14
26
|
const startTime = Date.now();
|
|
15
27
|
const results = {};
|
|
16
28
|
const allErrors = [];
|
|
17
29
|
const sitemapIntervalMinutes = resolveSitemapIntervalMinutes(config.runtimeSync.ttl);
|
|
18
|
-
const
|
|
19
|
-
|
|
30
|
+
const lock = await tryAcquireCronLock(event).catch((error) => {
|
|
31
|
+
logger.error(`[cron] Could not acquire the lock: ${describeFailure(error)}`);
|
|
32
|
+
return null;
|
|
33
|
+
});
|
|
34
|
+
if (lock === null)
|
|
35
|
+
return { failed: { stage: "lock", message: "Could not acquire the cron lock" } };
|
|
36
|
+
if (lock._tag === "held") {
|
|
20
37
|
if (debug) {
|
|
21
38
|
logger.info(`[cron] Skipping - another cron run is in progress`);
|
|
22
39
|
}
|
|
@@ -122,7 +139,7 @@ export async function runCron(event, options) {
|
|
|
122
139
|
}
|
|
123
140
|
return results;
|
|
124
141
|
} finally {
|
|
125
|
-
await releaseCronLock(event).catch((err) => {
|
|
142
|
+
await releaseCronLock(event, lock.token).catch((err) => {
|
|
126
143
|
logger.warn(`[cron] Failed to release lock: ${err?.message || err}`);
|
|
127
144
|
});
|
|
128
145
|
}
|
|
@@ -152,7 +169,7 @@ async function pingSitemap(event, config, sitemapIntervalMinutes, debug) {
|
|
|
152
169
|
route: nextSitemap.route,
|
|
153
170
|
state: nextSitemap.crawlState
|
|
154
171
|
});
|
|
155
|
-
const routes = [...mapSitemapRoutes(result.urls).
|
|
172
|
+
const routes = [...mapSitemapRoutes(result.urls).entries()].map(([route, url]) => ({ route, url: url.loc }));
|
|
156
173
|
if (routes.length > 0)
|
|
157
174
|
await seedRoutes(event, routes);
|
|
158
175
|
if (result._tag === "failed") {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SITEMAP_MD_ROUTE = "/sitemap.md";
|
|
2
|
+
export interface SitemapMdEntry {
|
|
3
|
+
route: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
updatedAt?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SitemapMdOptions {
|
|
8
|
+
siteName?: string;
|
|
9
|
+
resolveHref?: (route: string) => string;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildSitemapMd(entries: SitemapMdEntry[], options?: SitemapMdOptions): string;
|
|
12
|
+
export declare function appendSitemapSection(markdown: string, sitemapHref?: string): string;
|
|
13
|
+
export declare function isSitemapMdRequest(path: string, baseURL: string, enabled: boolean): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { toMarkdownPath } from "../../markdown-path.js";
|
|
2
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
3
|
+
export const SITEMAP_MD_ROUTE = "/sitemap.md";
|
|
4
|
+
function topSegment(route) {
|
|
5
|
+
return route.split("/").filter(Boolean)[0] || "";
|
|
6
|
+
}
|
|
7
|
+
function escapeTitle(title) {
|
|
8
|
+
return title.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
9
|
+
}
|
|
10
|
+
function formatEntry(entry, resolveHref) {
|
|
11
|
+
const title = escapeTitle(entry.title?.trim() || entry.route);
|
|
12
|
+
const href = resolveHref(entry.route);
|
|
13
|
+
if (entry.updatedAt) {
|
|
14
|
+
const parsed = new Date(entry.updatedAt);
|
|
15
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
16
|
+
return `- [${title}](${href} "${parsed.toISOString()}")`;
|
|
17
|
+
}
|
|
18
|
+
return `- [${title}](${href})`;
|
|
19
|
+
}
|
|
20
|
+
export function buildSitemapMd(entries, options = {}) {
|
|
21
|
+
const resolveHref = options.resolveHref || toMarkdownPath;
|
|
22
|
+
const groups = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const entry of [...entries].sort((a, b) => a.route.localeCompare(b.route))) {
|
|
24
|
+
const key = topSegment(entry.route);
|
|
25
|
+
const group = groups.get(key);
|
|
26
|
+
if (group)
|
|
27
|
+
group.push(entry);
|
|
28
|
+
else
|
|
29
|
+
groups.set(key, [entry]);
|
|
30
|
+
}
|
|
31
|
+
const keys = [...groups.keys()].sort((a, b) => {
|
|
32
|
+
if (a === "")
|
|
33
|
+
return -1;
|
|
34
|
+
if (b === "")
|
|
35
|
+
return 1;
|
|
36
|
+
return a.localeCompare(b);
|
|
37
|
+
});
|
|
38
|
+
const parts = [`# ${options.siteName || "Site"} Sitemap`, "", "All pages in Markdown format."];
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
parts.push("", `## ${key || "Root"}`);
|
|
41
|
+
for (const entry of groups.get(key))
|
|
42
|
+
parts.push(formatEntry(entry, resolveHref));
|
|
43
|
+
}
|
|
44
|
+
return `${parts.join("\n")}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
export function appendSitemapSection(markdown, sitemapHref = SITEMAP_MD_ROUTE) {
|
|
48
|
+
const section = `## Sitemap
|
|
49
|
+
|
|
50
|
+
See the full [sitemap](${sitemapHref}) for all pages.`;
|
|
51
|
+
const trimmed = markdown.trimEnd();
|
|
52
|
+
if (trimmed.endsWith(section))
|
|
53
|
+
return `${trimmed}
|
|
54
|
+
`;
|
|
55
|
+
return `${trimmed}
|
|
56
|
+
|
|
57
|
+
${section}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
export function isSitemapMdRequest(path, baseURL, enabled) {
|
|
61
|
+
if (!enabled)
|
|
62
|
+
return false;
|
|
63
|
+
const withoutQuery = path.split("?")[0] || path;
|
|
64
|
+
return withoutQuery === SITEMAP_MD_ROUTE || withoutQuery === toDeployedRoute(SITEMAP_MD_ROUTE, baseURL);
|
|
65
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -149,6 +149,19 @@ export interface ModuleOptions {
|
|
|
149
149
|
* Structured llms.txt configuration
|
|
150
150
|
*/
|
|
151
151
|
llmsTxt?: LlmsTxtConfig;
|
|
152
|
+
/**
|
|
153
|
+
* Generate a Markdown sitemap at `/sitemap.md` from indexed pages and end
|
|
154
|
+
* every Markdown page with a `## Sitemap` section linking to it.
|
|
155
|
+
* @default true
|
|
156
|
+
*/
|
|
157
|
+
sitemapMd?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Advertise llms.txt as `rel="describedby"` from HTML and Markdown
|
|
160
|
+
* responses, as the llms.txt v2 spec requires. Emits an HTML `<link>` tag
|
|
161
|
+
* and a Link header entry pointing at the llms.txt route.
|
|
162
|
+
* @default true
|
|
163
|
+
*/
|
|
164
|
+
describedby?: boolean;
|
|
152
165
|
/**
|
|
153
166
|
* Content Signal Directives
|
|
154
167
|
*/
|
|
@@ -258,7 +271,7 @@ export interface ModuleOptions {
|
|
|
258
271
|
filename?: string;
|
|
259
272
|
/**
|
|
260
273
|
* D1 binding name for Cloudflare Workers/Pages
|
|
261
|
-
* @default '
|
|
274
|
+
* @default 'DB'
|
|
262
275
|
*/
|
|
263
276
|
bindingName?: string;
|
|
264
277
|
/**
|
|
@@ -272,7 +285,7 @@ export interface ModuleOptions {
|
|
|
272
285
|
authToken?: string;
|
|
273
286
|
};
|
|
274
287
|
/**
|
|
275
|
-
* Enable scheduled cron task (runs every
|
|
288
|
+
* Enable scheduled cron task (runs every 5 minutes)
|
|
276
289
|
* When true, automatically enables runtimeSync for background indexing
|
|
277
290
|
*/
|
|
278
291
|
cron?: boolean;
|
|
@@ -322,7 +335,7 @@ export interface ModuleOptions {
|
|
|
322
335
|
ttl?: number;
|
|
323
336
|
/**
|
|
324
337
|
* Pages to index per batch
|
|
325
|
-
* @default
|
|
338
|
+
* @default 50
|
|
326
339
|
*/
|
|
327
340
|
batchSize?: number;
|
|
328
341
|
/**
|
|
@@ -8,9 +8,12 @@ import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-con
|
|
|
8
8
|
import { resolveNuxtContentVersion, renderNitroTypeAugmentations, setupNitroRuntimeCompatibility } from 'nuxtseo-shared/kit';
|
|
9
9
|
import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
import { isReservedPath, normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
12
|
+
import { SITEMAP_MD_ROUTE } from '../../dist/runtime/server/utils/sitemap-md.js';
|
|
11
13
|
import { MCP_SERVER_CARD_MEDIA_TYPE, AI_CATALOG_MEDIA_TYPE } from '../../dist/runtime/server/utils/discovery-response.js';
|
|
12
14
|
import { isAbsolute, join as join$1 } from 'pathe';
|
|
13
15
|
import { resolveI18nConfig, toRuntimeI18nConfig as toRuntimeI18nConfig$1 } from 'nuxtseo-shared/i18n';
|
|
16
|
+
import { toDeployedRoute } from '../../dist/runtime/route-path.js';
|
|
14
17
|
|
|
15
18
|
const SELECT_PAGE_DATA = "SELECT route, title, description, markdown, headings, keywords, updated_at, is_error, locale FROM ai_ready_pages";
|
|
16
19
|
function createBuildPageDataVirtual(options) {
|
|
@@ -175,7 +178,7 @@ function resolveModuleEntryUrl(url) {
|
|
|
175
178
|
|
|
176
179
|
const MARKDOWN_LINK_AVAILABILITY_FILE = "markdown-link-availability.json";
|
|
177
180
|
|
|
178
|
-
const
|
|
181
|
+
const webmcpTypes = `
|
|
179
182
|
import type { WebMcpToolsContext } from 'nuxt-ai-ready/webmcp'
|
|
180
183
|
|
|
181
184
|
declare module '#app' {
|
|
@@ -184,37 +187,6 @@ declare module '#app' {
|
|
|
184
187
|
'ai-ready:webmcp:tools': (context: WebMcpToolsContext) => void | Promise<void>
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
|
-
|
|
188
|
-
declare module '@vue/runtime-dom' {
|
|
189
|
-
interface HTMLAttributes {
|
|
190
|
-
/** Names the tool this form exposes to agents. */
|
|
191
|
-
toolname?: string
|
|
192
|
-
/** Describes what submitting this form does. */
|
|
193
|
-
tooldescription?: string
|
|
194
|
-
/** Describes this field as a tool parameter. */
|
|
195
|
-
toolparamdescription?: string
|
|
196
|
-
/** Submit the form as soon as an agent fills it in. */
|
|
197
|
-
toolautosubmit?: boolean | ''
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
declare global {
|
|
202
|
-
interface SubmitEvent {
|
|
203
|
-
/** Whether an agent triggered this submit through a tool call. */
|
|
204
|
-
readonly agentInvoked?: boolean
|
|
205
|
-
/** Return a result to the agent. Call preventDefault() first. */
|
|
206
|
-
respondWith?: (result: Promise<unknown>) => void
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
interface WebMcpToolEvent extends Event {
|
|
210
|
-
readonly toolName: string
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
interface WindowEventMap {
|
|
214
|
-
toolactivated: WebMcpToolEvent
|
|
215
|
-
toolcancel: WebMcpToolEvent
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
190
|
`;
|
|
219
191
|
function registerTypeTemplates(ctx) {
|
|
220
192
|
const nitroTypes = renderNitroTypeAugmentations(ctx.nitroCompatibility, {
|
|
@@ -231,7 +203,7 @@ import type { MarkdownContext, MarkdownSourceContext, PageIndexedContext } from
|
|
|
231
203
|
import type { MdreamOptions } from 'mdream'
|
|
232
204
|
|
|
233
205
|
${nitroTypes}
|
|
234
|
-
${ctx.config.webmcp ?
|
|
206
|
+
${ctx.config.webmcp ? webmcpTypes : ""}
|
|
235
207
|
export {}
|
|
236
208
|
`
|
|
237
209
|
});
|
|
@@ -892,6 +864,57 @@ function ensureStaticHeader(contents, route, name, value) {
|
|
|
892
864
|
return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
|
|
893
865
|
}
|
|
894
866
|
|
|
867
|
+
const RE_MD_EXT = /\.md$/;
|
|
868
|
+
function isStaticMarkdownSourceRoute(route) {
|
|
869
|
+
if (route.includes("*") || route.includes(":"))
|
|
870
|
+
return false;
|
|
871
|
+
const path = route.split("?")[0] || route;
|
|
872
|
+
if (isReservedPath(path))
|
|
873
|
+
return false;
|
|
874
|
+
const lastSegment = path.split("/").pop() || "";
|
|
875
|
+
return !lastSegment.includes(".");
|
|
876
|
+
}
|
|
877
|
+
function staticDescribedbyEntry(baseURL) {
|
|
878
|
+
return `<${encodeURI(toDeployedRoute("/llms.txt", baseURL))}>; rel="describedby"`;
|
|
879
|
+
}
|
|
880
|
+
function buildStaticMarkdownLinkHeader(route, baseURL, describedby) {
|
|
881
|
+
const htmlRoute = encodeURI(toDeployedRoute(normalizePagePath(route), baseURL));
|
|
882
|
+
const parts = [
|
|
883
|
+
`<${htmlRoute}>; rel="alternate"; type="text/html"`,
|
|
884
|
+
`<${htmlRoute}>; rel="canonical"`
|
|
885
|
+
];
|
|
886
|
+
if (describedby)
|
|
887
|
+
parts.push(staticDescribedbyEntry(baseURL));
|
|
888
|
+
return parts.join(", ");
|
|
889
|
+
}
|
|
890
|
+
function pageRouteFromMarkdownTwin(fileName) {
|
|
891
|
+
if (!fileName?.endsWith(".md") || fileName === SITEMAP_MD_ROUTE)
|
|
892
|
+
return null;
|
|
893
|
+
const pageRoute = normalizePagePath(fileName.replace(RE_MD_EXT, ""));
|
|
894
|
+
if (pageRoute === "/index")
|
|
895
|
+
return "/";
|
|
896
|
+
return isStaticMarkdownSourceRoute(pageRoute) ? pageRoute : null;
|
|
897
|
+
}
|
|
898
|
+
function prerenderedMarkdownHeaderRules(prerenderedRoutes, baseURL, describedby) {
|
|
899
|
+
const rules = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const entry of prerenderedRoutes) {
|
|
901
|
+
const pageRoute = pageRouteFromMarkdownTwin(entry.fileName);
|
|
902
|
+
if (pageRoute === null)
|
|
903
|
+
continue;
|
|
904
|
+
const mdRoute = toMarkdownPath(pageRoute);
|
|
905
|
+
if (!rules.has(mdRoute)) {
|
|
906
|
+
rules.set(mdRoute, {
|
|
907
|
+
route: mdRoute,
|
|
908
|
+
headers: {
|
|
909
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
910
|
+
"Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby)
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return [...rules.values()];
|
|
916
|
+
}
|
|
917
|
+
|
|
895
918
|
const DEFAULT_MAX_OUTPUT_CHARS = 1500;
|
|
896
919
|
const DEFAULT_LIST_LIMIT = 20;
|
|
897
920
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
@@ -1216,7 +1239,12 @@ ${details}`);
|
|
|
1216
1239
|
userAgent: "*",
|
|
1217
1240
|
// Preserve nuxt-robots' default wildcard rule so the injected group remains valid.
|
|
1218
1241
|
disallow: [""],
|
|
1219
|
-
|
|
1242
|
+
// Object form renders one comma-separated line; an array renders one line per entry, which validators reject.
|
|
1243
|
+
contentSignal: {
|
|
1244
|
+
"ai-train": config.contentSignal.aiTrain ? "yes" : "no",
|
|
1245
|
+
"search": config.contentSignal.search ? "yes" : "no",
|
|
1246
|
+
"ai-input": config.contentSignal.aiInput ? "yes" : "no"
|
|
1247
|
+
}
|
|
1220
1248
|
};
|
|
1221
1249
|
if (config.contentSignal.contentUsage !== false)
|
|
1222
1250
|
group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
|
|
@@ -1600,6 +1628,8 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1600
1628
|
debugCron: config.debugCron || false,
|
|
1601
1629
|
contentNegotiation: config.contentNegotiation === void 0 ? "auto" : config.contentNegotiation ? "enabled" : "disabled",
|
|
1602
1630
|
mdreamOptions: config.mdreamOptions || {},
|
|
1631
|
+
sitemapMd: config.sitemapMd !== false,
|
|
1632
|
+
describedby: config.describedby !== false,
|
|
1603
1633
|
markdownCacheHeaders: defu(config.markdownCacheHeaders, {
|
|
1604
1634
|
maxAge: 3600,
|
|
1605
1635
|
swr: true
|
|
@@ -1643,6 +1673,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1643
1673
|
});
|
|
1644
1674
|
addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get"), lazy: true });
|
|
1645
1675
|
addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms-full.txt.get"), lazy: true });
|
|
1676
|
+
if (config.sitemapMd !== false) {
|
|
1677
|
+
addServerHandler({ route: SITEMAP_MD_ROUTE, handler: resolve("./runtime/server/routes/sitemap.md.get"), lazy: true });
|
|
1678
|
+
}
|
|
1646
1679
|
if (agentSkillsResult._tag === "Enabled") {
|
|
1647
1680
|
addServerHandler({ route: AGENT_SKILLS_INDEX_ROUTE, handler: resolve("./runtime/server/routes/agent-skills-index"), lazy: true });
|
|
1648
1681
|
for (const route of Object.keys(agentSkillsResult.localArtifacts)) {
|
|
@@ -1689,7 +1722,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1689
1722
|
addServerHandler({ route: "/__ai-ready/pages", handler: resolve("./runtime/server/routes/__ai-ready/pages.get"), lazy: true });
|
|
1690
1723
|
}
|
|
1691
1724
|
}
|
|
1692
|
-
|
|
1725
|
+
if (nuxt.options.dev || config.debug) {
|
|
1726
|
+
addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get"), lazy: true });
|
|
1727
|
+
}
|
|
1693
1728
|
if (config.debug) {
|
|
1694
1729
|
addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get"), lazy: true });
|
|
1695
1730
|
}
|
|
@@ -1698,6 +1733,7 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1698
1733
|
addServerHandler({ route: "/__ai-ready/poll", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/poll.post"), lazy: true });
|
|
1699
1734
|
addServerHandler({ route: "/__ai-ready/prune", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/prune.post"), lazy: true });
|
|
1700
1735
|
addServerHandler({ route: "/__ai-ready/restore", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/restore.post"), lazy: true });
|
|
1736
|
+
addServerHandler({ route: "/__ai-ready/reindex", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/reindex.post"), lazy: true });
|
|
1701
1737
|
addServerPlugin(resolve("./runtime/server/plugins/sitemap-seeder"));
|
|
1702
1738
|
}
|
|
1703
1739
|
if (config.cron && !nuxt.options.dev) {
|
|
@@ -1735,19 +1771,50 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1735
1771
|
for (const route of ["/llms.txt", "/llms-full.txt"]) {
|
|
1736
1772
|
extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
1737
1773
|
}
|
|
1774
|
+
if (config.sitemapMd !== false) {
|
|
1775
|
+
extendRouteRules(SITEMAP_MD_ROUTE, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
|
|
1776
|
+
}
|
|
1777
|
+
const staticBaseURL = nuxt.options.app.baseURL || "/";
|
|
1778
|
+
for (const route of nuxt.options.nitro.prerender?.routes || []) {
|
|
1779
|
+
if (!isStaticMarkdownSourceRoute(route))
|
|
1780
|
+
continue;
|
|
1781
|
+
extendRouteRules(toMarkdownPath(route), {
|
|
1782
|
+
headers: {
|
|
1783
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1784
|
+
"Link": buildStaticMarkdownLinkHeader(route, staticBaseURL, config.describedby !== false)
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1738
1788
|
nuxt.hooks.hook("nitro:build:before", (nitro) => {
|
|
1789
|
+
nitro.hooks.hook("prerender:done", () => {
|
|
1790
|
+
for (const { route, headers } of prerenderedMarkdownHeaderRules(
|
|
1791
|
+
nitro._prerenderedRoutes || [],
|
|
1792
|
+
staticBaseURL,
|
|
1793
|
+
config.describedby !== false
|
|
1794
|
+
)) {
|
|
1795
|
+
nitro.options.routeRules[route] = defu({ headers }, nitro.options.routeRules[route]);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1739
1798
|
nitro.hooks.hook("compiled", async () => {
|
|
1740
1799
|
const headersPath = join(nitro.options.output.publicDir, "_headers");
|
|
1741
1800
|
logger.debug(`Checking for _headers file: ${headersPath}`);
|
|
1742
1801
|
const exists = await access(headersPath).then(() => true).catch(() => false);
|
|
1743
1802
|
if (exists) {
|
|
1744
1803
|
const headers = await readFile(headersPath, "utf8");
|
|
1745
|
-
|
|
1804
|
+
let mergedHeaders = ensureStaticHeader(
|
|
1746
1805
|
headers,
|
|
1747
1806
|
"/*.md",
|
|
1748
1807
|
"Content-Type",
|
|
1749
1808
|
"text/markdown; charset=utf-8"
|
|
1750
1809
|
);
|
|
1810
|
+
if (config.describedby !== false) {
|
|
1811
|
+
mergedHeaders = ensureStaticHeader(
|
|
1812
|
+
mergedHeaders,
|
|
1813
|
+
"/*.md",
|
|
1814
|
+
"Link",
|
|
1815
|
+
staticDescribedbyEntry(nitro.options.baseURL || "/")
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1751
1818
|
if (mergedHeaders !== headers) {
|
|
1752
1819
|
await writeFile(headersPath, mergedHeaders);
|
|
1753
1820
|
logger.debug("Merged .md charset header into _headers");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -66,17 +66,17 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@mdream/js": "^1.7.
|
|
69
|
+
"@mdream/js": "^1.7.1",
|
|
70
70
|
"@nuxt/kit": "^4.5.2",
|
|
71
71
|
"citty": "^0.2.2",
|
|
72
72
|
"consola": "^3.4.2",
|
|
73
73
|
"defu": "^6.1.7",
|
|
74
|
-
"drizzle-orm": "
|
|
75
|
-
"mdream": "^1.7.
|
|
74
|
+
"drizzle-orm": "1.0.0-rc.4",
|
|
75
|
+
"mdream": "^1.7.1",
|
|
76
76
|
"nuxt-site-config": "^4.2.3",
|
|
77
77
|
"nuxtseo-shared": "^5.3.14",
|
|
78
78
|
"pathe": "^2.0.3",
|
|
79
|
-
"pkg-types": "^2.3.
|
|
79
|
+
"pkg-types": "^2.3.2",
|
|
80
80
|
"site-config-stack": "^4.2.3",
|
|
81
81
|
"sitemapd": "^0.2.2",
|
|
82
82
|
"ufo": "^1.6.4",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"yaml": "^2.9.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@antfu/eslint-config": "^9.
|
|
87
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
88
88
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
89
89
|
"@harlan-zw/comark-content": "^0.1.5",
|
|
90
|
-
"@libsql/client": "^0.
|
|
90
|
+
"@libsql/client": "^0.18.0",
|
|
91
91
|
"@nuxt/content": "^3.16.0",
|
|
92
92
|
"@nuxt/module-builder": "^1.0.3",
|
|
93
93
|
"@nuxt/test-utils": "^4.2.0",
|
|
@@ -96,31 +96,31 @@
|
|
|
96
96
|
"@nuxtjs/robots": "^6.2.0",
|
|
97
97
|
"@nuxtjs/sitemap": "^8.5.0",
|
|
98
98
|
"@types/better-sqlite3": "^9.6.0",
|
|
99
|
-
"@vitest/coverage-v8": "^
|
|
99
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
100
100
|
"@vue/test-utils": "^2.5.0",
|
|
101
101
|
"@vueuse/nuxt": "^14.4.0",
|
|
102
102
|
"better-sqlite3": "^13.0.3",
|
|
103
|
-
"bumpp": "^12.
|
|
104
|
-
"eslint": "^10.
|
|
105
|
-
"eslint-plugin-harlanzw": "^0.21.
|
|
103
|
+
"bumpp": "^12.3.0",
|
|
104
|
+
"eslint": "^10.10.0",
|
|
105
|
+
"eslint-plugin-harlanzw": "^0.21.1",
|
|
106
106
|
"execa": "^10.0.1",
|
|
107
107
|
"h3": "^1.15.11",
|
|
108
|
-
"happy-dom": "^20.
|
|
108
|
+
"happy-dom": "^20.14.0",
|
|
109
109
|
"nitropack": "^2.13.4",
|
|
110
110
|
"nuxt": "^4.5.2",
|
|
111
111
|
"nuxt-site-config": "^4.2.3",
|
|
112
112
|
"nuxtseo-layer-devtools": "^5.3.14",
|
|
113
|
-
"playwright": "^1.
|
|
114
|
-
"playwright-core": "^1.
|
|
113
|
+
"playwright": "^1.63.0",
|
|
114
|
+
"playwright-core": "^1.63.0",
|
|
115
115
|
"postgres": "^3.4.9",
|
|
116
116
|
"tinyglobby": "^0.2.17",
|
|
117
117
|
"typescript": "6.0.3",
|
|
118
118
|
"unbuild": "^3.6.1",
|
|
119
|
-
"vitest": "^
|
|
119
|
+
"vitest": "^5.0.0",
|
|
120
120
|
"vue": "^3.5.42",
|
|
121
|
-
"vue-router": "^5.3.
|
|
121
|
+
"vue-router": "^5.3.1",
|
|
122
122
|
"vue-tsc": "^3.3.11",
|
|
123
|
-
"wrangler": "^4.
|
|
123
|
+
"wrangler": "^4.129.0",
|
|
124
124
|
"zod": "^4.5.4"
|
|
125
125
|
},
|
|
126
126
|
"scripts": {
|