nuxt-ai-ready 2.1.0 → 2.2.1
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.BVWXwmUS.mjs} +140 -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
|
});
|
|
@@ -891,6 +863,85 @@ function ensureStaticHeader(contents, route, name, value) {
|
|
|
891
863
|
const prefix = blockStart === routeLineEnd ? eol : "";
|
|
892
864
|
return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
|
|
893
865
|
}
|
|
866
|
+
const CLOUDFLARE_STATIC_HEADER_RULE_LIMIT = 100;
|
|
867
|
+
const RE_EXACT_MARKDOWN_ROUTE = /^\/[^*\s]*\.md[\t ]*\r?$/;
|
|
868
|
+
function splitHeaderBlocks(contents) {
|
|
869
|
+
const blocks = [];
|
|
870
|
+
let current = { route: null, text: "" };
|
|
871
|
+
for (const line of contents.split(/(?<=\n)/)) {
|
|
872
|
+
const isRoute = /^[^\s#]/.test(line);
|
|
873
|
+
if (isRoute) {
|
|
874
|
+
blocks.push(current);
|
|
875
|
+
current = { route: line.trimEnd(), text: "" };
|
|
876
|
+
}
|
|
877
|
+
current.text += line;
|
|
878
|
+
}
|
|
879
|
+
blocks.push(current);
|
|
880
|
+
return blocks;
|
|
881
|
+
}
|
|
882
|
+
function enforceStaticHeaderBudget(contents, limit) {
|
|
883
|
+
const blocks = splitHeaderBlocks(contents);
|
|
884
|
+
const ruleCount = blocks.filter((block) => block.route !== null).length;
|
|
885
|
+
if (ruleCount <= limit)
|
|
886
|
+
return { contents, total: ruleCount, dropped: 0 };
|
|
887
|
+
const kept = blocks.filter((block) => block.route === null || !RE_EXACT_MARKDOWN_ROUTE.test(block.route));
|
|
888
|
+
return {
|
|
889
|
+
contents: kept.map((block) => block.text).join(""),
|
|
890
|
+
total: kept.filter((block) => block.route !== null).length,
|
|
891
|
+
dropped: ruleCount - kept.filter((block) => block.route !== null).length
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const RE_MD_EXT = /\.md$/;
|
|
896
|
+
function isStaticMarkdownSourceRoute(route) {
|
|
897
|
+
if (route.includes("*") || route.includes(":"))
|
|
898
|
+
return false;
|
|
899
|
+
const path = route.split("?")[0] || route;
|
|
900
|
+
if (isReservedPath(path))
|
|
901
|
+
return false;
|
|
902
|
+
const lastSegment = path.split("/").pop() || "";
|
|
903
|
+
return !lastSegment.includes(".");
|
|
904
|
+
}
|
|
905
|
+
function staticDescribedbyEntry(baseURL) {
|
|
906
|
+
return `<${encodeURI(toDeployedRoute("/llms.txt", baseURL))}>; rel="describedby"`;
|
|
907
|
+
}
|
|
908
|
+
function buildStaticMarkdownLinkHeader(route, baseURL, describedby) {
|
|
909
|
+
const htmlRoute = encodeURI(toDeployedRoute(normalizePagePath(route), baseURL));
|
|
910
|
+
const parts = [
|
|
911
|
+
`<${htmlRoute}>; rel="alternate"; type="text/html"`,
|
|
912
|
+
`<${htmlRoute}>; rel="canonical"`
|
|
913
|
+
];
|
|
914
|
+
if (describedby)
|
|
915
|
+
parts.push(staticDescribedbyEntry(baseURL));
|
|
916
|
+
return parts.join(", ");
|
|
917
|
+
}
|
|
918
|
+
function pageRouteFromMarkdownTwin(fileName) {
|
|
919
|
+
if (!fileName?.endsWith(".md") || fileName === SITEMAP_MD_ROUTE)
|
|
920
|
+
return null;
|
|
921
|
+
const pageRoute = normalizePagePath(fileName.replace(RE_MD_EXT, ""));
|
|
922
|
+
if (pageRoute === "/index")
|
|
923
|
+
return "/";
|
|
924
|
+
return isStaticMarkdownSourceRoute(pageRoute) ? pageRoute : null;
|
|
925
|
+
}
|
|
926
|
+
function prerenderedMarkdownHeaderRules(prerenderedRoutes, baseURL, describedby) {
|
|
927
|
+
const rules = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const entry of prerenderedRoutes) {
|
|
929
|
+
const pageRoute = pageRouteFromMarkdownTwin(entry.fileName);
|
|
930
|
+
if (pageRoute === null)
|
|
931
|
+
continue;
|
|
932
|
+
const mdRoute = toMarkdownPath(pageRoute);
|
|
933
|
+
if (!rules.has(mdRoute)) {
|
|
934
|
+
rules.set(mdRoute, {
|
|
935
|
+
route: mdRoute,
|
|
936
|
+
headers: {
|
|
937
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
938
|
+
"Link": buildStaticMarkdownLinkHeader(pageRoute, baseURL, describedby)
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return [...rules.values()];
|
|
944
|
+
}
|
|
894
945
|
|
|
895
946
|
const DEFAULT_MAX_OUTPUT_CHARS = 1500;
|
|
896
947
|
const DEFAULT_LIST_LIMIT = 20;
|
|
@@ -1216,7 +1267,12 @@ ${details}`);
|
|
|
1216
1267
|
userAgent: "*",
|
|
1217
1268
|
// Preserve nuxt-robots' default wildcard rule so the injected group remains valid.
|
|
1218
1269
|
disallow: [""],
|
|
1219
|
-
|
|
1270
|
+
// Object form renders one comma-separated line; an array renders one line per entry, which validators reject.
|
|
1271
|
+
contentSignal: {
|
|
1272
|
+
"ai-train": config.contentSignal.aiTrain ? "yes" : "no",
|
|
1273
|
+
"search": config.contentSignal.search ? "yes" : "no",
|
|
1274
|
+
"ai-input": config.contentSignal.aiInput ? "yes" : "no"
|
|
1275
|
+
}
|
|
1220
1276
|
};
|
|
1221
1277
|
if (config.contentSignal.contentUsage !== false)
|
|
1222
1278
|
group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
|
|
@@ -1600,6 +1656,8 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1600
1656
|
debugCron: config.debugCron || false,
|
|
1601
1657
|
contentNegotiation: config.contentNegotiation === void 0 ? "auto" : config.contentNegotiation ? "enabled" : "disabled",
|
|
1602
1658
|
mdreamOptions: config.mdreamOptions || {},
|
|
1659
|
+
sitemapMd: config.sitemapMd !== false,
|
|
1660
|
+
describedby: config.describedby !== false,
|
|
1603
1661
|
markdownCacheHeaders: defu(config.markdownCacheHeaders, {
|
|
1604
1662
|
maxAge: 3600,
|
|
1605
1663
|
swr: true
|
|
@@ -1643,6 +1701,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1643
1701
|
});
|
|
1644
1702
|
addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get"), lazy: true });
|
|
1645
1703
|
addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms-full.txt.get"), lazy: true });
|
|
1704
|
+
if (config.sitemapMd !== false) {
|
|
1705
|
+
addServerHandler({ route: SITEMAP_MD_ROUTE, handler: resolve("./runtime/server/routes/sitemap.md.get"), lazy: true });
|
|
1706
|
+
}
|
|
1646
1707
|
if (agentSkillsResult._tag === "Enabled") {
|
|
1647
1708
|
addServerHandler({ route: AGENT_SKILLS_INDEX_ROUTE, handler: resolve("./runtime/server/routes/agent-skills-index"), lazy: true });
|
|
1648
1709
|
for (const route of Object.keys(agentSkillsResult.localArtifacts)) {
|
|
@@ -1689,7 +1750,9 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1689
1750
|
addServerHandler({ route: "/__ai-ready/pages", handler: resolve("./runtime/server/routes/__ai-ready/pages.get"), lazy: true });
|
|
1690
1751
|
}
|
|
1691
1752
|
}
|
|
1692
|
-
|
|
1753
|
+
if (nuxt.options.dev || config.debug) {
|
|
1754
|
+
addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get"), lazy: true });
|
|
1755
|
+
}
|
|
1693
1756
|
if (config.debug) {
|
|
1694
1757
|
addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get"), lazy: true });
|
|
1695
1758
|
}
|
|
@@ -1698,6 +1761,7 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1698
1761
|
addServerHandler({ route: "/__ai-ready/poll", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/poll.post"), lazy: true });
|
|
1699
1762
|
addServerHandler({ route: "/__ai-ready/prune", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/prune.post"), lazy: true });
|
|
1700
1763
|
addServerHandler({ route: "/__ai-ready/restore", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/restore.post"), lazy: true });
|
|
1764
|
+
addServerHandler({ route: "/__ai-ready/reindex", method: "post", handler: resolve("./runtime/server/routes/__ai-ready/reindex.post"), lazy: true });
|
|
1701
1765
|
addServerPlugin(resolve("./runtime/server/plugins/sitemap-seeder"));
|
|
1702
1766
|
}
|
|
1703
1767
|
if (config.cron && !nuxt.options.dev) {
|
|
@@ -1735,19 +1799,59 @@ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
|
|
|
1735
1799
|
for (const route of ["/llms.txt", "/llms-full.txt"]) {
|
|
1736
1800
|
extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
1737
1801
|
}
|
|
1802
|
+
if (config.sitemapMd !== false) {
|
|
1803
|
+
extendRouteRules(SITEMAP_MD_ROUTE, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
|
|
1804
|
+
}
|
|
1805
|
+
const staticBaseURL = nuxt.options.app.baseURL || "/";
|
|
1806
|
+
for (const route of nuxt.options.nitro.prerender?.routes || []) {
|
|
1807
|
+
if (!isStaticMarkdownSourceRoute(route))
|
|
1808
|
+
continue;
|
|
1809
|
+
extendRouteRules(toMarkdownPath(route), {
|
|
1810
|
+
headers: {
|
|
1811
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1812
|
+
"Link": buildStaticMarkdownLinkHeader(route, staticBaseURL, config.describedby !== false)
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1738
1816
|
nuxt.hooks.hook("nitro:build:before", (nitro) => {
|
|
1817
|
+
nitro.hooks.hook("prerender:done", () => {
|
|
1818
|
+
for (const { route, headers } of prerenderedMarkdownHeaderRules(
|
|
1819
|
+
nitro._prerenderedRoutes || [],
|
|
1820
|
+
staticBaseURL,
|
|
1821
|
+
config.describedby !== false
|
|
1822
|
+
)) {
|
|
1823
|
+
nitro.options.routeRules[route] = defu({ headers }, nitro.options.routeRules[route]);
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1739
1826
|
nitro.hooks.hook("compiled", async () => {
|
|
1740
1827
|
const headersPath = join(nitro.options.output.publicDir, "_headers");
|
|
1741
1828
|
logger.debug(`Checking for _headers file: ${headersPath}`);
|
|
1742
1829
|
const exists = await access(headersPath).then(() => true).catch(() => false);
|
|
1743
1830
|
if (exists) {
|
|
1744
1831
|
const headers = await readFile(headersPath, "utf8");
|
|
1745
|
-
|
|
1832
|
+
let mergedHeaders = ensureStaticHeader(
|
|
1746
1833
|
headers,
|
|
1747
1834
|
"/*.md",
|
|
1748
1835
|
"Content-Type",
|
|
1749
1836
|
"text/markdown; charset=utf-8"
|
|
1750
1837
|
);
|
|
1838
|
+
if (config.describedby !== false) {
|
|
1839
|
+
mergedHeaders = ensureStaticHeader(
|
|
1840
|
+
mergedHeaders,
|
|
1841
|
+
"/*.md",
|
|
1842
|
+
"Link",
|
|
1843
|
+
staticDescribedbyEntry(nitro.options.baseURL || "/")
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
if (String(nitro.options.preset || "").startsWith("cloudflare")) {
|
|
1847
|
+
const budget = enforceStaticHeaderBudget(mergedHeaders, CLOUDFLARE_STATIC_HEADER_RULE_LIMIT);
|
|
1848
|
+
if (budget.dropped > 0) {
|
|
1849
|
+
mergedHeaders = budget.contents;
|
|
1850
|
+
logger.warn(`_headers had ${budget.total + budget.dropped} rules and Cloudflare allows ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}. Dropped the ${budget.dropped} per-page .md rules; the /*.md glob still sets the charset and describedby entries, but static markdown no longer sends a per-page canonical Link.`);
|
|
1851
|
+
}
|
|
1852
|
+
if (budget.total > CLOUDFLARE_STATIC_HEADER_RULE_LIMIT)
|
|
1853
|
+
logger.warn(`_headers still has ${budget.total} rules after dropping the .md rules. Cloudflare will reject the deploy until the other rules fit under ${CLOUDFLARE_STATIC_HEADER_RULE_LIMIT}.`);
|
|
1854
|
+
}
|
|
1751
1855
|
if (mergedHeaders !== headers) {
|
|
1752
1856
|
await writeFile(headersPath, mergedHeaders);
|
|
1753
1857
|
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.1
|
|
4
|
+
"version": "2.2.1",
|
|
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": {
|