nuxt-ai-ready 1.5.5 → 1.5.6
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/module.json +1 -1
- package/dist/module.mjs +11 -9
- package/dist/runtime/llms-txt-utils.js +42 -20
- package/dist/runtime/route-path.d.ts +10 -0
- package/dist/runtime/route-path.js +16 -0
- package/dist/runtime/server/db/queries.js +9 -9
- package/dist/runtime/server/middleware/markdown.js +16 -12
- package/dist/runtime/server/middleware/markdown.prerender.js +6 -3
- package/dist/runtime/server/routes/llms-full.txt.get.js +7 -2
- package/dist/runtime/server/utils/indexnow-shared.js +3 -2
- package/dist/runtime/server/utils/link-header.js +4 -2
- package/dist/runtime/server/utils/llms-full.js +2 -2
- package/package.json +8 -8
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -9,8 +9,9 @@ import { resolveNuxtContentVersion } from 'nuxtseo-shared/kit';
|
|
|
9
9
|
import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
|
10
10
|
import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
|
|
11
11
|
import { colorize } from 'consola/utils';
|
|
12
|
-
import { withBase } from 'ufo';
|
|
12
|
+
import { withLeadingSlash, withBase } from 'ufo';
|
|
13
13
|
import { normalizePagePath, toMarkdownPath } from '../dist/runtime/markdown-path.js';
|
|
14
|
+
import { toLogicalRoute, toDeployedRoute } from '../dist/runtime/route-path.js';
|
|
14
15
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../dist/runtime/server/db/shared.js';
|
|
15
16
|
import { comparePageHashes, submitToIndexNowShared } from '../dist/runtime/server/utils/indexnow-shared.js';
|
|
16
17
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../dist/runtime/server/utils/llms-full.js';
|
|
@@ -25,7 +26,7 @@ const RE_HTML_MD_EXT = /\.(html|md)$/;
|
|
|
25
26
|
const RE_INDEX_SUFFIX = /\/index$/;
|
|
26
27
|
const RE_MD_EXT = /\.md$/;
|
|
27
28
|
async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
28
|
-
const metaUrl =
|
|
29
|
+
const metaUrl = withBase("/__ai-ready/pages.meta.json", siteUrl);
|
|
29
30
|
logger.info(`Fetching previous build meta from ${metaUrl}`);
|
|
30
31
|
const controller = new AbortController();
|
|
31
32
|
const timeoutId = setTimeout(() => controller.abort(), BUILD_FETCH_TIMEOUT);
|
|
@@ -41,7 +42,7 @@ async function fetchPreviousMeta(siteUrl, indexNow) {
|
|
|
41
42
|
}
|
|
42
43
|
logger.info(`Previous build: ${prevMeta.pageCount} pages (buildId: ${prevMeta.buildId})`);
|
|
43
44
|
if (indexNow) {
|
|
44
|
-
const keyUrl =
|
|
45
|
+
const keyUrl = withBase(`/${indexNow}.txt`, siteUrl);
|
|
45
46
|
const keyLive = await fetch(keyUrl, { signal: AbortSignal.timeout(5e3) }).then((r) => r.ok).catch(() => false);
|
|
46
47
|
if (!keyLive) {
|
|
47
48
|
logger.info("IndexNow key file not live yet - IndexNow submission will be skipped");
|
|
@@ -161,7 +162,7 @@ async function processMarkdownRoute(state, nuxt, route, parsed, lastmod, options
|
|
|
161
162
|
async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
162
163
|
const loc = typeof entry === "string" ? entry : entry.loc;
|
|
163
164
|
const lastmod = typeof entry === "string" ? void 0 : entry.lastmod;
|
|
164
|
-
const route =
|
|
165
|
+
const route = toLogicalRoute(loc, nitro.options.baseURL);
|
|
165
166
|
if (route.split("/").some((segment) => segment.startsWith("_"))) {
|
|
166
167
|
return { crawled: false, skipped: true };
|
|
167
168
|
}
|
|
@@ -169,7 +170,7 @@ async function processSitemapEntry(state, nuxt, nitro, entry) {
|
|
|
169
170
|
return { crawled: false, skipped: true };
|
|
170
171
|
}
|
|
171
172
|
const mdRoute = toMarkdownPath(route);
|
|
172
|
-
const mdUrl =
|
|
173
|
+
const mdUrl = toDeployedRoute(mdRoute, nitro.options.baseURL);
|
|
173
174
|
logger.debug(`Fetching markdown for ${route} \u2192 ${mdUrl}`);
|
|
174
175
|
const res = await globalThis.$fetch(mdUrl, {
|
|
175
176
|
headers: { "x-nitro-prerender": mdRoute },
|
|
@@ -292,7 +293,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
|
|
|
292
293
|
let initPromise = null;
|
|
293
294
|
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
294
295
|
if (route.error) {
|
|
295
|
-
const
|
|
296
|
+
const routePath = route.fileName || route.route;
|
|
297
|
+
const pageRoute2 = withLeadingSlash(routePath).replace(RE_HTML_MD_EXT, "").replace(RE_INDEX_SUFFIX, "").replace(RE_HTML_MD_EXT, "") || "/";
|
|
296
298
|
state.errorRoutes.add(pageRoute2);
|
|
297
299
|
logger.debug(`Detected error page: ${pageRoute2}`);
|
|
298
300
|
return;
|
|
@@ -727,7 +729,7 @@ const module$1 = defineNuxtModule({
|
|
|
727
729
|
}
|
|
728
730
|
registerTypeTemplates();
|
|
729
731
|
const defaultLlmsTxtSections = [];
|
|
730
|
-
const llmsFullRoute = withSiteUrl("llms-full.txt");
|
|
732
|
+
const llmsFullRoute = withSiteUrl("llms-full.txt", { withBase: true });
|
|
731
733
|
defaultLlmsTxtSections.push({
|
|
732
734
|
title: "LLM Resources",
|
|
733
735
|
links: [
|
|
@@ -750,7 +752,7 @@ const module$1 = defineNuxtModule({
|
|
|
750
752
|
});
|
|
751
753
|
const mcpLink = {
|
|
752
754
|
title: "MCP",
|
|
753
|
-
href: withSiteUrl(nuxt.options.mcp !== false && nuxt.options.mcp?.route || "/mcp"),
|
|
755
|
+
href: withSiteUrl(nuxt.options.mcp !== false && nuxt.options.mcp?.route || "/mcp", { withBase: true }),
|
|
754
756
|
description: "Model Context Protocol server endpoint for AI agent integration."
|
|
755
757
|
};
|
|
756
758
|
if (defaultLlmsTxtSections[0]) {
|
|
@@ -1038,7 +1040,7 @@ export async function lookupContentPage(event, path) {
|
|
|
1038
1040
|
const siteConfig = useSiteConfig();
|
|
1039
1041
|
setupPrerenderHandler(config, buildDbPath, {
|
|
1040
1042
|
name: siteConfig.name,
|
|
1041
|
-
url: siteConfig.url,
|
|
1043
|
+
url: siteConfig.url ? withSiteUrl("/", { withBase: true }) : void 0,
|
|
1042
1044
|
description: siteConfig.description
|
|
1043
1045
|
}, mergedLlmsTxt, indexNow, { ftsTokenizer, i18n: i18nConfig });
|
|
1044
1046
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
2
2
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
3
|
+
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
3
4
|
import { normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
5
|
+
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
4
6
|
import { queryPages } from "./server/db/queries.js";
|
|
5
7
|
import { logger } from "./server/logger.js";
|
|
6
8
|
import { localePath, resolveLocaleFromRoute } from "./server/utils/i18n.js";
|
|
7
9
|
import { fetchSitemapUrls } from "./server/utils/sitemap.js";
|
|
8
10
|
export { normalizeLlmsTxtConfig };
|
|
9
|
-
function formatAvailableLanguagesSection(i18n, pageCounts) {
|
|
11
|
+
function formatAvailableLanguagesSection(i18n, pageCounts, resolvePath) {
|
|
10
12
|
const lines = ["## Available Languages on Website", ""];
|
|
11
13
|
for (const locale of i18n.locales) {
|
|
12
14
|
const isDefault = locale.code === i18n.defaultLocale;
|
|
13
|
-
const prefix = localePath("/", locale.code, i18n);
|
|
15
|
+
const prefix = resolvePath(localePath("/", locale.code, i18n));
|
|
14
16
|
const count = pageCounts.get(locale.code) ?? 0;
|
|
15
17
|
const display = locale.nativeName ? `${locale.nativeName} (${locale.code})` : locale.name ? `${locale.name} (${locale.code})` : locale.code;
|
|
16
18
|
const suffix = isDefault ? "Content included below" : "Visit website for content";
|
|
@@ -102,10 +104,11 @@ function formatPagesWithGroups(pages) {
|
|
|
102
104
|
}
|
|
103
105
|
urlsInCurrentGroup++;
|
|
104
106
|
const descText = page.description ? `: ${page.description.substring(0, 160)}${page.description.length > 160 ? "..." : ""}` : "";
|
|
107
|
+
const href = page.href ?? page.pathname;
|
|
105
108
|
if (page.title && page.title !== page.pathname)
|
|
106
|
-
lines.push(`- [${page.title}](${
|
|
109
|
+
lines.push(`- [${page.title}](${href})${descText}`);
|
|
107
110
|
else
|
|
108
|
-
lines.push(`- ${
|
|
111
|
+
lines.push(`- ${href}${descText}`);
|
|
109
112
|
}
|
|
110
113
|
return lines;
|
|
111
114
|
}
|
|
@@ -116,26 +119,39 @@ export async function buildLlmsTxt(event) {
|
|
|
116
119
|
const siteConfig = getSiteConfig(event);
|
|
117
120
|
const llmsTxtConfig = aiReadyConfig.llmsTxt;
|
|
118
121
|
const i18n = aiReadyConfig.i18n;
|
|
122
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
123
|
+
const resolvePath = (path) => withSiteTrailingSlash(event, toDeployedRoute(path, baseURL));
|
|
124
|
+
const resolveUrl = (path) => withSiteUrl(event, toDeployedRoute(path, baseURL));
|
|
125
|
+
const canonicalSiteUrl = siteConfig.url ? resolveUrl("/") : void 0;
|
|
119
126
|
const parts = [];
|
|
120
|
-
parts.push(`# ${siteConfig.name ||
|
|
127
|
+
parts.push(`# ${siteConfig.name || canonicalSiteUrl}`);
|
|
121
128
|
if (siteConfig.description) {
|
|
122
129
|
parts.push(`
|
|
123
130
|
> ${siteConfig.description}`);
|
|
124
131
|
}
|
|
125
|
-
if (
|
|
132
|
+
if (canonicalSiteUrl) {
|
|
126
133
|
parts.push(`
|
|
127
|
-
Canonical Origin: ${
|
|
134
|
+
Canonical Origin: ${canonicalSiteUrl}`);
|
|
128
135
|
}
|
|
129
136
|
parts.push("");
|
|
130
|
-
const sections = llmsTxtConfig.sections
|
|
137
|
+
const sections = llmsTxtConfig.sections?.map((section) => ({
|
|
138
|
+
...section,
|
|
139
|
+
links: section.links ? [...section.links] : void 0
|
|
140
|
+
})) ?? [];
|
|
131
141
|
if (sections[0]?.links) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
const sitemapRoutes = sitemapConfig?.sitemaps ? Object.values(sitemapConfig.sitemaps).map((s) => s.sitemapName) : ["sitemap.xml"];
|
|
143
|
+
for (const name of sitemapRoutes) {
|
|
144
|
+
sections[0].links.push({
|
|
145
|
+
title: name,
|
|
146
|
+
href: resolveUrl(`/${name}`),
|
|
147
|
+
description: "XML sitemap for search engines and crawlers."
|
|
148
|
+
});
|
|
137
149
|
}
|
|
138
|
-
sections[0].links.push({
|
|
150
|
+
sections[0].links.push({
|
|
151
|
+
title: "robots.txt",
|
|
152
|
+
href: resolveUrl("/robots.txt"),
|
|
153
|
+
description: "Crawler rules and permissions."
|
|
154
|
+
});
|
|
139
155
|
}
|
|
140
156
|
const normalizedContent = normalizeLlmsTxtConfig({ ...llmsTxtConfig, sections });
|
|
141
157
|
if (normalizedContent) {
|
|
@@ -157,23 +173,29 @@ Canonical Origin: ${siteConfig.url}`);
|
|
|
157
173
|
);
|
|
158
174
|
}
|
|
159
175
|
const urls = await urlsPromise;
|
|
176
|
+
const sitemapPaths = urls.map((url) => toLogicalRoute(url.loc, baseURL));
|
|
177
|
+
const sitemapPathSet = new Set(sitemapPaths);
|
|
160
178
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
161
|
-
const errorSet = new Set(errorPages.map((p) => p.route));
|
|
179
|
+
const errorSet = new Set(errorPages.map((p) => normalizePersistedRoute(p.route, sitemapPathSet, baseURL)));
|
|
162
180
|
const prerendered = [];
|
|
163
181
|
for (const page of pages) {
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
const pathname = normalizePersistedRoute(page.route, sitemapPathSet, baseURL);
|
|
183
|
+
if (seenPaths.has(pathname))
|
|
184
|
+
continue;
|
|
185
|
+
seenPaths.add(pathname);
|
|
186
|
+
prerendered.push({ pathname, title: page.title, description: page.description, locale: page.locale });
|
|
166
187
|
}
|
|
167
188
|
const devModeHint = import.meta.dev && prerendered.length === 0 ? " (dev mode - run `nuxi generate` for page titles)" : "";
|
|
168
189
|
const other = [];
|
|
169
|
-
for (const
|
|
170
|
-
const pathname = url.loc.startsWith("/") ? url.loc : new URL(url.loc).pathname;
|
|
190
|
+
for (const pathname of sitemapPaths) {
|
|
171
191
|
if (!seenPaths.has(pathname) && !errorSet.has(pathname)) {
|
|
172
192
|
const locale = i18n ? resolveLocaleFromRoute(pathname, i18n).locale : void 0;
|
|
173
193
|
other.push({ pathname, locale });
|
|
174
194
|
seenPaths.add(pathname);
|
|
175
195
|
}
|
|
176
196
|
}
|
|
197
|
+
for (const page of [...prerendered, ...other])
|
|
198
|
+
page.href = resolvePath(page.pathname);
|
|
177
199
|
if (i18n) {
|
|
178
200
|
const pageCounts = /* @__PURE__ */ new Map();
|
|
179
201
|
for (const locale of i18n.locales) pageCounts.set(locale.code, 0);
|
|
@@ -181,7 +203,7 @@ Canonical Origin: ${siteConfig.url}`);
|
|
|
181
203
|
const code = p.locale || resolveLocaleFromRoute(p.pathname, i18n).locale;
|
|
182
204
|
pageCounts.set(code, (pageCounts.get(code) ?? 0) + 1);
|
|
183
205
|
}
|
|
184
|
-
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts));
|
|
206
|
+
parts.push(...formatAvailableLanguagesSection(i18n, pageCounts, resolvePath));
|
|
185
207
|
parts.push("");
|
|
186
208
|
}
|
|
187
209
|
const isDefaultLocale = (item) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Convert a sitemap URL (which includes app.baseURL) to a logical route. */
|
|
2
|
+
export declare function toLogicalRoute(pathOrUrl: string, baseURL: string): string;
|
|
3
|
+
/** Apply app.baseURL to a logical route exactly once. */
|
|
4
|
+
export declare function toDeployedRoute(route: string, baseURL: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize rows written by older releases, which stored deployed paths.
|
|
7
|
+
* A sitemap match disambiguates those rows from real logical routes whose
|
|
8
|
+
* first segment happens to match app.baseURL.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePersistedRoute(route: string, sitemapRoutes: ReadonlySet<string>, baseURL: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { joinURL, parseURL, withLeadingSlash, withoutBase } from "ufo";
|
|
2
|
+
import { normalizePagePath } from "./markdown-path.js";
|
|
3
|
+
export function toLogicalRoute(pathOrUrl, baseURL) {
|
|
4
|
+
const pathname = withLeadingSlash(parseURL(pathOrUrl).pathname);
|
|
5
|
+
return normalizePagePath(withoutBase(pathname, baseURL));
|
|
6
|
+
}
|
|
7
|
+
export function toDeployedRoute(route, baseURL) {
|
|
8
|
+
return joinURL(baseURL, withLeadingSlash(route));
|
|
9
|
+
}
|
|
10
|
+
export function normalizePersistedRoute(route, sitemapRoutes, baseURL) {
|
|
11
|
+
const normalizedRoute = normalizePagePath(withLeadingSlash(parseURL(route).pathname));
|
|
12
|
+
if (sitemapRoutes.has(normalizedRoute))
|
|
13
|
+
return normalizedRoute;
|
|
14
|
+
const logicalRoute = toLogicalRoute(normalizedRoute, baseURL);
|
|
15
|
+
return sitemapRoutes.has(logicalRoute) ? logicalRoute : normalizedRoute;
|
|
16
|
+
}
|
|
@@ -57,17 +57,17 @@ async function getPrerenderDb() {
|
|
|
57
57
|
const excludeErrors = sql.includes("is_error = 0");
|
|
58
58
|
const includeMarkdown = wantsMarkdown(sql);
|
|
59
59
|
if (isErrorQuery) {
|
|
60
|
-
return
|
|
61
|
-
route
|
|
62
|
-
title:
|
|
63
|
-
description:
|
|
64
|
-
...includeMarkdown ? { markdown:
|
|
65
|
-
headings:
|
|
66
|
-
keywords:
|
|
67
|
-
updated_at:
|
|
60
|
+
return [...errorRoutes].map((route) => ({
|
|
61
|
+
route,
|
|
62
|
+
title: "",
|
|
63
|
+
description: "",
|
|
64
|
+
...includeMarkdown ? { markdown: "" } : {},
|
|
65
|
+
headings: "[]",
|
|
66
|
+
keywords: "[]",
|
|
67
|
+
updated_at: "",
|
|
68
68
|
is_error: 1,
|
|
69
69
|
indexed: 0,
|
|
70
|
-
locale:
|
|
70
|
+
locale: ""
|
|
71
71
|
}));
|
|
72
72
|
}
|
|
73
73
|
const filtered = excludeErrors ? pages.filter((p) => !errorRoutes.has(p.route)) : pages;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getHeader, sendRedirect, setHeader } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
5
|
import { queryPages } from "../db/queries.js";
|
|
5
6
|
import { logger } from "../logger.js";
|
|
6
7
|
import { convertHtmlToMarkdown, extractLastUpdated, getMarkdownRenderInfo, toMarkdownPath } from "../utils.js";
|
|
@@ -23,7 +24,7 @@ function setMarkdownHeaders(event, path, config, resolveUrl) {
|
|
|
23
24
|
setHeader(event, "cache-control", cacheControl);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
function notFoundMarkdown(canonicalUrl, path, config) {
|
|
27
|
+
function notFoundMarkdown(canonicalUrl, path, config, resolveUrl) {
|
|
27
28
|
const body = [
|
|
28
29
|
`# Page not found`,
|
|
29
30
|
``,
|
|
@@ -31,14 +32,14 @@ function notFoundMarkdown(canonicalUrl, path, config) {
|
|
|
31
32
|
``,
|
|
32
33
|
`Try one of these resources:`,
|
|
33
34
|
``,
|
|
34
|
-
`- [Sitemap](/sitemap.xml)`,
|
|
35
|
-
`- [llms.txt](/llms.txt)`,
|
|
36
|
-
`- [llms-full.txt](/llms-full.txt)`,
|
|
35
|
+
`- [Sitemap](${resolveUrl("/sitemap.xml")})`,
|
|
36
|
+
`- [llms.txt](${resolveUrl("/llms.txt")})`,
|
|
37
|
+
`- [llms-full.txt](${resolveUrl("/llms-full.txt")})`,
|
|
37
38
|
``
|
|
38
39
|
].join("\n");
|
|
39
40
|
const i18n = config.i18n;
|
|
40
41
|
const locale = i18n ? resolveLocaleFromRoute(path, i18n).locale : void 0;
|
|
41
|
-
const alternates = i18n ? computeLocaleAlternates(path, i18n).map((a) => ({ hreflang: a.hreflang, href: a.path })) : void 0;
|
|
42
|
+
const alternates = i18n ? computeLocaleAlternates(path, i18n).map((a) => ({ hreflang: a.hreflang, href: resolveUrl(a.path) })) : void 0;
|
|
42
43
|
const frontmatter = buildFrontmatter({
|
|
43
44
|
title: "Page not found",
|
|
44
45
|
description: `No content is available at ${path}.`,
|
|
@@ -64,15 +65,18 @@ export default defineEventHandler(async (event) => {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
const { path, isExplicit, negotiation } = renderInfo;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
68
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
69
|
+
const config = runtimeConfig["nuxt-ai-ready"];
|
|
70
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
71
|
+
const resolvePath = (path2) => toDeployedRoute(path2, baseURL);
|
|
72
|
+
const resolveUrl = (path2) => withSiteUrl(event, resolvePath(path2));
|
|
69
73
|
const canonicalUrl = resolveUrl(path);
|
|
70
74
|
if (negotiation === "html") {
|
|
71
75
|
setNegotiationHeaders(event, path, config, resolveUrl);
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
78
|
if (!isExplicit) {
|
|
75
|
-
return sendRedirect(event, toMarkdownPath(path), 307);
|
|
79
|
+
return sendRedirect(event, resolvePath(toMarkdownPath(path)), 307);
|
|
76
80
|
}
|
|
77
81
|
const contentPage = await tryGetContentMarkdown(event, path).catch((e) => {
|
|
78
82
|
logger.debug(`[markdown] Content lookup failed for ${path}`, e);
|
|
@@ -91,7 +95,7 @@ export default defineEventHandler(async (event) => {
|
|
|
91
95
|
${contentPage.markdown}`;
|
|
92
96
|
}
|
|
93
97
|
logger.debug(`[markdown] Fetching HTML for ${path}`);
|
|
94
|
-
const response = await event.fetch(path, {
|
|
98
|
+
const response = await event.fetch(resolvePath(path), {
|
|
95
99
|
headers: { [INTERNAL_HEADER]: "1" },
|
|
96
100
|
redirect: "manual"
|
|
97
101
|
}).catch((e) => {
|
|
@@ -100,7 +104,7 @@ ${contentPage.markdown}`;
|
|
|
100
104
|
});
|
|
101
105
|
if (!response) {
|
|
102
106
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
103
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
107
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
104
108
|
}
|
|
105
109
|
if (response.status >= 300 && response.status < 400) {
|
|
106
110
|
const location = response.headers.get("location");
|
|
@@ -115,12 +119,12 @@ ${contentPage.markdown}`;
|
|
|
115
119
|
}
|
|
116
120
|
if (!response.ok) {
|
|
117
121
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
118
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
122
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
119
123
|
}
|
|
120
124
|
const contentType = response.headers.get("content-type") || "";
|
|
121
125
|
if (!contentType.includes("text/html")) {
|
|
122
126
|
setMarkdownHeaders(event, path, config, resolveUrl);
|
|
123
|
-
return notFoundMarkdown(canonicalUrl, path, config);
|
|
127
|
+
return notFoundMarkdown(canonicalUrl, path, config, resolveUrl);
|
|
124
128
|
}
|
|
125
129
|
const html = await response.text();
|
|
126
130
|
logger.debug(`[markdown] Fetched HTML for ${path} (${html.length} bytes)`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
5
|
import { logger } from "../logger.js";
|
|
5
6
|
import { convertHtmlToMarkdown, extractLastUpdated, getMarkdownRenderInfo } from "../utils.js";
|
|
6
7
|
import { tryGetContentMarkdown } from "../utils/content.js";
|
|
@@ -26,8 +27,10 @@ export default defineEventHandler(async (event) => {
|
|
|
26
27
|
if (!renderInfo || "notAcceptable" in renderInfo)
|
|
27
28
|
return;
|
|
28
29
|
const { path } = renderInfo;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const fullRuntimeConfig = useRuntimeConfig(event);
|
|
31
|
+
const runtimeConfig = fullRuntimeConfig["nuxt-ai-ready"];
|
|
32
|
+
const deployedPath = toDeployedRoute(path, fullRuntimeConfig.app.baseURL);
|
|
33
|
+
const canonicalUrl = withSiteUrl(event, deployedPath);
|
|
31
34
|
const contentPage = await tryGetContentMarkdown(event, path).catch(() => null);
|
|
32
35
|
if (contentPage) {
|
|
33
36
|
logger.debug(`[markdown.prerender] Using content source for ${path} (${contentPage.markdown.length} bytes)`);
|
|
@@ -56,7 +59,7 @@ ${contentPage.markdown}`;
|
|
|
56
59
|
logger.debug(`[markdown.prerender] Reusing prerendered HTML for ${path} (${html.length} bytes)`);
|
|
57
60
|
} else {
|
|
58
61
|
logger.debug(`[markdown.prerender] Fetching HTML for ${path}`);
|
|
59
|
-
const response = await event.fetch(
|
|
62
|
+
const response = await event.fetch(deployedPath, { signal: AbortSignal.timeout(3e4) }).catch((err) => {
|
|
60
63
|
if (err?.name === "TimeoutError" || err?.name === "AbortError") {
|
|
61
64
|
throw createError({
|
|
62
65
|
statusCode: 504,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { eventHandler, sendIterable, setHeader, setResponseHeader } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
|
+
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
5
|
+
import { toDeployedRoute } from "../../route-path.js";
|
|
4
6
|
import { countPages, streamPages } from "../db/queries.js";
|
|
5
7
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from "../utils/llms-full.js";
|
|
6
8
|
export default eventHandler(async (event) => {
|
|
@@ -8,12 +10,15 @@ export default eventHandler(async (event) => {
|
|
|
8
10
|
setHeader(event, "Content-Type", "text/plain; charset=utf-8");
|
|
9
11
|
return "# llms-full.txt\n\nThis file is generated during prerender.";
|
|
10
12
|
}
|
|
11
|
-
const
|
|
13
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
14
|
+
const config = runtimeConfig["nuxt-ai-ready"];
|
|
12
15
|
const siteConfig = getSiteConfig(event);
|
|
16
|
+
const baseURL = runtimeConfig.app.baseURL;
|
|
17
|
+
const canonicalSiteUrl = siteConfig.url ? withSiteUrl(event, toDeployedRoute("/", baseURL)) : void 0;
|
|
13
18
|
const header = buildLlmsFullTxtHeader(
|
|
14
19
|
{
|
|
15
20
|
name: siteConfig.name,
|
|
16
|
-
url:
|
|
21
|
+
url: canonicalSiteUrl,
|
|
17
22
|
description: siteConfig.description
|
|
18
23
|
},
|
|
19
24
|
config.llmsTxt
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
1
2
|
export const INDEXNOW_HOSTS = ["api.indexnow.org", "www.bing.com"];
|
|
2
3
|
export function getIndexNowEndpoints() {
|
|
3
4
|
const testEndpoint = process.env.INDEXNOW_TEST_ENDPOINT;
|
|
@@ -8,12 +9,12 @@ export function getIndexNowEndpoints() {
|
|
|
8
9
|
}
|
|
9
10
|
export function buildIndexNowBody(routes, key, siteUrl) {
|
|
10
11
|
const urlList = routes.map(
|
|
11
|
-
(route) => route.startsWith("http") ? route :
|
|
12
|
+
(route) => route.startsWith("http") ? route : joinURL(siteUrl, route)
|
|
12
13
|
);
|
|
13
14
|
return {
|
|
14
15
|
host: new URL(siteUrl).host,
|
|
15
16
|
key,
|
|
16
|
-
keyLocation: `${
|
|
17
|
+
keyLocation: joinURL(siteUrl, `${key}.txt`),
|
|
17
18
|
urlList
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -15,9 +15,11 @@ function resolveHeaderUrl(path, resolveUrl) {
|
|
|
15
15
|
export function buildLinkHeader(path, variant, config, resolveUrl) {
|
|
16
16
|
const parts = [];
|
|
17
17
|
if (variant === "html") {
|
|
18
|
-
|
|
18
|
+
const href = resolveHeaderUrl(toMarkdownPath(path), resolveUrl);
|
|
19
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/markdown"`);
|
|
19
20
|
} else {
|
|
20
|
-
|
|
21
|
+
const href = resolveHeaderUrl(path, resolveUrl);
|
|
22
|
+
parts.push(`<${encodePathForHeader(href)}>; rel="alternate"; type="text/html"`);
|
|
21
23
|
}
|
|
22
24
|
if (config.i18n) {
|
|
23
25
|
const alternates = computeLocaleAlternates(path, config.i18n);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
1
2
|
import { normalizeLlmsTxtConfig } from "../../llms-txt-format.js";
|
|
2
|
-
const RE_TRAILING_SLASH = /\/$/;
|
|
3
3
|
const RE_FRONTMATTER = /^---\n[\s\S]*?\n---\n*/;
|
|
4
4
|
const RE_HEADING = /^(#{1,6}) ([^\n]+)$/gm;
|
|
5
5
|
export function formatPageForLlmsFullTxt(route, title, description, markdown, siteUrl) {
|
|
6
|
-
const canonicalUrl = siteUrl ?
|
|
6
|
+
const canonicalUrl = siteUrl ? joinURL(siteUrl, route) : route;
|
|
7
7
|
const heading = title && title !== route ? `### ${title}` : `### ${route}`;
|
|
8
8
|
const content = markdown.replace(RE_FRONTMATTER, "").replace(RE_HEADING, (_, hashes, text) => `h${hashes.length}. ${text}`);
|
|
9
9
|
const parts = [heading, ""];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ai-ready",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.6",
|
|
5
5
|
"description": "Best practice AI & LLM discoverability for Nuxt sites.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@mdream/js": "^1.5.
|
|
60
|
+
"@mdream/js": "^1.5.3",
|
|
61
61
|
"@nuxt/kit": "^4.5.0",
|
|
62
62
|
"citty": "^0.2.2",
|
|
63
63
|
"consola": "^3.4.2",
|
|
64
64
|
"defu": "^6.1.7",
|
|
65
65
|
"drizzle-orm": "^0.45.2",
|
|
66
|
-
"mdream": "^1.5.
|
|
66
|
+
"mdream": "^1.5.3",
|
|
67
67
|
"nuxt-site-config": "^4.1.1",
|
|
68
|
-
"nuxtseo-shared": "^5.3.
|
|
68
|
+
"nuxtseo-shared": "^5.3.3",
|
|
69
69
|
"pathe": "^2.0.3",
|
|
70
70
|
"pkg-types": "^2.3.1",
|
|
71
71
|
"site-config-stack": "^4.1.1",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"@nuxt/test-utils": "^4.0.3",
|
|
82
82
|
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
83
83
|
"@nuxtjs/mcp-toolkit": "^0.18.0",
|
|
84
|
-
"@nuxtjs/robots": "^6.1.
|
|
84
|
+
"@nuxtjs/robots": "^6.1.3",
|
|
85
85
|
"@nuxtjs/sitemap": "^8.2.2",
|
|
86
86
|
"@types/better-sqlite3": "^7.6.13",
|
|
87
87
|
"@vitest/coverage-v8": "^4.1.10",
|
|
@@ -92,15 +92,15 @@
|
|
|
92
92
|
"eslint": "^10.7.0",
|
|
93
93
|
"eslint-plugin-harlanzw": "^0.17.0",
|
|
94
94
|
"execa": "^10.0.0",
|
|
95
|
-
"happy-dom": "^20.
|
|
95
|
+
"happy-dom": "^20.11.0",
|
|
96
96
|
"nitropack": "^2.13.4",
|
|
97
97
|
"nuxt": "^4.5.0",
|
|
98
98
|
"nuxt-site-config": "^4.1.1",
|
|
99
|
-
"nuxtseo-layer-devtools": "^5.3.
|
|
99
|
+
"nuxtseo-layer-devtools": "^5.3.3",
|
|
100
100
|
"playwright": "^1.61.1",
|
|
101
101
|
"playwright-core": "^1.61.1",
|
|
102
102
|
"tinyglobby": "^0.2.17",
|
|
103
|
-
"typescript": "
|
|
103
|
+
"typescript": "6.0.3",
|
|
104
104
|
"unbuild": "^3.6.1",
|
|
105
105
|
"vitest": "^4.1.10",
|
|
106
106
|
"vue": "^3.5.40",
|