next-ai-discovery 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +267 -0
  3. package/dist/index.cjs +21 -0
  4. package/dist/index.d.cts +11 -0
  5. package/dist/index.d.cts.map +1 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +7 -0
  9. package/dist/llms-route.cjs +37 -0
  10. package/dist/llms-route.d.cts +13 -0
  11. package/dist/llms-route.d.cts.map +1 -0
  12. package/dist/llms-route.d.ts +13 -0
  13. package/dist/llms-route.d.ts.map +1 -0
  14. package/dist/llms-route.js +33 -0
  15. package/dist/llms.cjs +40 -0
  16. package/dist/llms.d.cts +20 -0
  17. package/dist/llms.d.cts.map +1 -0
  18. package/dist/llms.d.ts +20 -0
  19. package/dist/llms.d.ts.map +1 -0
  20. package/dist/llms.js +36 -0
  21. package/dist/markdown-route.cjs +59 -0
  22. package/dist/markdown-route.d.cts +16 -0
  23. package/dist/markdown-route.d.cts.map +1 -0
  24. package/dist/markdown-route.d.ts +16 -0
  25. package/dist/markdown-route.d.ts.map +1 -0
  26. package/dist/markdown-route.js +55 -0
  27. package/dist/metadata.cjs +26 -0
  28. package/dist/metadata.d.cts +57 -0
  29. package/dist/metadata.d.cts.map +1 -0
  30. package/dist/metadata.d.ts +57 -0
  31. package/dist/metadata.d.ts.map +1 -0
  32. package/dist/metadata.js +22 -0
  33. package/dist/pathname.cjs +37 -0
  34. package/dist/pathname.d.cts +4 -0
  35. package/dist/pathname.d.cts.map +1 -0
  36. package/dist/pathname.d.ts +4 -0
  37. package/dist/pathname.d.ts.map +1 -0
  38. package/dist/pathname.js +32 -0
  39. package/dist/proxy.cjs +79 -0
  40. package/dist/proxy.d.cts +22 -0
  41. package/dist/proxy.d.cts.map +1 -0
  42. package/dist/proxy.d.ts +22 -0
  43. package/dist/proxy.d.ts.map +1 -0
  44. package/dist/proxy.js +75 -0
  45. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2025 Nivalis
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # next-ai-discovery
2
+
3
+ Next.js 16 (App Router) helpers to make your site AI-discoverable by serving Markdown variants and advertising them via standard `<link rel="alternate" type="text/markdown" ...>` tags.
4
+
5
+ What this package provides:
6
+
7
+ - A `proxy.ts` factory to rewrite Markdown requests to a single internal route handler
8
+ - A route handler factory to serve per-page Markdown with correct headers (`Content-Type`, `Vary: Accept`)
9
+ - Metadata helpers to advertise per-page Markdown alternates
10
+ - A `llms.txt` route handler factory (and optional `llms-full.txt`)
11
+
12
+ ## Core behavior (explicit contract)
13
+
14
+ - Only these requests are rewritten to the internal Markdown endpoint:
15
+ - URLs ending in `.md` (example: `/docs.md`)
16
+ - Requests whose `Accept` header contains `text/markdown` (example: `Accept: text/markdown, text/html;q=0.9`)
17
+ - Other dot-paths are never rewritten (assets). Example: `/logo.png` is not rewritten.
18
+ - Root mapping is intentional: `/` is normalized to `/index`, so the home page Markdown twin is `/index.md`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm i next-ai-discovery
24
+ # or
25
+ pnpm add next-ai-discovery
26
+ # or
27
+ bun add next-ai-discovery
28
+ ```
29
+
30
+ ## Minimal setup (Next.js 16 App Router)
31
+
32
+ ### 1) Add `proxy.ts`
33
+
34
+ Create `proxy.ts` at the project root:
35
+
36
+ ```ts
37
+ import { createMarkdownProxy } from 'next-ai-discovery';
38
+
39
+ export default createMarkdownProxy();
40
+
41
+ export const config = {
42
+ // Recommended matcher:
43
+ // - runs on "normal" routes (no dot)
44
+ // - also runs on explicit `.md` routes
45
+ matcher: ['/((?!_next/|api/|.*\\..*).*)', '/(.*\\.md)'],
46
+ };
47
+ ```
48
+
49
+ Notes:
50
+
51
+ - Your `matcher` controls where Next runs the proxy. The proxy itself still has its own rewrite rules.
52
+ - Internal endpoint default is `DEFAULT_ENDPOINT_PATH = '/__aid/md'`.
53
+ - The internal endpoint is automatically excluded to avoid rewrite loops.
54
+
55
+ ### 2) Add the internal Markdown endpoint
56
+
57
+ Create `app/__aid/md/route.ts`:
58
+
59
+ ```ts
60
+ import { createMarkdownRoute } from 'next-ai-discovery';
61
+ import type { NextRequest } from 'next/server';
62
+
63
+ const handler = createMarkdownRoute({
64
+ async getMarkdown(pathname, request: NextRequest) {
65
+ // IMPORTANT: `getMarkdown()` is your policy boundary.
66
+ // Enforce the same auth/policy as your HTML routes.
67
+
68
+ // Root is normalized to `/index`.
69
+ if (pathname === '/index') {
70
+ return {
71
+ frontmatter: {
72
+ title: 'home',
73
+ canonical: 'https://example.com/',
74
+ },
75
+ body: '# home\n\nhello.',
76
+ };
77
+ }
78
+
79
+ return null;
80
+ },
81
+ });
82
+
83
+ export const GET = handler;
84
+ export const HEAD = handler;
85
+ ```
86
+
87
+ Notes:
88
+
89
+ - Responses include `Content-Type: text/markdown; charset=utf-8` and `Vary: Accept`.
90
+ - `HEAD` is supported; the handler returns the same status/headers but no body.
91
+
92
+ ### 3) Add `llms.txt`
93
+
94
+ `/llms.txt` is a proposed convention for publishing a short, curated Markdown index to help LLMs and agents understand your site.
95
+ Reference: `https://llmstxt.org/`
96
+
97
+ Create `app/llms.txt/route.ts`:
98
+
99
+ ```ts
100
+ import { createLlmsTxtRoute } from 'next-ai-discovery';
101
+
102
+ export const GET = createLlmsTxtRoute({
103
+ config: {
104
+ site: {
105
+ name: 'Example.com',
106
+ description: 'This site publishes articles about X.',
107
+ url: 'https://example.com',
108
+ },
109
+ sections: [{ title: 'Key sections', items: ['/blog', '/docs', '/about'] }],
110
+ markdown: {
111
+ appendDotMd: true,
112
+ acceptNegotiation: true,
113
+ fullIndexPath: '/llms-full.txt',
114
+ },
115
+ },
116
+ });
117
+ ```
118
+
119
+ Optional full variant:
120
+
121
+ - Create `app/llms-full.txt/route.ts` with `variant: 'full'`.
122
+ - “full” is not auto-generated by this package; it just selects a variant so you can return a larger inventory if you want.
123
+
124
+ Advertising `llms.txt`:
125
+
126
+ - This package does not auto-inject a global `<link ... href="/llms.txt">`.
127
+ - If you want it, add it in your root `app/layout.tsx` metadata manually.
128
+
129
+ ## What crawlers will see (HTTP examples)
130
+
131
+ ```bash
132
+ # content negotiation
133
+ curl -i -H 'Accept: text/markdown' https://example.com/docs
134
+
135
+ # explicit .md
136
+ curl -i https://example.com/docs.md
137
+ ```
138
+
139
+ Expected headers (both):
140
+
141
+ - `Content-Type: text/markdown; charset=utf-8`
142
+ - `Vary: Accept`
143
+
144
+ ## Per-page Markdown auto-discovery
145
+
146
+ To advertise a Markdown twin from HTML using the Next.js Metadata API, use `withMarkdownAlternate()`.
147
+
148
+ ```ts
149
+ import { withMarkdownAlternate } from 'next-ai-discovery';
150
+ import type { Metadata } from 'next';
151
+
152
+ export async function generateMetadata(): Promise<Metadata> {
153
+ return withMarkdownAlternate({ title: 'Docs' }, '/docs');
154
+ }
155
+ ```
156
+
157
+ This emits:
158
+
159
+ ```html
160
+ <link rel="alternate" type="text/markdown" href="/docs.md" />
161
+ ```
162
+
163
+ Home page note:
164
+
165
+ - `pathnameToMd('/')` yields `/index.md` (because `/` is normalized to `/index`).
166
+
167
+ ## Configuration
168
+
169
+ ### `createMarkdownProxy(options)`
170
+
171
+ - `endpointPath` (default: `/__aid/md`)
172
+ - `enableDotMd` (default: `true`)
173
+ - `enableAcceptNegotiation` (default: `true`)
174
+ - `acceptHeader` (default: `text/markdown`)
175
+ - `exclude(pathname): boolean` (optional)
176
+ - `excludePrefixes` (default: `["/_next", "/api"]`)
177
+ - `excludeExact` (default: `["/robots.txt", "/sitemap.xml"]`)
178
+ - `onRewrite({ type: 'accept' | 'dotmd', pathname })` (optional)
179
+
180
+ Precedence / terminology:
181
+
182
+ - `config.matcher` decides which requests execute the proxy at all.
183
+ - `exclude*` decides which requests the proxy will rewrite.
184
+
185
+ Content negotiation semantics:
186
+
187
+ - Any `Accept` header value that contains a comma-separated entry starting with `text/markdown` triggers Markdown (q-values are ignored).
188
+
189
+ Dot-path behavior:
190
+
191
+ - For `Accept` negotiation, the proxy will not rewrite “asset-like” URLs containing a dot after the last slash (example: `/logo.png`).
192
+ - Explicit `.md` URLs are always eligible (example: `/docs.md`).
193
+
194
+ ### `createMarkdownRoute({ getMarkdown, includeFrontmatter, onServed })`
195
+
196
+ - `getMarkdown(pathname, request)` returns `{ body, frontmatter? }` or `null`
197
+ - `includeFrontmatter` (default: `true`)
198
+ - `onServed({ pathname, status })` (optional)
199
+
200
+ ### Path normalization
201
+
202
+ Internally, paths are normalized to make matching predictable:
203
+
204
+ - `/` -> `/index`
205
+ - Trailing slash is removed (`/docs/` -> `/docs`)
206
+ - `.md` suffix is removed (`/docs.md` -> `/docs`)
207
+
208
+ ## Compatibility / runtime
209
+
210
+ - Next.js: App Router only
211
+ - Proxy (`proxy.ts`): Edge runtime (Next.js proxy)
212
+ - Route handlers: can be Edge or Node depending on how you configure your Next route file; this library itself does not require Node-only APIs.
213
+
214
+ ## Why not HTML -> Markdown conversion?
215
+
216
+ This package avoids HTML rewriting/conversion on purpose: it is hard to do reliably, and it risks leaking content. Instead, you provide an explicit Markdown representation via `getMarkdown()`.
217
+
218
+ ## Auth parity patterns
219
+
220
+ This package intentionally makes `getMarkdown()` your policy boundary.
221
+
222
+ ### 1) Reuse an existing access check
223
+
224
+ ```ts
225
+ import { createMarkdownRoute } from 'next-ai-discovery';
226
+ import type { NextRequest } from 'next/server';
227
+
228
+ async function canViewPath(request: NextRequest, pathname: string) {
229
+ // Example only. Wire this to your auth/session logic.
230
+ // Return false for protected routes.
231
+ return !pathname.startsWith('/admin');
232
+ }
233
+
234
+ const handler = createMarkdownRoute({
235
+ async getMarkdown(pathname, request) {
236
+ if (!(await canViewPath(request, pathname))) {
237
+ // Recommended default: return null (404) to avoid leaking existence.
238
+ return null;
239
+ }
240
+
241
+ return { body: `# ${pathname}\n` };
242
+ },
243
+ });
244
+
245
+ export const GET = handler;
246
+ export const HEAD = handler;
247
+ ```
248
+
249
+ ### 2) 404 vs 401/403
250
+
251
+ If your HTML route would redirect unauthenticated users, decide whether your Markdown variant should:
252
+
253
+ - Return `404` (recommended for private areas)
254
+ - Return `401/403`
255
+
256
+ Because Next.js route handlers return `Response`, you can also wrap the handler and map outcomes.
257
+
258
+ ## Roadmap
259
+
260
+ - frontmatter format options (yaml vs json vs none)
261
+ - llms-full helpers (sitemap-driven inventory)
262
+ - observability hooks (`onServed`, `onRewrite`) docs + examples
263
+ - codemod/snippets for adding `withMarkdownAlternate()`
264
+
265
+ ## License
266
+
267
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PROXY_EXCLUDE_PREFIXES = exports.DEFAULT_PROXY_EXCLUDE_EXACT = exports.DEFAULT_MARKDOWN_ACCEPT = exports.DEFAULT_ENDPOINT_PATH = exports.createMarkdownProxy = exports.pathnameToMd = exports.normalizePathname = exports.withMarkdownAlternate = exports.createMarkdownRoute = exports.createLlmsTxtRoute = exports.renderLlmsTxt = void 0;
4
+ /* biome-ignore lint/performance/noBarrelFile: public entrypoint exports */
5
+ var llms_js_1 = require("./llms.cjs");
6
+ Object.defineProperty(exports, "renderLlmsTxt", { enumerable: true, get: function () { return llms_js_1.renderLlmsTxt; } });
7
+ var llms_route_js_1 = require("./llms-route.cjs");
8
+ Object.defineProperty(exports, "createLlmsTxtRoute", { enumerable: true, get: function () { return llms_route_js_1.createLlmsTxtRoute; } });
9
+ var markdown_route_js_1 = require("./markdown-route.cjs");
10
+ Object.defineProperty(exports, "createMarkdownRoute", { enumerable: true, get: function () { return markdown_route_js_1.createMarkdownRoute; } });
11
+ var metadata_js_1 = require("./metadata.cjs");
12
+ Object.defineProperty(exports, "withMarkdownAlternate", { enumerable: true, get: function () { return metadata_js_1.withMarkdownAlternate; } });
13
+ var pathname_js_1 = require("./pathname.cjs");
14
+ Object.defineProperty(exports, "normalizePathname", { enumerable: true, get: function () { return pathname_js_1.normalizePathname; } });
15
+ Object.defineProperty(exports, "pathnameToMd", { enumerable: true, get: function () { return pathname_js_1.pathnameToMd; } });
16
+ var proxy_js_1 = require("./proxy.cjs");
17
+ Object.defineProperty(exports, "createMarkdownProxy", { enumerable: true, get: function () { return proxy_js_1.createMarkdownProxy; } });
18
+ Object.defineProperty(exports, "DEFAULT_ENDPOINT_PATH", { enumerable: true, get: function () { return proxy_js_1.DEFAULT_ENDPOINT_PATH; } });
19
+ Object.defineProperty(exports, "DEFAULT_MARKDOWN_ACCEPT", { enumerable: true, get: function () { return proxy_js_1.DEFAULT_MARKDOWN_ACCEPT; } });
20
+ Object.defineProperty(exports, "DEFAULT_PROXY_EXCLUDE_EXACT", { enumerable: true, get: function () { return proxy_js_1.DEFAULT_PROXY_EXCLUDE_EXACT; } });
21
+ Object.defineProperty(exports, "DEFAULT_PROXY_EXCLUDE_PREFIXES", { enumerable: true, get: function () { return proxy_js_1.DEFAULT_PROXY_EXCLUDE_PREFIXES; } });
@@ -0,0 +1,11 @@
1
+ export { renderLlmsTxt } from "./llms.cjs";
2
+ export { createLlmsTxtRoute } from "./llms-route.cjs";
3
+ export { createMarkdownRoute } from "./markdown-route.cjs";
4
+ export { withMarkdownAlternate } from "./metadata.cjs";
5
+ export { normalizePathname, pathnameToMd } from "./pathname.cjs";
6
+ export { createMarkdownProxy, DEFAULT_ENDPOINT_PATH, DEFAULT_MARKDOWN_ACCEPT, DEFAULT_PROXY_EXCLUDE_EXACT, DEFAULT_PROXY_EXCLUDE_PREFIXES, } from "./proxy.cjs";
7
+ export type { LlmsTxtConfig, LlmsTxtProvider, LlmsTxtVariant, } from "./llms.cjs";
8
+ export type { GetMarkdown, MarkdownContent, MarkdownRouteOptions, } from "./markdown-route.cjs";
9
+ export type { MarkdownAlternateOptions } from "./metadata.cjs";
10
+ export type { MarkdownProxyOptions, MarkdownRewriteType, } from "./proxy.cjs";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,mBAAkB;AAC1C,OAAO,EAAE,kBAAkB,EAAE,yBAAwB;AACrD,OAAO,EAAE,mBAAmB,EAAE,6BAA4B;AAC1D,OAAO,EAAE,qBAAqB,EAAE,uBAAsB;AACtD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,uBAAsB;AAChE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,uBAAuB,EACvB,2BAA2B,EAC3B,8BAA8B,GAC/B,oBAAmB;AACpB,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,GACf,mBAAkB;AACnB,YAAY,EACV,WAAW,EACX,eAAe,EACf,oBAAoB,GACrB,6BAA4B;AAC7B,YAAY,EAAE,wBAAwB,EAAE,uBAAsB;AAC9D,YAAY,EACV,oBAAoB,EACpB,mBAAmB,GACpB,oBAAmB"}
@@ -0,0 +1,11 @@
1
+ export { renderLlmsTxt } from "./llms.js";
2
+ export { createLlmsTxtRoute } from "./llms-route.js";
3
+ export { createMarkdownRoute } from "./markdown-route.js";
4
+ export { withMarkdownAlternate } from "./metadata.js";
5
+ export { normalizePathname, pathnameToMd } from "./pathname.js";
6
+ export { createMarkdownProxy, DEFAULT_ENDPOINT_PATH, DEFAULT_MARKDOWN_ACCEPT, DEFAULT_PROXY_EXCLUDE_EXACT, DEFAULT_PROXY_EXCLUDE_PREFIXES, } from "./proxy.js";
7
+ export type { LlmsTxtConfig, LlmsTxtProvider, LlmsTxtVariant, } from "./llms.js";
8
+ export type { GetMarkdown, MarkdownContent, MarkdownRouteOptions, } from "./markdown-route.js";
9
+ export type { MarkdownAlternateOptions } from "./metadata.js";
10
+ export type { MarkdownProxyOptions, MarkdownRewriteType, } from "./proxy.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB;AAC1C,OAAO,EAAE,kBAAkB,EAAE,wBAAwB;AACrD,OAAO,EAAE,mBAAmB,EAAE,4BAA4B;AAC1D,OAAO,EAAE,qBAAqB,EAAE,sBAAsB;AACtD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,sBAAsB;AAChE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,uBAAuB,EACvB,2BAA2B,EAC3B,8BAA8B,GAC/B,mBAAmB;AACpB,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,GACf,kBAAkB;AACnB,YAAY,EACV,WAAW,EACX,eAAe,EACf,oBAAoB,GACrB,4BAA4B;AAC7B,YAAY,EAAE,wBAAwB,EAAE,sBAAsB;AAC9D,YAAY,EACV,oBAAoB,EACpB,mBAAmB,GACpB,mBAAmB"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /* biome-ignore lint/performance/noBarrelFile: public entrypoint exports */
2
+ export { renderLlmsTxt } from "./llms.js";
3
+ export { createLlmsTxtRoute } from "./llms-route.js";
4
+ export { createMarkdownRoute } from "./markdown-route.js";
5
+ export { withMarkdownAlternate } from "./metadata.js";
6
+ export { normalizePathname, pathnameToMd } from "./pathname.js";
7
+ export { createMarkdownProxy, DEFAULT_ENDPOINT_PATH, DEFAULT_MARKDOWN_ACCEPT, DEFAULT_PROXY_EXCLUDE_EXACT, DEFAULT_PROXY_EXCLUDE_PREFIXES, } from "./proxy.js";
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLlmsTxtRoute = void 0;
4
+ const llms_js_1 = require("./llms.cjs");
5
+ const DEFAULT_HEADERS = {
6
+ 'Content-Type': 'text/markdown; charset=utf-8',
7
+ };
8
+ const resolveConfig = (options, variant) => {
9
+ if ('config' in options && options.config) {
10
+ return options.config;
11
+ }
12
+ return options.getLlmsTxt(variant);
13
+ };
14
+ const handleError = (error) => {
15
+ console.error('[next-ai-discovery] llms.txt route error', error);
16
+ return new Response('Internal Server Error', {
17
+ status: 500,
18
+ headers: DEFAULT_HEADERS,
19
+ });
20
+ };
21
+ const createLlmsTxtRoute = (options) => {
22
+ const variant = options.variant ?? 'default';
23
+ return async (_request) => {
24
+ try {
25
+ const config = await resolveConfig(options, variant);
26
+ const body = (0, llms_js_1.renderLlmsTxt)(config, variant);
27
+ return new Response(body, {
28
+ status: 200,
29
+ headers: DEFAULT_HEADERS,
30
+ });
31
+ }
32
+ catch (error) {
33
+ return handleError(error);
34
+ }
35
+ };
36
+ };
37
+ exports.createLlmsTxtRoute = createLlmsTxtRoute;
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from 'next/server.js';
2
+ import type { LlmsTxtConfig, LlmsTxtProvider, LlmsTxtVariant } from "./llms.cjs";
3
+ export type LlmsTxtRouteOptions = {
4
+ config: LlmsTxtConfig;
5
+ getLlmsTxt?: never;
6
+ variant?: LlmsTxtVariant;
7
+ } | {
8
+ config?: never;
9
+ getLlmsTxt: LlmsTxtProvider;
10
+ variant?: LlmsTxtVariant;
11
+ };
12
+ export declare const createLlmsTxtRoute: (options: LlmsTxtRouteOptions) => (_request: NextRequest) => Promise<Response>;
13
+ //# sourceMappingURL=llms-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms-route.d.ts","sourceRoot":"","sources":["../src/llms-route.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,mBAAkB;AAEhF,MAAM,MAAM,mBAAmB,GAC3B;IACE,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,GACD;IACE,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,CAAC;AAyBN,eAAO,MAAM,kBAAkB,GAAI,SAAS,mBAAmB,MAG/C,UAAU,WAAW,sBAapC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { NextRequest } from 'next/server.js';
2
+ import type { LlmsTxtConfig, LlmsTxtProvider, LlmsTxtVariant } from "./llms.js";
3
+ export type LlmsTxtRouteOptions = {
4
+ config: LlmsTxtConfig;
5
+ getLlmsTxt?: never;
6
+ variant?: LlmsTxtVariant;
7
+ } | {
8
+ config?: never;
9
+ getLlmsTxt: LlmsTxtProvider;
10
+ variant?: LlmsTxtVariant;
11
+ };
12
+ export declare const createLlmsTxtRoute: (options: LlmsTxtRouteOptions) => (_request: NextRequest) => Promise<Response>;
13
+ //# sourceMappingURL=llms-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms-route.d.ts","sourceRoot":"","sources":["../src/llms-route.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,kBAAkB;AAEhF,MAAM,MAAM,mBAAmB,GAC3B;IACE,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,GACD;IACE,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B,CAAC;AAyBN,eAAO,MAAM,kBAAkB,GAAI,SAAS,mBAAmB,MAG/C,UAAU,WAAW,sBAapC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { renderLlmsTxt } from "./llms.js";
2
+ const DEFAULT_HEADERS = {
3
+ 'Content-Type': 'text/markdown; charset=utf-8',
4
+ };
5
+ const resolveConfig = (options, variant) => {
6
+ if ('config' in options && options.config) {
7
+ return options.config;
8
+ }
9
+ return options.getLlmsTxt(variant);
10
+ };
11
+ const handleError = (error) => {
12
+ console.error('[next-ai-discovery] llms.txt route error', error);
13
+ return new Response('Internal Server Error', {
14
+ status: 500,
15
+ headers: DEFAULT_HEADERS,
16
+ });
17
+ };
18
+ export const createLlmsTxtRoute = (options) => {
19
+ const variant = options.variant ?? 'default';
20
+ return async (_request) => {
21
+ try {
22
+ const config = await resolveConfig(options, variant);
23
+ const body = renderLlmsTxt(config, variant);
24
+ return new Response(body, {
25
+ status: 200,
26
+ headers: DEFAULT_HEADERS,
27
+ });
28
+ }
29
+ catch (error) {
30
+ return handleError(error);
31
+ }
32
+ };
33
+ };
package/dist/llms.cjs ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderLlmsTxt = void 0;
4
+ const formatSection = (section) => {
5
+ const lines = [`## ${section.title}`];
6
+ for (const item of section.items) {
7
+ lines.push(`- ${item}`);
8
+ }
9
+ return lines.join('\n');
10
+ };
11
+ const joinBlocks = (blocks) => blocks.filter(block => block.trim().length > 0).join('\n\n');
12
+ const renderLlmsTxt = (config, variant) => {
13
+ const blocks = [`# ${config.site.name}`];
14
+ if (config.site.description) {
15
+ blocks.push(config.site.description);
16
+ }
17
+ if (config.sections && config.sections.length > 0) {
18
+ const rendered = config.sections.map(section => formatSection(section));
19
+ blocks.push(joinBlocks(rendered));
20
+ }
21
+ if (config.markdown) {
22
+ const markdownLines = ['## Machine-readable variants'];
23
+ if (config.markdown.appendDotMd ?? true) {
24
+ markdownLines.push('- Markdown pages: append `.md` to most URLs');
25
+ }
26
+ if (config.markdown.acceptNegotiation ?? true) {
27
+ markdownLines.push('- Negotiation: send `Accept: text/markdown`');
28
+ }
29
+ if (variant === 'default') {
30
+ const fullIndex = config.markdown.fullIndexPath ?? '/llms-full.txt';
31
+ markdownLines.push(`- Full index: ${fullIndex}`);
32
+ }
33
+ blocks.push(markdownLines.join('\n'));
34
+ }
35
+ if (config.site.url) {
36
+ blocks.push(`Site: ${config.site.url}`);
37
+ }
38
+ return joinBlocks(blocks);
39
+ };
40
+ exports.renderLlmsTxt = renderLlmsTxt;
@@ -0,0 +1,20 @@
1
+ export type LlmsTxtVariant = 'default' | 'full';
2
+ export type LlmsTxtConfig = {
3
+ site: {
4
+ name: string;
5
+ description?: string;
6
+ url?: string;
7
+ };
8
+ sections?: Array<{
9
+ title: string;
10
+ items: Array<string>;
11
+ }>;
12
+ markdown?: {
13
+ appendDotMd?: boolean;
14
+ acceptNegotiation?: boolean;
15
+ fullIndexPath?: string;
16
+ };
17
+ };
18
+ export type LlmsTxtProvider = (variant: LlmsTxtVariant) => Promise<LlmsTxtConfig> | LlmsTxtConfig;
19
+ export declare const renderLlmsTxt: (config: LlmsTxtConfig, variant: LlmsTxtVariant) => string;
20
+ //# sourceMappingURL=llms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms.d.ts","sourceRoot":"","sources":["../src/llms.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IAC1D,QAAQ,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,cAAc,KACpB,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC;AAa5C,eAAO,MAAM,aAAa,GACxB,QAAQ,aAAa,EACrB,SAAS,cAAc,WAkCxB,CAAC"}
package/dist/llms.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type LlmsTxtVariant = 'default' | 'full';
2
+ export type LlmsTxtConfig = {
3
+ site: {
4
+ name: string;
5
+ description?: string;
6
+ url?: string;
7
+ };
8
+ sections?: Array<{
9
+ title: string;
10
+ items: Array<string>;
11
+ }>;
12
+ markdown?: {
13
+ appendDotMd?: boolean;
14
+ acceptNegotiation?: boolean;
15
+ fullIndexPath?: string;
16
+ };
17
+ };
18
+ export type LlmsTxtProvider = (variant: LlmsTxtVariant) => Promise<LlmsTxtConfig> | LlmsTxtConfig;
19
+ export declare const renderLlmsTxt: (config: LlmsTxtConfig, variant: LlmsTxtVariant) => string;
20
+ //# sourceMappingURL=llms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms.d.ts","sourceRoot":"","sources":["../src/llms.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IAC1D,QAAQ,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,cAAc,KACpB,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC;AAa5C,eAAO,MAAM,aAAa,GACxB,QAAQ,aAAa,EACrB,SAAS,cAAc,WAkCxB,CAAC"}
package/dist/llms.js ADDED
@@ -0,0 +1,36 @@
1
+ const formatSection = (section) => {
2
+ const lines = [`## ${section.title}`];
3
+ for (const item of section.items) {
4
+ lines.push(`- ${item}`);
5
+ }
6
+ return lines.join('\n');
7
+ };
8
+ const joinBlocks = (blocks) => blocks.filter(block => block.trim().length > 0).join('\n\n');
9
+ export const renderLlmsTxt = (config, variant) => {
10
+ const blocks = [`# ${config.site.name}`];
11
+ if (config.site.description) {
12
+ blocks.push(config.site.description);
13
+ }
14
+ if (config.sections && config.sections.length > 0) {
15
+ const rendered = config.sections.map(section => formatSection(section));
16
+ blocks.push(joinBlocks(rendered));
17
+ }
18
+ if (config.markdown) {
19
+ const markdownLines = ['## Machine-readable variants'];
20
+ if (config.markdown.appendDotMd ?? true) {
21
+ markdownLines.push('- Markdown pages: append `.md` to most URLs');
22
+ }
23
+ if (config.markdown.acceptNegotiation ?? true) {
24
+ markdownLines.push('- Negotiation: send `Accept: text/markdown`');
25
+ }
26
+ if (variant === 'default') {
27
+ const fullIndex = config.markdown.fullIndexPath ?? '/llms-full.txt';
28
+ markdownLines.push(`- Full index: ${fullIndex}`);
29
+ }
30
+ blocks.push(markdownLines.join('\n'));
31
+ }
32
+ if (config.site.url) {
33
+ blocks.push(`Site: ${config.site.url}`);
34
+ }
35
+ return joinBlocks(blocks);
36
+ };
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMarkdownRoute = void 0;
4
+ const pathname_js_1 = require("./pathname.cjs");
5
+ const DEFAULT_HEADERS = {
6
+ 'Content-Type': 'text/markdown; charset=utf-8',
7
+ Vary: 'Accept',
8
+ };
9
+ const toFrontmatter = (frontmatter) => {
10
+ const lines = ['---'];
11
+ for (const [key, value] of Object.entries(frontmatter)) {
12
+ lines.push(`${key}: ${JSON.stringify(value)}`);
13
+ }
14
+ lines.push('---');
15
+ return lines.join('\n');
16
+ };
17
+ const buildBody = (content, includeFrontmatter) => {
18
+ if (!(includeFrontmatter && content.frontmatter)) {
19
+ return content.body;
20
+ }
21
+ return `${toFrontmatter(content.frontmatter)}\n\n${content.body}`;
22
+ };
23
+ const handleError = (error) => {
24
+ console.error('[next-ai-discovery] markdown route error', error);
25
+ return new Response('Internal Server Error', {
26
+ status: 500,
27
+ headers: DEFAULT_HEADERS,
28
+ });
29
+ };
30
+ const createMarkdownRoute = (options) => {
31
+ const includeFrontmatter = options.includeFrontmatter ?? true;
32
+ return async (request) => {
33
+ const url = new URL(request.url);
34
+ const rawPath = url.searchParams.get('path') ?? '/';
35
+ const pathname = (0, pathname_js_1.normalizePathname)(rawPath);
36
+ try {
37
+ const content = await options.getMarkdown(pathname, request);
38
+ const isHead = request.method === 'HEAD';
39
+ if (!content) {
40
+ options.onServed?.({ pathname, status: 404 });
41
+ return new Response(isHead ? null : 'Not Found', {
42
+ status: 404,
43
+ headers: DEFAULT_HEADERS,
44
+ });
45
+ }
46
+ const body = buildBody(content, includeFrontmatter);
47
+ options.onServed?.({ pathname, status: 200 });
48
+ return new Response(isHead ? null : body, {
49
+ status: 200,
50
+ headers: DEFAULT_HEADERS,
51
+ });
52
+ }
53
+ catch (error) {
54
+ options.onServed?.({ pathname, status: 500 });
55
+ return handleError(error);
56
+ }
57
+ };
58
+ };
59
+ exports.createMarkdownRoute = createMarkdownRoute;
@@ -0,0 +1,16 @@
1
+ import type { NextRequest } from 'next/server.js';
2
+ export type MarkdownContent = {
3
+ body: string;
4
+ frontmatter?: Record<string, unknown>;
5
+ };
6
+ export type GetMarkdown = (pathname: string, request: NextRequest) => Promise<MarkdownContent | null> | MarkdownContent | null;
7
+ export type MarkdownRouteOptions = {
8
+ getMarkdown: GetMarkdown;
9
+ includeFrontmatter?: boolean;
10
+ onServed?: (event: {
11
+ pathname: string;
12
+ status: number;
13
+ }) => void;
14
+ };
15
+ export declare const createMarkdownRoute: (options: MarkdownRouteOptions) => (request: NextRequest) => Promise<Response>;
16
+ //# sourceMappingURL=markdown-route.d.ts.map