nuxt-ai-ready 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/chunks/agent-skills.mjs +1 -1
- package/dist/chunks/prerender.mjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/app/plugins/md-hints.prerender.js +4 -4
- package/dist/runtime/llms-txt-utils.js +2 -6
- package/dist/runtime/markdown-path.d.ts +7 -3
- package/dist/runtime/markdown-path.js +6 -6
- package/dist/runtime/server/db/queries.js +23 -1
- package/dist/runtime/server/middleware/markdown.js +1 -1
- package/dist/runtime/server/middleware/markdown.prerender.js +1 -0
- package/dist/runtime/server/utils/checkStale.js +13 -2
- package/dist/runtime/server/utils/cloudflare.js +3 -1
- package/dist/runtime/server/utils/frontmatter.d.ts +6 -0
- package/dist/runtime/server/utils/frontmatter.js +23 -1
- package/dist/runtime/server/utils/indexPage.js +4 -1
- package/dist/runtime/server/utils/sitemap-routes.js +2 -1
- package/dist/runtime/server/utils.d.ts +8 -4
- package/dist/runtime/server/utils.js +15 -19
- package/dist/runtime/types.d.ts +8 -4
- package/dist/shared/{nuxt-ai-ready.CxJE2WLA.mjs → nuxt-ai-ready.DsokNmXc.mjs} +4 -10
- package/package.json +4 -4
- package/skills/nuxt-ai-ready/SKILL.md +123 -0
- package/skills/nuxt-ai-ready/references/runtime-indexing.md +75 -0
- package/dist/runtime/server/plugins/mcp-data.d.ts +0 -10
- package/dist/runtime/server/plugins/mcp-data.js +0 -22
package/README.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
[![npm version][npm-version-src]][npm-version-href]
|
|
4
4
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
5
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
6
|
+
<a href="https://skilld.dev/gh/harlan-zw/nuxt-ai-ready">
|
|
7
|
+
<picture>
|
|
8
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://skilld.dev/b/harlan-zw/nuxt-ai-ready?theme=dark">
|
|
9
|
+
<source media="(prefers-color-scheme: light)" srcset="https://skilld.dev/b/harlan-zw/nuxt-ai-ready?theme=light">
|
|
10
|
+
<img alt="Skill repository on skilld.dev" src="https://skilld.dev/b/harlan-zw/nuxt-ai-ready?theme=light">
|
|
11
|
+
</picture>
|
|
12
|
+
</a>
|
|
6
13
|
|
|
7
14
|
> Best practice AI & LLM discoverability for Nuxt sites. Score 100 on [`npx @vercel/agent-readability audit <URL>`](https://github.com/vercel-labs/agent-readability).
|
|
8
15
|
|
|
@@ -38,10 +45,7 @@ npx nuxi@latest module add nuxt-ai-ready
|
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
> [!TIP]
|
|
41
|
-
>
|
|
42
|
-
> ```bash
|
|
43
|
-
> npx skilld add nuxt-ai-ready
|
|
44
|
-
> ```
|
|
48
|
+
> Using an AI agent? Get the nuxt-ai-ready Skill on [skilld.dev/gh/harlan-zw/nuxt-ai-ready](https://skilld.dev/gh/harlan-zw/nuxt-ai-ready).
|
|
45
49
|
|
|
46
50
|
💡 Made your site AI-ready? Preview how a page converts to LLM-readable markdown with the free [HTML→Markdown tool](https://nuxtseo.com/tools/html-to-markdown), or track how AI engines index, rank and cite your site with [Nuxt SEO Pro](https://nuxtseo.com/pro).
|
|
47
51
|
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { realpath, readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { resolve, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
|
-
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.
|
|
5
|
+
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.DsokNmXc.mjs';
|
|
6
6
|
import 'node:module';
|
|
7
7
|
import '@nuxt/kit';
|
|
8
8
|
import 'defu';
|
|
@@ -5,7 +5,7 @@ import { colorize } from 'consola/utils';
|
|
|
5
5
|
import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
|
|
6
6
|
import { collectSitemap } from 'sitemapd/parse';
|
|
7
7
|
import { withLeadingSlash, joinURL, withBase } from 'ufo';
|
|
8
|
-
import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.
|
|
8
|
+
import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.DsokNmXc.mjs';
|
|
9
9
|
import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
10
10
|
import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
|
|
11
11
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import 'defu';
|
|
|
7
7
|
import 'nuxt-site-config/kit';
|
|
8
8
|
import 'nuxtseo-shared/kit';
|
|
9
9
|
import 'pkg-types';
|
|
10
|
-
export { m as default } from './shared/nuxt-ai-ready.
|
|
10
|
+
export { m as default } from './shared/nuxt-ai-ready.DsokNmXc.mjs';
|
|
11
11
|
import '../dist/runtime/markdown-path.js';
|
|
12
12
|
import '../dist/runtime/server/utils/sitemap-md.js';
|
|
13
13
|
import '../dist/runtime/server/utils/discovery-response.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { isPathFile } from "nuxt-site-config/urls";
|
|
2
1
|
import { defineNuxtPlugin, prerenderRoutes } from "nuxt/app";
|
|
3
|
-
import {
|
|
2
|
+
import { markdownAlternatePath } from "../../markdown-path.js";
|
|
4
3
|
export default defineNuxtPlugin({
|
|
5
4
|
setup(nuxtApp) {
|
|
6
5
|
if (!import.meta.prerender) {
|
|
@@ -8,10 +7,11 @@ export default defineNuxtPlugin({
|
|
|
8
7
|
}
|
|
9
8
|
nuxtApp.hooks.hook("app:rendered", (ctx) => {
|
|
10
9
|
const url = ctx.ssrContext?.url || "";
|
|
11
|
-
|
|
10
|
+
const markdownPath = markdownAlternatePath(url);
|
|
11
|
+
if (!markdownPath || ctx.ssrContext?.error || ctx.ssrContext?.noSSR) {
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
prerenderRoutes(
|
|
14
|
+
prerenderRoutes(markdownPath);
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
});
|
|
@@ -4,16 +4,12 @@ import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
|
4
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
5
5
|
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
6
6
|
import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
7
|
-
import {
|
|
7
|
+
import { hasMarkdownTwin, toMarkdownPath } from "./markdown-path.js";
|
|
8
8
|
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
9
9
|
import { queryPages } from "./server/db/queries.js";
|
|
10
10
|
import { logger } from "./server/logger.js";
|
|
11
11
|
import { fetchSitemapUrls } from "./server/utils/sitemap.js";
|
|
12
12
|
export { normalizeLlmsTxtConfig };
|
|
13
|
-
function hasRuntimeMarkdownHandler(pathname) {
|
|
14
|
-
const lastSegment = pathname.split("/").pop() || "";
|
|
15
|
-
return !isReservedPath(pathname) && !lastSegment.includes(".");
|
|
16
|
-
}
|
|
17
13
|
async function findMarkdownLinkAvailability() {
|
|
18
14
|
if (!import.meta.prerender) {
|
|
19
15
|
return {
|
|
@@ -205,7 +201,7 @@ Canonical Origin: ${canonicalSiteUrl}`);
|
|
|
205
201
|
resolvePageHref = (pathname) => {
|
|
206
202
|
const deployedPathname = resolvePath(pathname);
|
|
207
203
|
const markdownPath = resolvePath(toMarkdownPath(pathname));
|
|
208
|
-
if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable &&
|
|
204
|
+
if (markdownLinkAvailability.paths.has(decodePath(markdownPath)) || markdownLinkAvailability.runtimeMarkdownAvailable && hasMarkdownTwin(pathname)) {
|
|
209
205
|
return markdownPath;
|
|
210
206
|
}
|
|
211
207
|
return deployedPathname;
|
|
@@ -3,9 +3,13 @@ export declare function isReservedPath(path: string): boolean;
|
|
|
3
3
|
export declare function normalizePagePath(path: string): string;
|
|
4
4
|
export declare function toMarkdownPath(path: string): string;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* True when a path names a page with a `.md` twin: outside the reserved
|
|
7
|
+
* namespaces, and not a file such as `/feed.xml`. A dot alone does not make a
|
|
8
|
+
* file, so `/v1.2` is a page.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* a representation the markdown handler
|
|
10
|
+
* Prerendering and every advertisement read this one predicate, so a page
|
|
11
|
+
* cannot link a representation the build or the markdown handler lacks.
|
|
10
12
|
*/
|
|
13
|
+
export declare function hasMarkdownTwin(path: string): boolean;
|
|
14
|
+
/** The `.md` sibling a page may advertise, or null when it has none. */
|
|
11
15
|
export declare function markdownAlternatePath(path: string): string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const RE_TRAILING_SLASHES = /\/+$/;
|
|
2
|
+
const RE_FILE_EXTENSION = /\.[a-z][a-z\d]*$/i;
|
|
2
3
|
const RE_RESERVED_PATH = /^\/(?:api(?:\/|$)|_|@(?:id|fs|vite|react-refresh)(?:\/|$))/;
|
|
3
4
|
export function isReservedPath(path) {
|
|
4
5
|
return RE_RESERVED_PATH.test(path);
|
|
@@ -12,11 +13,10 @@ export function toMarkdownPath(path) {
|
|
|
12
13
|
return "/index.md";
|
|
13
14
|
return `${normalizedPath}.md`;
|
|
14
15
|
}
|
|
15
|
-
export function
|
|
16
|
-
if (isReservedPath(path))
|
|
17
|
-
return null;
|
|
16
|
+
export function hasMarkdownTwin(path) {
|
|
18
17
|
const lastSegment = normalizePagePath(path).split("/").pop() || "";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return !isReservedPath(path) && !RE_FILE_EXTENSION.test(lastSegment);
|
|
19
|
+
}
|
|
20
|
+
export function markdownAlternatePath(path) {
|
|
21
|
+
return hasMarkdownTwin(path) ? toMarkdownPath(path) : null;
|
|
22
22
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "uncrypto";
|
|
2
2
|
import { useEvent, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
|
+
import { checkAndHandleStale } from "../utils/checkStale.js";
|
|
3
4
|
import { parseSitemapCrawlState, serializeSitemapCrawlState } from "../utils/sitemap-crawl-state.js";
|
|
4
5
|
import { initSchema } from "./drizzle/queries.js";
|
|
5
6
|
import { useRawDb } from "./drizzle/raw.js";
|
|
@@ -17,6 +18,24 @@ function getEventFromContext(providedEvent) {
|
|
|
17
18
|
}
|
|
18
19
|
let devWarningShown = false;
|
|
19
20
|
let schemaInitializationState = { _tag: "Uninitialized" };
|
|
21
|
+
let dumpSyncState = { _tag: "Pending" };
|
|
22
|
+
function syncBuildDump(event) {
|
|
23
|
+
if (dumpSyncState._tag === "Synced")
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
if (dumpSyncState._tag === "Syncing")
|
|
26
|
+
return dumpSyncState.promise;
|
|
27
|
+
const promise = checkAndHandleStale(event).then(
|
|
28
|
+
() => {
|
|
29
|
+
dumpSyncState = { _tag: "Synced" };
|
|
30
|
+
},
|
|
31
|
+
(error) => {
|
|
32
|
+
dumpSyncState = { _tag: "Pending" };
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
dumpSyncState = { _tag: "Syncing", promise };
|
|
37
|
+
return promise;
|
|
38
|
+
}
|
|
20
39
|
const RE_FTS_CHARS = /[*:^"()]/g;
|
|
21
40
|
const RE_WHITESPACE = /\s+/;
|
|
22
41
|
async function getDb(event) {
|
|
@@ -32,7 +51,8 @@ async function getDb(event) {
|
|
|
32
51
|
}
|
|
33
52
|
const resolvedEvent = getEventFromContext(event);
|
|
34
53
|
const cfg = useRuntimeConfig(resolvedEvent);
|
|
35
|
-
|
|
54
|
+
const database = cfg["nuxt-ai-ready"]?.database?._tag;
|
|
55
|
+
if (database === "Disabled")
|
|
36
56
|
return null;
|
|
37
57
|
const db = await useRawDb(resolvedEvent);
|
|
38
58
|
if (schemaInitializationState._tag === "Uninitialized") {
|
|
@@ -46,6 +66,8 @@ async function getDb(event) {
|
|
|
46
66
|
}
|
|
47
67
|
if (schemaInitializationState._tag === "Initializing")
|
|
48
68
|
await schemaInitializationState.promise;
|
|
69
|
+
if (database === "Enabled")
|
|
70
|
+
await syncBuildDump(resolvedEvent);
|
|
49
71
|
return db;
|
|
50
72
|
}
|
|
51
73
|
async function getPrerenderDb() {
|
|
@@ -104,6 +104,7 @@ ${contentPage.markdown}`;
|
|
|
104
104
|
runtimeConfig.mdreamOptions,
|
|
105
105
|
{
|
|
106
106
|
extractUpdatedAt: true,
|
|
107
|
+
hooks: { route: path, event, isPrerender: true },
|
|
107
108
|
additionalFrontmatter: {
|
|
108
109
|
canonical_url: canonicalUrl,
|
|
109
110
|
...lastUpdated && { last_updated: lastUpdated }
|
|
@@ -10,9 +10,17 @@ import {
|
|
|
10
10
|
import { decompressFromBase64, importDbDump } from "../db/shared.js";
|
|
11
11
|
import { logger } from "../logger.js";
|
|
12
12
|
import { fetchPublicAsset } from "./cloudflare.js";
|
|
13
|
+
function parseBuildMeta(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return null;
|
|
16
|
+
const { buildId, pageCount, createdAt } = value;
|
|
17
|
+
if (typeof buildId !== "string" || !buildId || typeof pageCount !== "number")
|
|
18
|
+
return null;
|
|
19
|
+
return { buildId, pageCount, createdAt: typeof createdAt === "string" ? createdAt : "" };
|
|
20
|
+
}
|
|
13
21
|
async function fetchBuildMeta(event) {
|
|
14
22
|
logger.debug("[stale-check] Fetching meta...");
|
|
15
|
-
return fetchPublicAsset(event, "/__ai-ready/pages.meta.json");
|
|
23
|
+
return parseBuildMeta(await fetchPublicAsset(event, "/__ai-ready/pages.meta.json"));
|
|
16
24
|
}
|
|
17
25
|
async function fetchDump(event) {
|
|
18
26
|
logger.debug("[stale-check] Fetching dump...");
|
|
@@ -22,7 +30,10 @@ async function fetchDump(event) {
|
|
|
22
30
|
return null;
|
|
23
31
|
}
|
|
24
32
|
logger.debug(`[stale-check] Decompressing dump (${(dumpData.length / 1024).toFixed(1)}kb)...`);
|
|
25
|
-
return decompressFromBase64(dumpData)
|
|
33
|
+
return decompressFromBase64(dumpData).catch((error) => {
|
|
34
|
+
logger.warn("[stale-check] Build dump is unreadable, skipping restore:", error);
|
|
35
|
+
return null;
|
|
36
|
+
});
|
|
26
37
|
}
|
|
27
38
|
async function getStoredBuildId(event) {
|
|
28
39
|
return getInfoValue(event, "build_id");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withBase } from "ufo";
|
|
2
|
+
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
1
3
|
const FETCH_TIMEOUT = 5e3;
|
|
2
4
|
function unavailablePublicAsset(_error) {
|
|
3
5
|
return null;
|
|
@@ -29,7 +31,7 @@ export async function fetchPublicAsset(event, path, options) {
|
|
|
29
31
|
}
|
|
30
32
|
const controller = new AbortController();
|
|
31
33
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
32
|
-
return globalThis.$fetch(path, {
|
|
34
|
+
return globalThis.$fetch(withBase(path, useRuntimeConfig(event).app.baseURL), {
|
|
33
35
|
baseURL: "/",
|
|
34
36
|
signal: controller.signal,
|
|
35
37
|
responseType
|
|
@@ -12,4 +12,10 @@ interface FrontmatterFields {
|
|
|
12
12
|
}
|
|
13
13
|
export declare function buildFrontmatter(fields: FrontmatterFields): string;
|
|
14
14
|
export declare function layerFrontmatter(fields: FrontmatterFields, markdown: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Move mdream's `meta.description` to a root `description` field, the shape
|
|
17
|
+
* Content source pages and hand-built frontmatter already use. Other `meta`
|
|
18
|
+
* fields stay where mdream put them.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hoistMetaDescription(markdown: string): string;
|
|
15
21
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isMap, parseDocument } from "yaml";
|
|
1
|
+
import { isMap, isScalar, parseDocument } from "yaml";
|
|
2
2
|
function escapeYamlString(value) {
|
|
3
3
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
4
4
|
}
|
|
@@ -43,3 +43,25 @@ ${document.toString({ lineWidth: 0 }).trimEnd()}
|
|
|
43
43
|
|
|
44
44
|
${body}`;
|
|
45
45
|
}
|
|
46
|
+
export function hoistMetaDescription(markdown) {
|
|
47
|
+
const match = markdown.match(FRONTMATTER_RE);
|
|
48
|
+
if (!match)
|
|
49
|
+
return markdown;
|
|
50
|
+
const document = parseDocument(match[1]);
|
|
51
|
+
const root = document.contents;
|
|
52
|
+
const meta = document.get("meta");
|
|
53
|
+
if (document.errors.length || !isMap(root) || !isMap(meta) || !meta.has("description"))
|
|
54
|
+
return markdown;
|
|
55
|
+
const description = meta.get("description", true);
|
|
56
|
+
meta.delete("description");
|
|
57
|
+
if (!meta.items.length)
|
|
58
|
+
document.delete("meta");
|
|
59
|
+
if (!document.has("description")) {
|
|
60
|
+
const titleIndex = root.items.findIndex((pair) => isScalar(pair.key) && pair.key.value === "title");
|
|
61
|
+
root.items.splice(titleIndex + 1, 0, document.createPair("description", description));
|
|
62
|
+
}
|
|
63
|
+
return `---
|
|
64
|
+
${document.toString({ lineWidth: 0 }).trimEnd()}
|
|
65
|
+
---
|
|
66
|
+
${markdown.slice(match[0].length)}`;
|
|
67
|
+
}
|
|
@@ -19,7 +19,10 @@ export async function indexPage(route, html, options = {}, event) {
|
|
|
19
19
|
const isError = html.includes("__NUXT_ERROR__") || html.includes("nuxt-error-page");
|
|
20
20
|
const { siteUrl } = createUniversalContext(event);
|
|
21
21
|
const fullUrl = siteUrl ? `${siteUrl}${route}` : route;
|
|
22
|
-
const result = await convertHtmlToMarkdown(html, fullUrl, config.mdreamOptions, {
|
|
22
|
+
const result = await convertHtmlToMarkdown(html, fullUrl, config.mdreamOptions, {
|
|
23
|
+
extractUpdatedAt: true,
|
|
24
|
+
hooks: { route, event, isPrerender: false }
|
|
25
|
+
});
|
|
23
26
|
const headings = JSON.stringify(result.headings);
|
|
24
27
|
const keywords = extractKeywords(result.textContent, result.metaKeywords);
|
|
25
28
|
const contentHash = await computeContentHash(result.markdown);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { hasMarkdownTwin } from "../../markdown-path.js";
|
|
1
2
|
export function mapSitemapRoutes(urls) {
|
|
2
3
|
const routeToUrl = /* @__PURE__ */ new Map();
|
|
3
4
|
for (const url of urls) {
|
|
4
5
|
const route = url._path?.pathname ?? (url.loc.startsWith("/") ? url.loc.split("?")[0] ?? url.loc : new URL(url.loc).pathname);
|
|
5
|
-
if (
|
|
6
|
+
if (hasMarkdownTwin(route))
|
|
6
7
|
routeToUrl.set(route, url);
|
|
7
8
|
}
|
|
8
9
|
return routeToUrl;
|
|
@@ -4,15 +4,19 @@ export declare function extractLastUpdated(html: string): string | undefined;
|
|
|
4
4
|
interface ConvertHtmlOptions {
|
|
5
5
|
/** Extract updatedAt from meta tags */
|
|
6
6
|
extractUpdatedAt?: boolean;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Context for the Nitro hooks (ai-ready:mdreamConfig, ai-ready:page:markdown).
|
|
9
|
+
* Every conversion runs them, so prerender, runtime and indexing agree.
|
|
10
|
+
*/
|
|
11
|
+
hooks: {
|
|
9
12
|
route: string;
|
|
10
|
-
event
|
|
13
|
+
event?: H3Event;
|
|
14
|
+
isPrerender: boolean;
|
|
11
15
|
};
|
|
12
16
|
/** Extra fields to inject at the root of mdream's emitted YAML frontmatter */
|
|
13
17
|
additionalFrontmatter?: Record<string, string>;
|
|
14
18
|
}
|
|
15
|
-
export declare function convertHtmlToMarkdown(html: string, url: string, mdreamOptions: ModuleOptions['mdreamOptions'], opts
|
|
19
|
+
export declare function convertHtmlToMarkdown(html: string, url: string, mdreamOptions: ModuleOptions['mdreamOptions'], opts: ConvertHtmlOptions): Promise<{
|
|
16
20
|
updatedAt?: string | undefined;
|
|
17
21
|
markdown: string;
|
|
18
22
|
title: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { htmlToMarkdown } from "mdream";
|
|
2
2
|
import { useNitroApp } from "#nuxtseo/nitro";
|
|
3
|
+
import { hoistMetaDescription } from "./utils/frontmatter.js";
|
|
3
4
|
const RE_NBSP = /\u00A0/g;
|
|
4
5
|
function normalizeWhitespace(text) {
|
|
5
6
|
return text.replace(RE_NBSP, " ");
|
|
@@ -67,27 +68,22 @@ export function extractLastUpdated(html) {
|
|
|
67
68
|
}
|
|
68
69
|
return void 0;
|
|
69
70
|
}
|
|
70
|
-
export async function convertHtmlToMarkdown(html, url, mdreamOptions, opts
|
|
71
|
+
export async function convertHtmlToMarkdown(html, url, mdreamOptions, opts) {
|
|
71
72
|
const meta = { title: "", description: "", metaKeywords: "", headings: [], textContent: [] };
|
|
72
73
|
const options = buildMdreamOptions(url, mdreamOptions, meta, opts.extractUpdatedAt ?? false, opts.additionalFrontmatter ?? {});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await nitroApp.hooks.callHook("ai-ready:page:markdown", context);
|
|
87
|
-
markdown = context.markdown;
|
|
88
|
-
} else {
|
|
89
|
-
markdown = htmlToMarkdown(html, options);
|
|
90
|
-
}
|
|
74
|
+
const nitroApp = useNitroApp();
|
|
75
|
+
await nitroApp.hooks.callHook("ai-ready:mdreamConfig", options);
|
|
76
|
+
const context = {
|
|
77
|
+
html,
|
|
78
|
+
markdown: hoistMetaDescription(htmlToMarkdown(html, options)),
|
|
79
|
+
route: opts.hooks.route,
|
|
80
|
+
title: meta.title,
|
|
81
|
+
description: meta.description,
|
|
82
|
+
isPrerender: opts.hooks.isPrerender,
|
|
83
|
+
event: opts.hooks.event
|
|
84
|
+
};
|
|
85
|
+
await nitroApp.hooks.callHook("ai-ready:page:markdown", context);
|
|
86
|
+
const markdown = context.markdown;
|
|
91
87
|
return {
|
|
92
88
|
markdown: normalizeWhitespace(markdown),
|
|
93
89
|
title: normalizeWhitespace(meta.title),
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -406,8 +406,9 @@ export interface PageDocument {
|
|
|
406
406
|
/**
|
|
407
407
|
* Hook context for markdown processing (Nitro runtime hook)
|
|
408
408
|
*
|
|
409
|
-
* This hook is called
|
|
410
|
-
*
|
|
409
|
+
* This hook is called for every HTML→Markdown conversion: runtime `.md`
|
|
410
|
+
* requests, prerendering, and page indexing. Modify the markdown content
|
|
411
|
+
* before the module serves, writes, or stores it.
|
|
411
412
|
*
|
|
412
413
|
* @example Modify markdown content
|
|
413
414
|
* nitroApp.hooks.hook('ai-ready:page:markdown', async (context) => {
|
|
@@ -438,8 +439,11 @@ export interface MarkdownContext {
|
|
|
438
439
|
description: string;
|
|
439
440
|
/** Whether this is during prerendering (true) or runtime (false) */
|
|
440
441
|
isPrerender: boolean;
|
|
441
|
-
/**
|
|
442
|
-
|
|
442
|
+
/**
|
|
443
|
+
* The H3 event object for accessing request/response. Undefined when
|
|
444
|
+
* indexing runs outside a request, such as a scheduled task.
|
|
445
|
+
*/
|
|
446
|
+
event?: H3Event;
|
|
443
447
|
}
|
|
444
448
|
/**
|
|
445
449
|
* Markdown a site already holds for a route, supplied instead of converting the
|
|
@@ -2,13 +2,13 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { readFile, mkdir, writeFile, access } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { dirname, basename, join } from 'node:path';
|
|
5
|
-
import { hasNuxtModule, useNuxt, useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addServerImports,
|
|
5
|
+
import { hasNuxtModule, useNuxt, useLogger, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addServerImports, addServerHandler, extendRouteRules, addServerPlugin, addPlugin, addImports } from '@nuxt/kit';
|
|
6
6
|
import defu$1, { defu } from 'defu';
|
|
7
7
|
import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
|
|
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 {
|
|
11
|
+
import { hasMarkdownTwin, normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
12
12
|
import { SITEMAP_MD_ROUTE } from '../../dist/runtime/server/utils/sitemap-md.js';
|
|
13
13
|
import { publicCacheControl } from '../../dist/runtime/cache-control.js';
|
|
14
14
|
import { MCP_SERVER_CARD_MEDIA_TYPE, AI_CATALOG_MEDIA_TYPE } from '../../dist/runtime/server/utils/discovery-response.js';
|
|
@@ -912,11 +912,7 @@ const RE_MD_EXT = /\.md$/;
|
|
|
912
912
|
function isStaticMarkdownSourceRoute(route) {
|
|
913
913
|
if (route.includes("*") || route.includes(":"))
|
|
914
914
|
return false;
|
|
915
|
-
|
|
916
|
-
if (isReservedPath(path))
|
|
917
|
-
return false;
|
|
918
|
-
const lastSegment = path.split("/").pop() || "";
|
|
919
|
-
return !lastSegment.includes(".");
|
|
915
|
+
return hasMarkdownTwin(route.split("?")[0] || route);
|
|
920
916
|
}
|
|
921
917
|
function staticDescribedbyEntry(baseURL) {
|
|
922
918
|
return `<${encodeURI(toDeployedRoute("/llms.txt", baseURL))}>; rel="describedby"`;
|
|
@@ -1467,8 +1463,6 @@ ${details}`);
|
|
|
1467
1463
|
updateSeoProFeatures(seoProModules);
|
|
1468
1464
|
return;
|
|
1469
1465
|
}
|
|
1470
|
-
if (databaseEnabled)
|
|
1471
|
-
addServerPlugin(resolve("./runtime/server/plugins/mcp-data"));
|
|
1472
1466
|
const mcpLink = {
|
|
1473
1467
|
title: "MCP",
|
|
1474
1468
|
href: withSiteUrl(finalMcpToolkitState.route, { withBase: true }),
|
|
@@ -1610,7 +1604,7 @@ ${details}`);
|
|
|
1610
1604
|
if (!runtimeSyncSecret && runtimeSyncEnabled) {
|
|
1611
1605
|
runtimeSyncSecret = randomBytes(32).toString("hex");
|
|
1612
1606
|
if (!nuxt.options.dev && !nuxt.options._prepare)
|
|
1613
|
-
logger.info(
|
|
1607
|
+
logger.info("Generated a new runtimeSyncSecret for this build. If an external scheduler calls /__ai-ready/*, set NUXT_AI_READY_RUNTIME_SYNC_SECRET; the generated secret changes on every build.");
|
|
1614
1608
|
}
|
|
1615
1609
|
if (runtimeSyncSecret) {
|
|
1616
1610
|
const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt/ai-ready");
|
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.5.0",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
44
44
|
"mcp.d.ts",
|
|
45
|
+
"skills",
|
|
45
46
|
"webmcp.d.ts"
|
|
46
47
|
],
|
|
47
48
|
"peerDependencies": {
|
|
48
49
|
"@libsql/client": "^0.18.0",
|
|
49
50
|
"@neondatabase/serverless": "^1.0.0",
|
|
50
|
-
"@nuxtjs/sitemap": ">=8.3.1",
|
|
51
51
|
"better-sqlite3": "^11.0.0 || ^12.0.0 || ^13.0.0",
|
|
52
52
|
"postgres": "^3.4.9"
|
|
53
53
|
},
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@mdream/js": "^1.7.2",
|
|
70
70
|
"@nuxt/kit": "^4.5.2",
|
|
71
|
+
"@nuxtjs/robots": "^6.2.3",
|
|
72
|
+
"@nuxtjs/sitemap": "^8.5.1",
|
|
71
73
|
"citty": "^0.2.2",
|
|
72
74
|
"consola": "^3.4.2",
|
|
73
75
|
"defu": "^6.1.7",
|
|
@@ -93,8 +95,6 @@
|
|
|
93
95
|
"@nuxt/test-utils": "^4.3.2",
|
|
94
96
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
95
97
|
"@nuxtjs/mcp-toolkit": "^0.21.0",
|
|
96
|
-
"@nuxtjs/robots": "^6.2.3",
|
|
97
|
-
"@nuxtjs/sitemap": "^8.5.1",
|
|
98
98
|
"@types/better-sqlite3": "^9.6.0",
|
|
99
99
|
"@vitest/coverage-v8": "^5.0.1",
|
|
100
100
|
"@vue/test-utils": "^2.5.1",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nuxt-ai-ready
|
|
3
|
+
description: Add, configure, or debug AI and LLM discoverability in a Nuxt site with the nuxt-ai-ready module. Use when a task mentions llms.txt, llms-full.txt, serving pages as Markdown (.md routes, Accept text/markdown), an MCP server for site content, WebMCP or useWebMcpTool, Content Signals in robots.txt, agent skills discovery, runtime page indexing, queryPages or searchPages, or the aiReady config key. Gives the automatic outputs, the build versus runtime split, and the traps that leave output empty or stale.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# nuxt-ai-ready
|
|
7
|
+
|
|
8
|
+
Tested against `nuxt-ai-ready` 2.4.1 plus its unreleased fixes on `main`, on Nuxt 4.5 and Node 24 (requires Nuxt `>=4.0.0`).
|
|
9
|
+
The module converts rendered pages to Markdown with mdream and publishes `llms.txt`, `llms-full.txt`, and `.md` twins.
|
|
10
|
+
Optional parts add MCP tools, WebMCP tools, and a runtime page index. Config key: `aiReady`. Docs: https://nuxtseo.com/ai-ready
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
The package installs `@nuxtjs/robots` and `@nuxtjs/sitemap` and loads them as module dependencies. Configure them with the `robots` and `sitemap` keys.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add nuxt-ai-ready
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Set `site.url`. Canonical URLs, `Link` headers, and `/.well-known/ai-catalog.json` use it.
|
|
21
|
+
Prerender the pages you want in `llms-full.txt` and in the page index (`nuxi generate`, or `nitro.prerender.routes`).
|
|
22
|
+
A page visit in dev or production does not index anything.
|
|
23
|
+
|
|
24
|
+
## Automatic behaviour
|
|
25
|
+
|
|
26
|
+
With no `aiReady` config, the module adds:
|
|
27
|
+
|
|
28
|
+
- `/llms.txt`: site name, description, resource links, and a `## Pages` list. In dev it lists sitemap routes without titles.
|
|
29
|
+
- `/llms-full.txt`: the full Markdown of each prerendered page. Only prerendered pages go in the static file.
|
|
30
|
+
- A `.md` twin per page. `/about` becomes `/about.md`, and `/` becomes `/index.md`. Paths under `/api` and `/_` get none.
|
|
31
|
+
- Accept negotiation: a request that prefers `text/markdown`, or a known AI bot user agent, gets a `307` to the `.md` URL.
|
|
32
|
+
- `<link rel="alternate" type="text/markdown">` and `<link rel="describedby" href="/llms.txt">` in HTML, plus matching `Link` headers.
|
|
33
|
+
- `/sitemap.md`, and a `## Sitemap` footer on every generated `.md` page. Set `sitemapMd: false` to remove both.
|
|
34
|
+
- Nuxt Content v3 `page` collections: the `.md` route serves the source Markdown, not converted HTML. Set `contentSource: false` to convert HTML.
|
|
35
|
+
- Agent skills: every `skills/<name>/SKILL.md` in the project root or a local layer is published at `/.well-known/agent-skills/`, at `/skills/<name>/SKILL.md`, and in `llms.txt`. With one skill it is also served at `/SKILL.md`. Set `agentSkills: false` to publish nothing.
|
|
36
|
+
|
|
37
|
+
Off by default: Content Signals, MCP, WebMCP, runtime sync, cron, and the database.
|
|
38
|
+
|
|
39
|
+
## Common tasks
|
|
40
|
+
|
|
41
|
+
Add llms.txt sections. `notes` renders before the first heading:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export default defineNuxtConfig({
|
|
45
|
+
aiReady: {
|
|
46
|
+
llmsTxt: {
|
|
47
|
+
sections: [{ title: 'API Reference', links: [{ title: 'REST API', href: '/docs/api', description: 'API docs' }] }],
|
|
48
|
+
notes: 'Built with Nuxt AI Ready',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Remove an element from the Markdown on every path, build and runtime. Put it in `mdreamOptions`; the defaults `minimal` and `clean` stay on:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export default defineNuxtConfig({
|
|
58
|
+
aiReady: { mdreamOptions: { filter: { exclude: ['.author-bio'] } } },
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Change the Markdown text. The Nitro hook covers runtime `.md` responses, prerendered `.md` files, `llms-full.txt`, and indexed pages. `ctx.isPrerender` is `true` only during prerendering:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// server/plugins/markdown.ts
|
|
66
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
67
|
+
nitroApp.hooks.hook('ai-ready:page:markdown', (ctx) => {
|
|
68
|
+
ctx.markdown += '\n\nSee /support.'
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Allow Content Signals. An omitted permission renders as `no`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export default defineNuxtConfig({ aiReady: { contentSignal: { search: true } } })
|
|
77
|
+
// robots.txt: Content-Signal: ai-train=no, search=yes, ai-input=no
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
MCP: install `@nuxtjs/mcp-toolkit` and add it to `modules`. The module then registers `list_pages`, `search_pages`, and `get_page_markdown` at `/mcp`. MCP needs a running server; `nuxi generate` output cannot host it.
|
|
81
|
+
|
|
82
|
+
WebMCP: set `webmcp: true`. This registers the same three tools in the browser and auto-imports `useWebMcpTool()` and `useWebMcpSupported()`. Without `webmcp`, those composables are not defined.
|
|
83
|
+
|
|
84
|
+
Runtime indexing, database drivers, Cloudflare D1, and the server query helpers: see [references/runtime-indexing.md](references/runtime-indexing.md).
|
|
85
|
+
|
|
86
|
+
## Traps
|
|
87
|
+
|
|
88
|
+
- **The same edit in both hooks applies twice.** During prerendering the Nitro `ai-ready:page:markdown` hook runs, then the Nuxt hook of the same name. Put an edit in one of them, not both.
|
|
89
|
+
- **Content source pages skip conversion hooks.** A Nuxt Content page ignores `mdreamOptions` and both Nitro hooks.
|
|
90
|
+
- **Dev has no page data.** Query helpers, MCP tools, and search return empty results in `nuxi dev`. Test with `nuxi build` and `node .output/server/index.mjs`.
|
|
91
|
+
- **Response caching turns negotiation off.** An ISR, `swr`, or `cache` route rule without `varies: ['accept', 'sec-fetch-dest', 'user-agent']` serves HTML to Markdown clients. The `.md` URL still works. For a CDN that caches by URL only, set `contentNegotiation: false`.
|
|
92
|
+
- **Emptying `ctx.markdown` does not hide a page.** It drops the body from `llms-full.txt`. The URL stays in `llms.txt` and in the index. Use `sitemap.exclude` for discovery, and access control for private pages.
|
|
93
|
+
- **A project `skills/` folder is public.** Agent skills discovery publishes it with no opt in. Rename the folder with `agentSkills.dir`, or set `agentSkills: false`.
|
|
94
|
+
- **The same hook name exists twice.** `ai-ready:page:markdown` is a Nuxt hook (`nuxt.config` `hooks`, prerender only) and a Nitro hook (`nitroApp.hooks`, every HTML conversion). They receive different context. The Nitro `ctx.event` is undefined when indexing runs outside a request.
|
|
95
|
+
- **`database: false` conflicts with `runtimeSync` and `cron`.** The build fails and names the option.
|
|
96
|
+
|
|
97
|
+
## Version limits
|
|
98
|
+
|
|
99
|
+
- v2 removed IndexNow. The `indexNow` option and `/__ai-ready/indexnow` no longer exist. Submit URLs from the `ai-ready:page:indexed` Nitro hook: https://nuxtseo.com/docs/ai-ready/advanced/indexnow
|
|
100
|
+
- v1 renamed these. Do not write the old form:
|
|
101
|
+
|
|
102
|
+
| Old | New |
|
|
103
|
+
|-----|-----|
|
|
104
|
+
| `mdreamOptions: { preset: 'minimal' }` | `mdreamOptions: { minimal: true }` |
|
|
105
|
+
| `cacheMaxAgeSeconds` | `llmsTxtCacheSeconds` |
|
|
106
|
+
| Nitro hook `ai-ready:markdown` | `ai-ready:page:markdown` |
|
|
107
|
+
| `BulkDocument` type | `PageDocument` |
|
|
108
|
+
| `JSON.parse(page.headings)` | `page.headings` (already an array) |
|
|
109
|
+
| `?secret=TOKEN` on `/__ai-ready/*` | `Authorization: Bearer TOKEN` header |
|
|
110
|
+
|
|
111
|
+
## Config
|
|
112
|
+
|
|
113
|
+
- `llmsTxt.markdownLinks` (`false`): link page entries in `llms.txt` to their `.md` twins.
|
|
114
|
+
- `describedby` (`true`): the `describedby` link tag and header.
|
|
115
|
+
- `tools.<listPages|searchPages|getPageMarkdown>.<mcp|webmcp>.enabled`: attach a built in tool to one transport only.
|
|
116
|
+
- `autoI18n` (`true`): with `@nuxtjs/i18n`, adds hreflang `Link` headers and a languages section in `llms.txt`.
|
|
117
|
+
- Other options: https://nuxtseo.com/docs/ai-ready/api/config
|
|
118
|
+
|
|
119
|
+
## Debug
|
|
120
|
+
|
|
121
|
+
- `curl -H 'Accept: text/markdown' -D - -o /dev/null <url>` must return `307` with `Location: <path>.md`. A `200` means a cache rule turned negotiation off.
|
|
122
|
+
- `/__ai-ready__/debug.json` exists in dev, or with `debug: true`.
|
|
123
|
+
- Nuxt DevTools has an AI Ready tab.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Runtime indexing and the page database
|
|
2
|
+
|
|
3
|
+
Read this when pages change between deployments, or when server code queries stored pages.
|
|
4
|
+
If each deployment prerenders all content, you do not need runtime sync.
|
|
5
|
+
|
|
6
|
+
## When the database is on
|
|
7
|
+
|
|
8
|
+
The database stays off until a feature needs it: `runtimeSync`, `cron`, MCP page tools, WebMCP page tools, or an explicit `database` config.
|
|
9
|
+
Prerendering always writes a temporary SQLite index and a dump at `/__ai-ready/pages.dump`.
|
|
10
|
+
|
|
11
|
+
Driver selection:
|
|
12
|
+
|
|
13
|
+
| Platform | Driver | Install |
|
|
14
|
+
|----------|--------|---------|
|
|
15
|
+
| Node 22.13 and later | `node:sqlite` | nothing |
|
|
16
|
+
| Node before 22.13 | `better-sqlite3` | `better-sqlite3`, or the build fails |
|
|
17
|
+
| Bun | `bun:sqlite` | nothing |
|
|
18
|
+
| Cloudflare preset | D1 | a binding |
|
|
19
|
+
| Vercel with `POSTGRES_URL` | Neon | nothing |
|
|
20
|
+
| `database: { type: 'postgres' }` | PostgreSQL | `postgres` |
|
|
21
|
+
|
|
22
|
+
The build machine always uses SQLite, also for a D1 deployment.
|
|
23
|
+
For D1, set `database: { type: 'd1', bindingName: 'AI_READY_DB' }` and create the binding yourself. The default binding name is `DB`.
|
|
24
|
+
|
|
25
|
+
## Runtime sync
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export default defineNuxtConfig({
|
|
29
|
+
aiReady: {
|
|
30
|
+
runtimeSync: { ttl: 3600, batchSize: 50, pruneTtl: 0 },
|
|
31
|
+
runtimeSyncSecret: process.env.NUXT_AI_READY_RUNTIME_SYNC_SECRET,
|
|
32
|
+
cron: true, // every 5 minutes, also enables runtimeSync
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Set `runtimeSyncSecret`. Without it, each build generates a new random secret, and external schedulers lose access after the next deployment.
|
|
38
|
+
|
|
39
|
+
Every control endpoint needs `Authorization: Bearer <secret>`, including `GET /__ai-ready/status`. Only `POST /__ai-ready/prune?dry=true` works without it.
|
|
40
|
+
|
|
41
|
+
| Request | Effect |
|
|
42
|
+
|---------|--------|
|
|
43
|
+
| `POST /__ai-ready/poll` | Index at most 50 pending pages. Repeat while `remaining > 0`. |
|
|
44
|
+
| `POST /__ai-ready/reindex?route=/about` | Index one route now. The route does not need to be in the sitemap. |
|
|
45
|
+
| `POST /__ai-ready/restore` | Replace stored pages with the build dump. |
|
|
46
|
+
| `POST /__ai-ready/prune?ttl=604800` | Delete routes missing from the sitemap for that long. |
|
|
47
|
+
| `GET /__ai-ready/cron` | Production only. For hosts without native cron. |
|
|
48
|
+
|
|
49
|
+
Traps:
|
|
50
|
+
|
|
51
|
+
- **TTL expiry does not reindex a page.** Poll and cron process only pending rows. After a CMS edit, call `reindex` for that route.
|
|
52
|
+
- **Only prerendered pages are in the dump.** Each server process loads the dump before its first database query. A page that was not prerendered stays out of the index until poll, cron, or `reindex` adds it.
|
|
53
|
+
- **Cloudflare Pages has no cron triggers.** Keep `cron: true` and call `GET /__ai-ready/cron` from an external scheduler. On Workers the module adds the trigger.
|
|
54
|
+
|
|
55
|
+
## Server query helpers
|
|
56
|
+
|
|
57
|
+
`queryPages`, `searchPages`, `countPages`, `streamPages`, `indexPage`, and `indexPageByRoute` are auto-imported in Nitro code. Other exports need `import { ... } from '#ai-ready'`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// server/api/search.get.ts
|
|
61
|
+
export default defineEventHandler(async (event) => {
|
|
62
|
+
const q = getQuery(event).q
|
|
63
|
+
if (typeof q !== 'string' || !q.trim())
|
|
64
|
+
return []
|
|
65
|
+
return searchPages(event, q, { limit: 10 })
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `searchPages` returns `[]` in dev and during prerender. On SQLite and D1, `score` is BM25 and lower ranks first. PostgreSQL returns `score: 0`.
|
|
70
|
+
- `queryPages(event, { route })` returns one page or `undefined`. Without `route` it returns an array. Add `includeMarkdown: true` for the body.
|
|
71
|
+
- Raw SQL: `useRawDb` from `#ai-ready`. `useDatabase` does not exist.
|
|
72
|
+
- The event goes in a different position: `indexPage(route, html, options, event)` and `indexPageByRoute(route, event, options)`.
|
|
73
|
+
- `indexPage` throws on failure. `indexPageByRoute` returns `{ success: false, error }`.
|
|
74
|
+
|
|
75
|
+
React to a changed page with the `ai-ready:page:indexed` Nitro hook. Check `ctx.contentChanged`; a forced reindex of the same content still fires the hook.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { H3Event } from '#nuxtseo/h3';
|
|
2
|
-
interface McpDataNitroApp {
|
|
3
|
-
hooks: {
|
|
4
|
-
hook: (name: 'mcp:config:resolved', handler: (payload: {
|
|
5
|
-
event: H3Event;
|
|
6
|
-
}) => Promise<void>) => unknown;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export default function mcpDataPlugin(nitroApp: McpDataNitroApp): void;
|
|
10
|
-
export {};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { checkAndHandleStale } from "../utils/checkStale.js";
|
|
2
|
-
export default function mcpDataPlugin(nitroApp) {
|
|
3
|
-
let state = { _tag: "Idle" };
|
|
4
|
-
const ensureData = (event) => {
|
|
5
|
-
if (state._tag === "Ready")
|
|
6
|
-
return Promise.resolve();
|
|
7
|
-
if (state._tag === "Running")
|
|
8
|
-
return state.promise;
|
|
9
|
-
const promise = checkAndHandleStale(event).then(
|
|
10
|
-
() => {
|
|
11
|
-
state = { _tag: "Ready" };
|
|
12
|
-
},
|
|
13
|
-
(error) => {
|
|
14
|
-
state = { _tag: "Idle" };
|
|
15
|
-
return Promise.reject(error);
|
|
16
|
-
}
|
|
17
|
-
);
|
|
18
|
-
state = { _tag: "Running", promise };
|
|
19
|
-
return promise;
|
|
20
|
-
};
|
|
21
|
-
nitroApp.hooks.hook("mcp:config:resolved", ({ event }) => ensureData(event));
|
|
22
|
-
}
|