@agent-seo/next 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,90 @@
1
+ # @agent-seo/next
2
+
3
+ Next.js plugin for AI-readable websites. Zero-config `/llms.txt`, automatic HTML-to-Markdown for AI bots.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @agent-seo/next
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ **`next.config.ts`** — wrap your config:
14
+
15
+ ```typescript
16
+ import { withAgentSeo } from '@agent-seo/next';
17
+
18
+ export default withAgentSeo({
19
+ siteName: 'My App',
20
+ siteDescription: 'A brief description for AI systems.',
21
+ baseUrl: 'https://myapp.com',
22
+ sitemap: true,
23
+ })({
24
+ // your existing Next.js config
25
+ });
26
+ ```
27
+
28
+ **`middleware.ts`** — enable AI bot detection:
29
+
30
+ ```typescript
31
+ import { createAgentSeoMiddleware } from '@agent-seo/next/middleware';
32
+
33
+ export default createAgentSeoMiddleware({
34
+ exclude: ['/dashboard/**', '/admin/**'],
35
+ });
36
+
37
+ export const config = {
38
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
39
+ };
40
+ ```
41
+
42
+ That's it. Two files, ~10 lines total.
43
+
44
+ ## What It Does Automatically
45
+
46
+ - **Auto-generates `/llms.txt`** by scanning your `app/` directory and extracting `metadata` (title, description)
47
+ - **Auto-generates a transform API route** that converts HTML to Markdown on the fly
48
+ - **Auto-generates `robots.txt`** with an AI-friendly config pointing to `/llms.txt`
49
+ - **Detects 19 AI bots** via User-Agent and rewrites their requests to serve Markdown
50
+ - **Handles `.md` suffix** requests (e.g., `/about.md` returns Markdown)
51
+ - **Injects `Vary: Accept, User-Agent`** headers for correct CDN caching
52
+ - **Sets bot-friendly headers** — `Content-Disposition: inline`, `X-Robots-Tag: all`
53
+
54
+ ## How It Works
55
+
56
+ ```
57
+ Browser → GET /about → HTML (normal)
58
+ GPTBot → GET /about → Markdown (automatic)
59
+ Anyone → GET /about.md → Markdown (explicit)
60
+ Anyone → GET /llms.txt → Site directory for AI agents
61
+ ```
62
+
63
+ ## Plugin Options
64
+
65
+ | Option | Type | Description |
66
+ | ----------------- | ------------------- | ----------------------------------------------------------------------------------------- |
67
+ | `siteName` | `string` | Your site name (used in `llms.txt`) |
68
+ | `siteDescription` | `string` | Brief description for AI systems |
69
+ | `baseUrl` | `string` | Your site's public URL |
70
+ | `sitemap` | `boolean \| string` | Add `Sitemap:` to `robots.txt`. `true` uses `{baseUrl}/sitemap.xml`, or pass a custom URL |
71
+ | `appDir` | `string` | Override auto-detected `app/` directory path |
72
+ | `exclude` | `string[]` | Route patterns to exclude from `llms.txt` discovery |
73
+
74
+ ## Middleware Options
75
+
76
+ ```typescript
77
+ createAgentSeoMiddleware({
78
+ exclude: [
79
+ '/dashboard/**',
80
+ '/admin/**',
81
+ '/api/private/**',
82
+ ],
83
+ });
84
+ ```
85
+
86
+ Built-in defaults always excluded: `/api/**`, `/_next/**`, `/robots.txt`, `/sitemap.xml`, `/favicon.ico`, `/llms.txt`.
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/index.cjs CHANGED
@@ -40,8 +40,15 @@ function withAgentSeo(agentSeoOptions) {
40
40
  generateRobotsTxt(appDir, agentSeoOptions);
41
41
  }
42
42
  return (nextConfig = {}) => {
43
+ const existingExternal = nextConfig.serverExternalPackages ?? [];
43
44
  return {
44
45
  ...nextConfig,
46
+ // Prevent bundling jsdom (and its CJS deps) into serverless functions.
47
+ // jsdom must be loaded at runtime to avoid CJS/ESM incompatibilities.
48
+ serverExternalPackages: [
49
+ ...existingExternal,
50
+ ...["jsdom"].filter((pkg) => !existingExternal.includes(pkg))
51
+ ],
45
52
  async headers() {
46
53
  const existingHeaders = await (nextConfig.headers?.() ?? []);
47
54
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/plugin.ts","../src/middleware.ts","../src/route-handler.ts"],"sourcesContent":["export { withAgentSeo } from './plugin.js';\nexport type { WithAgentSeoOptions } from './plugin.js';\nexport { createAgentSeoMiddleware } from './middleware.js';\nexport type { AgentSeoMiddlewareOptions } from './middleware.js';\nexport { createLlmsTxtHandler } from './route-handler.js';\nexport type { LlmsTxtHandlerOptions } from './route-handler.js';\nexport {\n generateLlmsTxt,\n discoverNextRoutes,\n transform,\n} from '@agent-seo/core';\nexport type {\n AgentSeoOptions,\n AIRequestContext,\n TransformResult,\n BotInfo,\n BotPurpose,\n LlmsTxtRoute,\n DiscoverOptions,\n} from '@agent-seo/core';\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { NextConfig } from 'next';\nimport type { AgentSeoOptions } from '@agent-seo/core';\n\nexport interface WithAgentSeoOptions extends AgentSeoOptions {\n /**\n * Absolute path to the Next.js `app/` directory.\n * If omitted, auto-detected by checking `./app` and `./src/app` from cwd.\n */\n appDir?: string;\n /**\n * Sitemap URL for the `robots.txt` Sitemap directive.\n * - `true`: uses `{baseUrl}/sitemap.xml`\n * - `string`: uses the provided URL\n * - `undefined`/`false`: no Sitemap directive\n */\n sitemap?: boolean | string;\n}\n\n/**\n * Next.js config plugin that:\n * 1. Auto-generates `app/llms.txt/route.ts` (zero-config `/llms.txt` endpoint)\n * 2. Injects `Vary: Accept, User-Agent` headers on all pages\n *\n * @example\n * ```ts\n * // next.config.ts\n * import { withAgentSeo } from '@agent-seo/next';\n *\n * export default withAgentSeo({\n * siteName: 'My App',\n * siteDescription: 'A brief description for LLMs.',\n * baseUrl: 'https://myapp.com',\n * })({});\n * ```\n */\nexport function withAgentSeo(agentSeoOptions: WithAgentSeoOptions) {\n const appDir = agentSeoOptions.appDir || detectAppDir();\n\n // Auto-generate route handlers at config-evaluation time\n if (appDir) {\n generateLlmsTxtRoute(appDir, agentSeoOptions);\n generateTransformRoute(appDir, agentSeoOptions);\n generateRobotsTxt(appDir, agentSeoOptions);\n }\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n return {\n ...nextConfig,\n\n async headers() {\n const existingHeaders = await (nextConfig.headers?.() ?? []);\n return [\n ...existingHeaders,\n {\n source: '/((?!api|_next|static|favicon.ico).*)',\n headers: [{ key: 'Vary', value: 'Accept, User-Agent' }],\n },\n ];\n },\n };\n };\n}\n\n/**\n * Auto-detect the Next.js `app/` directory from common locations.\n */\nfunction detectAppDir(): string | null {\n const cwd = process.cwd();\n const candidates = [join(cwd, 'app'), join(cwd, 'src', 'app')];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n// Auto-generated file banner\nconst GENERATED_BANNER = `// AUTO-GENERATED by @agent-seo/next — do not edit manually.\n// This file is created by withAgentSeo() in next.config.ts.\n// Add \"app/llms.txt\" to your .gitignore.\n`;\n\n/**\n * Write `app/llms.txt/route.js` with a handler that auto-discovers routes.\n */\nfunction generateLlmsTxtRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'llms.txt');\n const routeFile = join(routeDir, 'route.js');\n\n // Serialize the options we need into the generated file\n const siteName = escapeStr(options.siteName);\n const siteDescription = escapeStr(options.siteDescription);\n const baseUrl = escapeStr(options.baseUrl);\n const excludePatterns = JSON.stringify(options.exclude || ['/api']);\n\n const content = `${GENERATED_BANNER}\nimport { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/next';\nimport path from 'node:path';\n\nconst appDir = path.resolve(process.cwd(), 'app');\n\nexport async function GET(request) {\n const url = new URL(request.url);\n\n const routes = discoverNextRoutes(appDir, {\n exclude: ${excludePatterns},\n });\n\n const result = generateLlmsTxt(\n {\n siteName: '${siteName}',\n siteDescription: '${siteDescription}',\n baseUrl: process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}',\n },\n routes,\n );\n\n return new Response(result.llmsTxt, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n}\n`;\n\n // Only write if the file doesn't exist or content has changed\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\nfunction escapeStr(s: string): string {\n return s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\n/**\n * Write `app/api/agent-seo-transform/route.js` — a Node.js API route that\n * receives a `?path=/some-page` param, fetches the HTML from the local\n * Next.js server, runs the core `transform()` pipeline (JSDOM → Readability\n * → Turndown), and returns clean Markdown.\n */\nfunction generateTransformRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'api', 'agent-seo-transform');\n const routeFile = join(routeDir, 'route.js');\n\n const baseUrl = escapeStr(options.baseUrl);\n\n const content = `${GENERATED_BANNER}\nimport { transform } from '@agent-seo/next';\n\n// Force Node.js runtime (required for jsdom / Readability / Turndown)\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\nexport async function GET(request) {\n const { searchParams } = new URL(request.url);\n const pagePath = searchParams.get('path') || '/';\n\n // Build the internal URL to fetch the original HTML page\n const origin = process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}';\n if (!origin) {\n return new Response('Base URL not configured', { status: 500 });\n }\n\n // Only allow same-origin, absolute paths (prevent SSRF via absolute or protocol-relative URLs)\n const isValidPath =\n pagePath.startsWith('/') &&\n !pagePath.startsWith('//') &&\n !pagePath.includes('://') &&\n !pagePath.includes('\\\\\\\\');\n if (!isValidPath) {\n return new Response('Invalid path', { status: 400 });\n }\n\n const originUrl = new URL(origin);\n if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {\n return new Response('Invalid base URL', { status: 500 });\n }\n const pageUrl = new URL(pagePath, originUrl.origin);\n if (pageUrl.origin !== originUrl.origin) {\n return new Response('Invalid origin', { status: 400 });\n }\n\n try {\n // Fetch the page HTML from the local server with a normal User-Agent\n // to avoid infinite rewrite loops\n const res = await fetch(pageUrl.toString(), {\n headers: {\n 'User-Agent': 'AgentSEO-Internal/1.0',\n 'Accept': 'text/html',\n },\n });\n\n if (!res.ok) {\n return new Response('Page not found', { status: 404 });\n }\n\n const html = await res.text();\n\n const result = await transform(html, {\n url: pageUrl.toString(),\n });\n\n return new Response(result.markdown, {\n status: 200,\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n 'Vary': 'Accept, User-Agent',\n 'X-Robots-Tag': 'all',\n 'X-Agent-Seo': 'transformed',\n },\n });\n } catch (err) {\n console.error('[agent-seo] Transform error:', err);\n return new Response('Transform failed', { status: 500 });\n }\n}\n`;\n\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\n/**\n * Write `app/robots.txt` — a static robots.txt that allows all crawlers\n * and references /llms.txt for AI agents.\n * Only created if the file doesn't already exist (won't overwrite user customizations).\n */\nfunction generateRobotsTxt(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const robotsFile = join(appDir, 'robots.txt');\n\n // Don't overwrite user-created robots.txt\n if (existsSync(robotsFile)) {\n return;\n }\n\n let content = `# AI-Optimized robots.txt — Generated by @agent-seo/next\n# For AI agents: see /llms.txt for a structured site manifest\n\nUser-agent: *\nAllow: /\n`;\n\n if (options.sitemap) {\n const sitemapUrl =\n typeof options.sitemap === 'string'\n ? options.sitemap\n : `${options.baseUrl}/sitemap.xml`;\n content += `\\nSitemap: ${sitemapUrl}\\n`;\n }\n\n writeFileSync(robotsFile, content, 'utf-8');\n}\n","import { detectAgent } from '@agent-seo/core/edge';\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport interface AgentSeoMiddlewareOptions {\n /**\n * Glob patterns for paths that should NEVER be rewritten to Markdown.\n * Merged with built-in defaults (see `DEFAULT_EXCLUDE`).\n *\n * @example ['\\/dashboard\\/**', '\\/admin\\/**', '\\/api\\/private\\/**']\n */\n exclude?: string[];\n}\n\n/** Paths that are always skipped — framework internals + standard files. */\nconst ALWAYS_SKIP = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/llms.txt',\n '/llms-full.txt',\n]);\n\n/** Default exclude patterns — users can extend but not remove these. */\nconst DEFAULT_EXCLUDE: string[] = [\n '/api/**',\n '/_next/**',\n];\n\n/**\n * Creates a Next.js middleware that:\n * 1. Detects AI bot requests via User-Agent\n * 2. Rewrites AI bot requests to an internal transform API route\n * that converts HTML → Markdown (runs on Node.js runtime)\n * 3. Handles `.md` suffix requests (e.g. `/about.md` → transform `/about`)\n * 4. Sets `Vary: Accept, User-Agent` on all responses\n */\nexport function createAgentSeoMiddleware(options: AgentSeoMiddlewareOptions = {}) {\n const excludePatterns = [...DEFAULT_EXCLUDE, ...(options.exclude || [])];\n\n return function middleware(request: NextRequest) {\n const { pathname } = request.nextUrl;\n\n // Skip standard files (robots.txt, favicon, llms.txt, etc.)\n if (ALWAYS_SKIP.has(pathname)) {\n return NextResponse.next();\n }\n\n // Skip excluded patterns (API routes, admin, dashboard, etc.)\n if (isExcluded(pathname, excludePatterns)) {\n return NextResponse.next();\n }\n\n const ua = request.headers.get('user-agent');\n const accept = request.headers.get('accept');\n const aiCtx = detectAgent(ua, accept);\n\n // Handle explicit .md suffix requests (e.g. /about.md)\n if (pathname.endsWith('.md')) {\n const originalPath = pathname.slice(0, -3) || '/';\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', originalPath);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // If AI bot, rewrite to transform API\n if (aiCtx.isAIBot) {\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', pathname);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // Normal request — just add Vary header\n const response = NextResponse.next();\n response.headers.set('Vary', 'Accept, User-Agent');\n return response;\n };\n}\n\n/**\n * Set clean, bot-friendly headers on a rewrite response.\n * Overrides Next.js RSC-related headers that can confuse AI bots.\n */\nfunction setBotHeaders(response: NextResponse): NextResponse {\n response.headers.set('Content-Disposition', 'inline');\n response.headers.set('Vary', 'Accept, User-Agent');\n response.headers.set('X-Robots-Tag', 'all');\n response.headers.delete('x-nextjs-matched-path');\n return response;\n}\n\nfunction isExcluded(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchGlob(pattern, path));\n}\n\nfunction matchGlob(pattern: string, path: string): boolean {\n const regex = pattern\n .replace(/\\*\\*/g, '{{DOUBLESTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLESTAR}}/g, '.*');\n return new RegExp(`^${regex}$`).test(path);\n}\n","import { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/core';\nimport type { AgentSeoOptions, DiscoverOptions } from '@agent-seo/core';\n\nexport interface LlmsTxtHandlerOptions extends AgentSeoOptions {\n /** Return llms-full.txt instead of llms.txt */\n full?: boolean;\n\n /**\n * Absolute path to the Next.js `app/` directory for automatic route discovery.\n * When set, routes for llms.txt are auto-discovered by scanning page.tsx files.\n * Titles and descriptions are extracted from `export const metadata` in each page.\n *\n * @example\n * ```ts\n * appDir: path.resolve(process.cwd(), 'app')\n * ```\n */\n appDir?: string;\n\n /** Options for route discovery when using appDir */\n discoverOptions?: DiscoverOptions;\n}\n\nexport function createLlmsTxtHandler(options: LlmsTxtHandlerOptions) {\n return async function GET() {\n // Auto-discover routes from app/ directory if no explicit routes are provided\n let routes = options.llmsTxt?.routes || [];\n\n if (routes.length === 0 && options.appDir) {\n routes = discoverNextRoutes(options.appDir, {\n exclude: options.exclude || ['/api'],\n ...options.discoverOptions,\n });\n }\n\n const result = generateLlmsTxt(\n {\n siteName: options.siteName,\n siteDescription: options.siteDescription,\n baseUrl: options.baseUrl,\n ...options.llmsTxt,\n },\n routes,\n );\n\n const content = options.full ? result.llmsFullTxt : result.llmsTxt;\n\n return new Response(content, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n };\n}\n"],"mappings":"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","names":["import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/plugin.ts","../src/middleware.ts","../src/route-handler.ts"],"sourcesContent":["export { withAgentSeo } from './plugin.js';\nexport type { WithAgentSeoOptions } from './plugin.js';\nexport { createAgentSeoMiddleware } from './middleware.js';\nexport type { AgentSeoMiddlewareOptions } from './middleware.js';\nexport { createLlmsTxtHandler } from './route-handler.js';\nexport type { LlmsTxtHandlerOptions } from './route-handler.js';\nexport {\n generateLlmsTxt,\n discoverNextRoutes,\n transform,\n} from '@agent-seo/core';\nexport type {\n AgentSeoOptions,\n AIRequestContext,\n TransformResult,\n BotInfo,\n BotPurpose,\n LlmsTxtRoute,\n DiscoverOptions,\n} from '@agent-seo/core';\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { NextConfig } from 'next';\nimport type { AgentSeoOptions } from '@agent-seo/core';\n\nexport interface WithAgentSeoOptions extends AgentSeoOptions {\n /**\n * Absolute path to the Next.js `app/` directory.\n * If omitted, auto-detected by checking `./app` and `./src/app` from cwd.\n */\n appDir?: string;\n /**\n * Sitemap URL for the `robots.txt` Sitemap directive.\n * - `true`: uses `{baseUrl}/sitemap.xml`\n * - `string`: uses the provided URL\n * - `undefined`/`false`: no Sitemap directive\n */\n sitemap?: boolean | string;\n}\n\n/**\n * Next.js config plugin that:\n * 1. Auto-generates `app/llms.txt/route.ts` (zero-config `/llms.txt` endpoint)\n * 2. Injects `Vary: Accept, User-Agent` headers on all pages\n *\n * @example\n * ```ts\n * // next.config.ts\n * import { withAgentSeo } from '@agent-seo/next';\n *\n * export default withAgentSeo({\n * siteName: 'My App',\n * siteDescription: 'A brief description for LLMs.',\n * baseUrl: 'https://myapp.com',\n * })({});\n * ```\n */\nexport function withAgentSeo(agentSeoOptions: WithAgentSeoOptions) {\n const appDir = agentSeoOptions.appDir || detectAppDir();\n\n // Auto-generate route handlers at config-evaluation time\n if (appDir) {\n generateLlmsTxtRoute(appDir, agentSeoOptions);\n generateTransformRoute(appDir, agentSeoOptions);\n generateRobotsTxt(appDir, agentSeoOptions);\n }\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n const existingExternal = nextConfig.serverExternalPackages ?? [];\n\n return {\n ...nextConfig,\n\n // Prevent bundling jsdom (and its CJS deps) into serverless functions.\n // jsdom must be loaded at runtime to avoid CJS/ESM incompatibilities.\n serverExternalPackages: [\n ...existingExternal,\n ...['jsdom'].filter((pkg) => !existingExternal.includes(pkg)),\n ],\n\n async headers() {\n const existingHeaders = await (nextConfig.headers?.() ?? []);\n return [\n ...existingHeaders,\n {\n source: '/((?!api|_next|static|favicon.ico).*)',\n headers: [{ key: 'Vary', value: 'Accept, User-Agent' }],\n },\n ];\n },\n };\n };\n}\n\n/**\n * Auto-detect the Next.js `app/` directory from common locations.\n */\nfunction detectAppDir(): string | null {\n const cwd = process.cwd();\n const candidates = [join(cwd, 'app'), join(cwd, 'src', 'app')];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n// Auto-generated file banner\nconst GENERATED_BANNER = `// AUTO-GENERATED by @agent-seo/next — do not edit manually.\n// This file is created by withAgentSeo() in next.config.ts.\n// Add \"app/llms.txt\" to your .gitignore.\n`;\n\n/**\n * Write `app/llms.txt/route.js` with a handler that auto-discovers routes.\n */\nfunction generateLlmsTxtRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'llms.txt');\n const routeFile = join(routeDir, 'route.js');\n\n // Serialize the options we need into the generated file\n const siteName = escapeStr(options.siteName);\n const siteDescription = escapeStr(options.siteDescription);\n const baseUrl = escapeStr(options.baseUrl);\n const excludePatterns = JSON.stringify(options.exclude || ['/api']);\n\n const content = `${GENERATED_BANNER}\nimport { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/next';\nimport path from 'node:path';\n\nconst appDir = path.resolve(process.cwd(), 'app');\n\nexport async function GET(request) {\n const url = new URL(request.url);\n\n const routes = discoverNextRoutes(appDir, {\n exclude: ${excludePatterns},\n });\n\n const result = generateLlmsTxt(\n {\n siteName: '${siteName}',\n siteDescription: '${siteDescription}',\n baseUrl: process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}',\n },\n routes,\n );\n\n return new Response(result.llmsTxt, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n}\n`;\n\n // Only write if the file doesn't exist or content has changed\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\nfunction escapeStr(s: string): string {\n return s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\n/**\n * Write `app/api/agent-seo-transform/route.js` — a Node.js API route that\n * receives a `?path=/some-page` param, fetches the HTML from the local\n * Next.js server, runs the core `transform()` pipeline (JSDOM → Readability\n * → Turndown), and returns clean Markdown.\n */\nfunction generateTransformRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'api', 'agent-seo-transform');\n const routeFile = join(routeDir, 'route.js');\n\n const baseUrl = escapeStr(options.baseUrl);\n\n const content = `${GENERATED_BANNER}\nimport { transform } from '@agent-seo/next';\n\n// Force Node.js runtime (required for jsdom / Readability / Turndown)\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\nexport async function GET(request) {\n const { searchParams } = new URL(request.url);\n const pagePath = searchParams.get('path') || '/';\n\n // Build the internal URL to fetch the original HTML page\n const origin = process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}';\n if (!origin) {\n return new Response('Base URL not configured', { status: 500 });\n }\n\n // Only allow same-origin, absolute paths (prevent SSRF via absolute or protocol-relative URLs)\n const isValidPath =\n pagePath.startsWith('/') &&\n !pagePath.startsWith('//') &&\n !pagePath.includes('://') &&\n !pagePath.includes('\\\\\\\\');\n if (!isValidPath) {\n return new Response('Invalid path', { status: 400 });\n }\n\n const originUrl = new URL(origin);\n if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {\n return new Response('Invalid base URL', { status: 500 });\n }\n const pageUrl = new URL(pagePath, originUrl.origin);\n if (pageUrl.origin !== originUrl.origin) {\n return new Response('Invalid origin', { status: 400 });\n }\n\n try {\n // Fetch the page HTML from the local server with a normal User-Agent\n // to avoid infinite rewrite loops\n const res = await fetch(pageUrl.toString(), {\n headers: {\n 'User-Agent': 'AgentSEO-Internal/1.0',\n 'Accept': 'text/html',\n },\n });\n\n if (!res.ok) {\n return new Response('Page not found', { status: 404 });\n }\n\n const html = await res.text();\n\n const result = await transform(html, {\n url: pageUrl.toString(),\n });\n\n return new Response(result.markdown, {\n status: 200,\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n 'Vary': 'Accept, User-Agent',\n 'X-Robots-Tag': 'all',\n 'X-Agent-Seo': 'transformed',\n },\n });\n } catch (err) {\n console.error('[agent-seo] Transform error:', err);\n return new Response('Transform failed', { status: 500 });\n }\n}\n`;\n\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\n/**\n * Write `app/robots.txt` — a static robots.txt that allows all crawlers\n * and references /llms.txt for AI agents.\n * Only created if the file doesn't already exist (won't overwrite user customizations).\n */\nfunction generateRobotsTxt(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const robotsFile = join(appDir, 'robots.txt');\n\n // Don't overwrite user-created robots.txt\n if (existsSync(robotsFile)) {\n return;\n }\n\n let content = `# AI-Optimized robots.txt — Generated by @agent-seo/next\n# For AI agents: see /llms.txt for a structured site manifest\n\nUser-agent: *\nAllow: /\n`;\n\n if (options.sitemap) {\n const sitemapUrl =\n typeof options.sitemap === 'string'\n ? options.sitemap\n : `${options.baseUrl}/sitemap.xml`;\n content += `\\nSitemap: ${sitemapUrl}\\n`;\n }\n\n writeFileSync(robotsFile, content, 'utf-8');\n}\n","import { detectAgent } from '@agent-seo/core/edge';\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport interface AgentSeoMiddlewareOptions {\n /**\n * Glob patterns for paths that should NEVER be rewritten to Markdown.\n * Merged with built-in defaults (see `DEFAULT_EXCLUDE`).\n *\n * @example ['\\/dashboard\\/**', '\\/admin\\/**', '\\/api\\/private\\/**']\n */\n exclude?: string[];\n}\n\n/** Paths that are always skipped — framework internals + standard files. */\nconst ALWAYS_SKIP = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/llms.txt',\n '/llms-full.txt',\n]);\n\n/** Default exclude patterns — users can extend but not remove these. */\nconst DEFAULT_EXCLUDE: string[] = [\n '/api/**',\n '/_next/**',\n];\n\n/**\n * Creates a Next.js middleware that:\n * 1. Detects AI bot requests via User-Agent\n * 2. Rewrites AI bot requests to an internal transform API route\n * that converts HTML → Markdown (runs on Node.js runtime)\n * 3. Handles `.md` suffix requests (e.g. `/about.md` → transform `/about`)\n * 4. Sets `Vary: Accept, User-Agent` on all responses\n */\nexport function createAgentSeoMiddleware(options: AgentSeoMiddlewareOptions = {}) {\n const excludePatterns = [...DEFAULT_EXCLUDE, ...(options.exclude || [])];\n\n return function middleware(request: NextRequest) {\n const { pathname } = request.nextUrl;\n\n // Skip standard files (robots.txt, favicon, llms.txt, etc.)\n if (ALWAYS_SKIP.has(pathname)) {\n return NextResponse.next();\n }\n\n // Skip excluded patterns (API routes, admin, dashboard, etc.)\n if (isExcluded(pathname, excludePatterns)) {\n return NextResponse.next();\n }\n\n const ua = request.headers.get('user-agent');\n const accept = request.headers.get('accept');\n const aiCtx = detectAgent(ua, accept);\n\n // Handle explicit .md suffix requests (e.g. /about.md)\n if (pathname.endsWith('.md')) {\n const originalPath = pathname.slice(0, -3) || '/';\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', originalPath);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // If AI bot, rewrite to transform API\n if (aiCtx.isAIBot) {\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', pathname);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // Normal request — just add Vary header\n const response = NextResponse.next();\n response.headers.set('Vary', 'Accept, User-Agent');\n return response;\n };\n}\n\n/**\n * Set clean, bot-friendly headers on a rewrite response.\n * Overrides Next.js RSC-related headers that can confuse AI bots.\n */\nfunction setBotHeaders(response: NextResponse): NextResponse {\n response.headers.set('Content-Disposition', 'inline');\n response.headers.set('Vary', 'Accept, User-Agent');\n response.headers.set('X-Robots-Tag', 'all');\n response.headers.delete('x-nextjs-matched-path');\n return response;\n}\n\nfunction isExcluded(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchGlob(pattern, path));\n}\n\nfunction matchGlob(pattern: string, path: string): boolean {\n const regex = pattern\n .replace(/\\*\\*/g, '{{DOUBLESTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLESTAR}}/g, '.*');\n return new RegExp(`^${regex}$`).test(path);\n}\n","import { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/core';\nimport type { AgentSeoOptions, DiscoverOptions } from '@agent-seo/core';\n\nexport interface LlmsTxtHandlerOptions extends AgentSeoOptions {\n /** Return llms-full.txt instead of llms.txt */\n full?: boolean;\n\n /**\n * Absolute path to the Next.js `app/` directory for automatic route discovery.\n * When set, routes for llms.txt are auto-discovered by scanning page.tsx files.\n * Titles and descriptions are extracted from `export const metadata` in each page.\n *\n * @example\n * ```ts\n * appDir: path.resolve(process.cwd(), 'app')\n * ```\n */\n appDir?: string;\n\n /** Options for route discovery when using appDir */\n discoverOptions?: DiscoverOptions;\n}\n\nexport function createLlmsTxtHandler(options: LlmsTxtHandlerOptions) {\n return async function GET() {\n // Auto-discover routes from app/ directory if no explicit routes are provided\n let routes = options.llmsTxt?.routes || [];\n\n if (routes.length === 0 && options.appDir) {\n routes = discoverNextRoutes(options.appDir, {\n exclude: options.exclude || ['/api'],\n ...options.discoverOptions,\n });\n }\n\n const result = generateLlmsTxt(\n {\n siteName: options.siteName,\n siteDescription: options.siteDescription,\n baseUrl: options.baseUrl,\n ...options.llmsTxt,\n },\n routes,\n );\n\n const content = options.full ? result.llmsFullTxt : result.llmsTxt;\n\n return new Response(content, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n };\n}\n"],"mappings":"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","names":["import_core"]}
package/dist/index.js CHANGED
@@ -16,8 +16,15 @@ function withAgentSeo(agentSeoOptions) {
16
16
  generateRobotsTxt(appDir, agentSeoOptions);
17
17
  }
18
18
  return (nextConfig = {}) => {
19
+ const existingExternal = nextConfig.serverExternalPackages ?? [];
19
20
  return {
20
21
  ...nextConfig,
22
+ // Prevent bundling jsdom (and its CJS deps) into serverless functions.
23
+ // jsdom must be loaded at runtime to avoid CJS/ESM incompatibilities.
24
+ serverExternalPackages: [
25
+ ...existingExternal,
26
+ ...["jsdom"].filter((pkg) => !existingExternal.includes(pkg))
27
+ ],
21
28
  async headers() {
22
29
  const existingHeaders = await (nextConfig.headers?.() ?? []);
23
30
  return [
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts","../src/middleware.ts","../src/route-handler.ts","../src/index.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { NextConfig } from 'next';\nimport type { AgentSeoOptions } from '@agent-seo/core';\n\nexport interface WithAgentSeoOptions extends AgentSeoOptions {\n /**\n * Absolute path to the Next.js `app/` directory.\n * If omitted, auto-detected by checking `./app` and `./src/app` from cwd.\n */\n appDir?: string;\n /**\n * Sitemap URL for the `robots.txt` Sitemap directive.\n * - `true`: uses `{baseUrl}/sitemap.xml`\n * - `string`: uses the provided URL\n * - `undefined`/`false`: no Sitemap directive\n */\n sitemap?: boolean | string;\n}\n\n/**\n * Next.js config plugin that:\n * 1. Auto-generates `app/llms.txt/route.ts` (zero-config `/llms.txt` endpoint)\n * 2. Injects `Vary: Accept, User-Agent` headers on all pages\n *\n * @example\n * ```ts\n * // next.config.ts\n * import { withAgentSeo } from '@agent-seo/next';\n *\n * export default withAgentSeo({\n * siteName: 'My App',\n * siteDescription: 'A brief description for LLMs.',\n * baseUrl: 'https://myapp.com',\n * })({});\n * ```\n */\nexport function withAgentSeo(agentSeoOptions: WithAgentSeoOptions) {\n const appDir = agentSeoOptions.appDir || detectAppDir();\n\n // Auto-generate route handlers at config-evaluation time\n if (appDir) {\n generateLlmsTxtRoute(appDir, agentSeoOptions);\n generateTransformRoute(appDir, agentSeoOptions);\n generateRobotsTxt(appDir, agentSeoOptions);\n }\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n return {\n ...nextConfig,\n\n async headers() {\n const existingHeaders = await (nextConfig.headers?.() ?? []);\n return [\n ...existingHeaders,\n {\n source: '/((?!api|_next|static|favicon.ico).*)',\n headers: [{ key: 'Vary', value: 'Accept, User-Agent' }],\n },\n ];\n },\n };\n };\n}\n\n/**\n * Auto-detect the Next.js `app/` directory from common locations.\n */\nfunction detectAppDir(): string | null {\n const cwd = process.cwd();\n const candidates = [join(cwd, 'app'), join(cwd, 'src', 'app')];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n// Auto-generated file banner\nconst GENERATED_BANNER = `// AUTO-GENERATED by @agent-seo/next — do not edit manually.\n// This file is created by withAgentSeo() in next.config.ts.\n// Add \"app/llms.txt\" to your .gitignore.\n`;\n\n/**\n * Write `app/llms.txt/route.js` with a handler that auto-discovers routes.\n */\nfunction generateLlmsTxtRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'llms.txt');\n const routeFile = join(routeDir, 'route.js');\n\n // Serialize the options we need into the generated file\n const siteName = escapeStr(options.siteName);\n const siteDescription = escapeStr(options.siteDescription);\n const baseUrl = escapeStr(options.baseUrl);\n const excludePatterns = JSON.stringify(options.exclude || ['/api']);\n\n const content = `${GENERATED_BANNER}\nimport { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/next';\nimport path from 'node:path';\n\nconst appDir = path.resolve(process.cwd(), 'app');\n\nexport async function GET(request) {\n const url = new URL(request.url);\n\n const routes = discoverNextRoutes(appDir, {\n exclude: ${excludePatterns},\n });\n\n const result = generateLlmsTxt(\n {\n siteName: '${siteName}',\n siteDescription: '${siteDescription}',\n baseUrl: process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}',\n },\n routes,\n );\n\n return new Response(result.llmsTxt, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n}\n`;\n\n // Only write if the file doesn't exist or content has changed\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\nfunction escapeStr(s: string): string {\n return s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\n/**\n * Write `app/api/agent-seo-transform/route.js` — a Node.js API route that\n * receives a `?path=/some-page` param, fetches the HTML from the local\n * Next.js server, runs the core `transform()` pipeline (JSDOM → Readability\n * → Turndown), and returns clean Markdown.\n */\nfunction generateTransformRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'api', 'agent-seo-transform');\n const routeFile = join(routeDir, 'route.js');\n\n const baseUrl = escapeStr(options.baseUrl);\n\n const content = `${GENERATED_BANNER}\nimport { transform } from '@agent-seo/next';\n\n// Force Node.js runtime (required for jsdom / Readability / Turndown)\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\nexport async function GET(request) {\n const { searchParams } = new URL(request.url);\n const pagePath = searchParams.get('path') || '/';\n\n // Build the internal URL to fetch the original HTML page\n const origin = process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}';\n if (!origin) {\n return new Response('Base URL not configured', { status: 500 });\n }\n\n // Only allow same-origin, absolute paths (prevent SSRF via absolute or protocol-relative URLs)\n const isValidPath =\n pagePath.startsWith('/') &&\n !pagePath.startsWith('//') &&\n !pagePath.includes('://') &&\n !pagePath.includes('\\\\\\\\');\n if (!isValidPath) {\n return new Response('Invalid path', { status: 400 });\n }\n\n const originUrl = new URL(origin);\n if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {\n return new Response('Invalid base URL', { status: 500 });\n }\n const pageUrl = new URL(pagePath, originUrl.origin);\n if (pageUrl.origin !== originUrl.origin) {\n return new Response('Invalid origin', { status: 400 });\n }\n\n try {\n // Fetch the page HTML from the local server with a normal User-Agent\n // to avoid infinite rewrite loops\n const res = await fetch(pageUrl.toString(), {\n headers: {\n 'User-Agent': 'AgentSEO-Internal/1.0',\n 'Accept': 'text/html',\n },\n });\n\n if (!res.ok) {\n return new Response('Page not found', { status: 404 });\n }\n\n const html = await res.text();\n\n const result = await transform(html, {\n url: pageUrl.toString(),\n });\n\n return new Response(result.markdown, {\n status: 200,\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n 'Vary': 'Accept, User-Agent',\n 'X-Robots-Tag': 'all',\n 'X-Agent-Seo': 'transformed',\n },\n });\n } catch (err) {\n console.error('[agent-seo] Transform error:', err);\n return new Response('Transform failed', { status: 500 });\n }\n}\n`;\n\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\n/**\n * Write `app/robots.txt` — a static robots.txt that allows all crawlers\n * and references /llms.txt for AI agents.\n * Only created if the file doesn't already exist (won't overwrite user customizations).\n */\nfunction generateRobotsTxt(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const robotsFile = join(appDir, 'robots.txt');\n\n // Don't overwrite user-created robots.txt\n if (existsSync(robotsFile)) {\n return;\n }\n\n let content = `# AI-Optimized robots.txt — Generated by @agent-seo/next\n# For AI agents: see /llms.txt for a structured site manifest\n\nUser-agent: *\nAllow: /\n`;\n\n if (options.sitemap) {\n const sitemapUrl =\n typeof options.sitemap === 'string'\n ? options.sitemap\n : `${options.baseUrl}/sitemap.xml`;\n content += `\\nSitemap: ${sitemapUrl}\\n`;\n }\n\n writeFileSync(robotsFile, content, 'utf-8');\n}\n","import { detectAgent } from '@agent-seo/core/edge';\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport interface AgentSeoMiddlewareOptions {\n /**\n * Glob patterns for paths that should NEVER be rewritten to Markdown.\n * Merged with built-in defaults (see `DEFAULT_EXCLUDE`).\n *\n * @example ['\\/dashboard\\/**', '\\/admin\\/**', '\\/api\\/private\\/**']\n */\n exclude?: string[];\n}\n\n/** Paths that are always skipped — framework internals + standard files. */\nconst ALWAYS_SKIP = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/llms.txt',\n '/llms-full.txt',\n]);\n\n/** Default exclude patterns — users can extend but not remove these. */\nconst DEFAULT_EXCLUDE: string[] = [\n '/api/**',\n '/_next/**',\n];\n\n/**\n * Creates a Next.js middleware that:\n * 1. Detects AI bot requests via User-Agent\n * 2. Rewrites AI bot requests to an internal transform API route\n * that converts HTML → Markdown (runs on Node.js runtime)\n * 3. Handles `.md` suffix requests (e.g. `/about.md` → transform `/about`)\n * 4. Sets `Vary: Accept, User-Agent` on all responses\n */\nexport function createAgentSeoMiddleware(options: AgentSeoMiddlewareOptions = {}) {\n const excludePatterns = [...DEFAULT_EXCLUDE, ...(options.exclude || [])];\n\n return function middleware(request: NextRequest) {\n const { pathname } = request.nextUrl;\n\n // Skip standard files (robots.txt, favicon, llms.txt, etc.)\n if (ALWAYS_SKIP.has(pathname)) {\n return NextResponse.next();\n }\n\n // Skip excluded patterns (API routes, admin, dashboard, etc.)\n if (isExcluded(pathname, excludePatterns)) {\n return NextResponse.next();\n }\n\n const ua = request.headers.get('user-agent');\n const accept = request.headers.get('accept');\n const aiCtx = detectAgent(ua, accept);\n\n // Handle explicit .md suffix requests (e.g. /about.md)\n if (pathname.endsWith('.md')) {\n const originalPath = pathname.slice(0, -3) || '/';\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', originalPath);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // If AI bot, rewrite to transform API\n if (aiCtx.isAIBot) {\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', pathname);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // Normal request — just add Vary header\n const response = NextResponse.next();\n response.headers.set('Vary', 'Accept, User-Agent');\n return response;\n };\n}\n\n/**\n * Set clean, bot-friendly headers on a rewrite response.\n * Overrides Next.js RSC-related headers that can confuse AI bots.\n */\nfunction setBotHeaders(response: NextResponse): NextResponse {\n response.headers.set('Content-Disposition', 'inline');\n response.headers.set('Vary', 'Accept, User-Agent');\n response.headers.set('X-Robots-Tag', 'all');\n response.headers.delete('x-nextjs-matched-path');\n return response;\n}\n\nfunction isExcluded(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchGlob(pattern, path));\n}\n\nfunction matchGlob(pattern: string, path: string): boolean {\n const regex = pattern\n .replace(/\\*\\*/g, '{{DOUBLESTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLESTAR}}/g, '.*');\n return new RegExp(`^${regex}$`).test(path);\n}\n","import { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/core';\nimport type { AgentSeoOptions, DiscoverOptions } from '@agent-seo/core';\n\nexport interface LlmsTxtHandlerOptions extends AgentSeoOptions {\n /** Return llms-full.txt instead of llms.txt */\n full?: boolean;\n\n /**\n * Absolute path to the Next.js `app/` directory for automatic route discovery.\n * When set, routes for llms.txt are auto-discovered by scanning page.tsx files.\n * Titles and descriptions are extracted from `export const metadata` in each page.\n *\n * @example\n * ```ts\n * appDir: path.resolve(process.cwd(), 'app')\n * ```\n */\n appDir?: string;\n\n /** Options for route discovery when using appDir */\n discoverOptions?: DiscoverOptions;\n}\n\nexport function createLlmsTxtHandler(options: LlmsTxtHandlerOptions) {\n return async function GET() {\n // Auto-discover routes from app/ directory if no explicit routes are provided\n let routes = options.llmsTxt?.routes || [];\n\n if (routes.length === 0 && options.appDir) {\n routes = discoverNextRoutes(options.appDir, {\n exclude: options.exclude || ['/api'],\n ...options.discoverOptions,\n });\n }\n\n const result = generateLlmsTxt(\n {\n siteName: options.siteName,\n siteDescription: options.siteDescription,\n baseUrl: options.baseUrl,\n ...options.llmsTxt,\n },\n routes,\n );\n\n const content = options.full ? result.llmsFullTxt : result.llmsTxt;\n\n return new Response(content, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n };\n}\n","export { withAgentSeo } from './plugin.js';\nexport type { WithAgentSeoOptions } from './plugin.js';\nexport { createAgentSeoMiddleware } from './middleware.js';\nexport type { AgentSeoMiddlewareOptions } from './middleware.js';\nexport { createLlmsTxtHandler } from './route-handler.js';\nexport type { LlmsTxtHandlerOptions } from './route-handler.js';\nexport {\n generateLlmsTxt,\n discoverNextRoutes,\n transform,\n} from '@agent-seo/core';\nexport type {\n AgentSeoOptions,\n AIRequestContext,\n TransformResult,\n BotInfo,\n BotPurpose,\n LlmsTxtRoute,\n DiscoverOptions,\n} from '@agent-seo/core';\n"],"mappings":"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","names":["generateLlmsTxt","discoverNextRoutes"]}
1
+ {"version":3,"sources":["../src/plugin.ts","../src/middleware.ts","../src/route-handler.ts","../src/index.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { NextConfig } from 'next';\nimport type { AgentSeoOptions } from '@agent-seo/core';\n\nexport interface WithAgentSeoOptions extends AgentSeoOptions {\n /**\n * Absolute path to the Next.js `app/` directory.\n * If omitted, auto-detected by checking `./app` and `./src/app` from cwd.\n */\n appDir?: string;\n /**\n * Sitemap URL for the `robots.txt` Sitemap directive.\n * - `true`: uses `{baseUrl}/sitemap.xml`\n * - `string`: uses the provided URL\n * - `undefined`/`false`: no Sitemap directive\n */\n sitemap?: boolean | string;\n}\n\n/**\n * Next.js config plugin that:\n * 1. Auto-generates `app/llms.txt/route.ts` (zero-config `/llms.txt` endpoint)\n * 2. Injects `Vary: Accept, User-Agent` headers on all pages\n *\n * @example\n * ```ts\n * // next.config.ts\n * import { withAgentSeo } from '@agent-seo/next';\n *\n * export default withAgentSeo({\n * siteName: 'My App',\n * siteDescription: 'A brief description for LLMs.',\n * baseUrl: 'https://myapp.com',\n * })({});\n * ```\n */\nexport function withAgentSeo(agentSeoOptions: WithAgentSeoOptions) {\n const appDir = agentSeoOptions.appDir || detectAppDir();\n\n // Auto-generate route handlers at config-evaluation time\n if (appDir) {\n generateLlmsTxtRoute(appDir, agentSeoOptions);\n generateTransformRoute(appDir, agentSeoOptions);\n generateRobotsTxt(appDir, agentSeoOptions);\n }\n\n return (nextConfig: NextConfig = {}): NextConfig => {\n const existingExternal = nextConfig.serverExternalPackages ?? [];\n\n return {\n ...nextConfig,\n\n // Prevent bundling jsdom (and its CJS deps) into serverless functions.\n // jsdom must be loaded at runtime to avoid CJS/ESM incompatibilities.\n serverExternalPackages: [\n ...existingExternal,\n ...['jsdom'].filter((pkg) => !existingExternal.includes(pkg)),\n ],\n\n async headers() {\n const existingHeaders = await (nextConfig.headers?.() ?? []);\n return [\n ...existingHeaders,\n {\n source: '/((?!api|_next|static|favicon.ico).*)',\n headers: [{ key: 'Vary', value: 'Accept, User-Agent' }],\n },\n ];\n },\n };\n };\n}\n\n/**\n * Auto-detect the Next.js `app/` directory from common locations.\n */\nfunction detectAppDir(): string | null {\n const cwd = process.cwd();\n const candidates = [join(cwd, 'app'), join(cwd, 'src', 'app')];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n// Auto-generated file banner\nconst GENERATED_BANNER = `// AUTO-GENERATED by @agent-seo/next — do not edit manually.\n// This file is created by withAgentSeo() in next.config.ts.\n// Add \"app/llms.txt\" to your .gitignore.\n`;\n\n/**\n * Write `app/llms.txt/route.js` with a handler that auto-discovers routes.\n */\nfunction generateLlmsTxtRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'llms.txt');\n const routeFile = join(routeDir, 'route.js');\n\n // Serialize the options we need into the generated file\n const siteName = escapeStr(options.siteName);\n const siteDescription = escapeStr(options.siteDescription);\n const baseUrl = escapeStr(options.baseUrl);\n const excludePatterns = JSON.stringify(options.exclude || ['/api']);\n\n const content = `${GENERATED_BANNER}\nimport { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/next';\nimport path from 'node:path';\n\nconst appDir = path.resolve(process.cwd(), 'app');\n\nexport async function GET(request) {\n const url = new URL(request.url);\n\n const routes = discoverNextRoutes(appDir, {\n exclude: ${excludePatterns},\n });\n\n const result = generateLlmsTxt(\n {\n siteName: '${siteName}',\n siteDescription: '${siteDescription}',\n baseUrl: process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}',\n },\n routes,\n );\n\n return new Response(result.llmsTxt, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n}\n`;\n\n // Only write if the file doesn't exist or content has changed\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\nfunction escapeStr(s: string): string {\n return s.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\n/**\n * Write `app/api/agent-seo-transform/route.js` — a Node.js API route that\n * receives a `?path=/some-page` param, fetches the HTML from the local\n * Next.js server, runs the core `transform()` pipeline (JSDOM → Readability\n * → Turndown), and returns clean Markdown.\n */\nfunction generateTransformRoute(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const routeDir = join(appDir, 'api', 'agent-seo-transform');\n const routeFile = join(routeDir, 'route.js');\n\n const baseUrl = escapeStr(options.baseUrl);\n\n const content = `${GENERATED_BANNER}\nimport { transform } from '@agent-seo/next';\n\n// Force Node.js runtime (required for jsdom / Readability / Turndown)\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\nexport async function GET(request) {\n const { searchParams } = new URL(request.url);\n const pagePath = searchParams.get('path') || '/';\n\n // Build the internal URL to fetch the original HTML page\n const origin = process.env.NEXT_PUBLIC_BASE_URL || '${baseUrl}';\n if (!origin) {\n return new Response('Base URL not configured', { status: 500 });\n }\n\n // Only allow same-origin, absolute paths (prevent SSRF via absolute or protocol-relative URLs)\n const isValidPath =\n pagePath.startsWith('/') &&\n !pagePath.startsWith('//') &&\n !pagePath.includes('://') &&\n !pagePath.includes('\\\\\\\\');\n if (!isValidPath) {\n return new Response('Invalid path', { status: 400 });\n }\n\n const originUrl = new URL(origin);\n if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {\n return new Response('Invalid base URL', { status: 500 });\n }\n const pageUrl = new URL(pagePath, originUrl.origin);\n if (pageUrl.origin !== originUrl.origin) {\n return new Response('Invalid origin', { status: 400 });\n }\n\n try {\n // Fetch the page HTML from the local server with a normal User-Agent\n // to avoid infinite rewrite loops\n const res = await fetch(pageUrl.toString(), {\n headers: {\n 'User-Agent': 'AgentSEO-Internal/1.0',\n 'Accept': 'text/html',\n },\n });\n\n if (!res.ok) {\n return new Response('Page not found', { status: 404 });\n }\n\n const html = await res.text();\n\n const result = await transform(html, {\n url: pageUrl.toString(),\n });\n\n return new Response(result.markdown, {\n status: 200,\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Content-Disposition': 'inline',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n 'Vary': 'Accept, User-Agent',\n 'X-Robots-Tag': 'all',\n 'X-Agent-Seo': 'transformed',\n },\n });\n } catch (err) {\n console.error('[agent-seo] Transform error:', err);\n return new Response('Transform failed', { status: 500 });\n }\n}\n`;\n\n mkdirSync(routeDir, { recursive: true });\n\n let existingContent = '';\n try {\n const { readFileSync } = require('node:fs');\n existingContent = readFileSync(routeFile, 'utf-8');\n } catch {\n // File doesn't exist yet\n }\n\n if (existingContent !== content) {\n writeFileSync(routeFile, content, 'utf-8');\n }\n}\n\n/**\n * Write `app/robots.txt` — a static robots.txt that allows all crawlers\n * and references /llms.txt for AI agents.\n * Only created if the file doesn't already exist (won't overwrite user customizations).\n */\nfunction generateRobotsTxt(\n appDir: string,\n options: WithAgentSeoOptions,\n): void {\n const robotsFile = join(appDir, 'robots.txt');\n\n // Don't overwrite user-created robots.txt\n if (existsSync(robotsFile)) {\n return;\n }\n\n let content = `# AI-Optimized robots.txt — Generated by @agent-seo/next\n# For AI agents: see /llms.txt for a structured site manifest\n\nUser-agent: *\nAllow: /\n`;\n\n if (options.sitemap) {\n const sitemapUrl =\n typeof options.sitemap === 'string'\n ? options.sitemap\n : `${options.baseUrl}/sitemap.xml`;\n content += `\\nSitemap: ${sitemapUrl}\\n`;\n }\n\n writeFileSync(robotsFile, content, 'utf-8');\n}\n","import { detectAgent } from '@agent-seo/core/edge';\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport interface AgentSeoMiddlewareOptions {\n /**\n * Glob patterns for paths that should NEVER be rewritten to Markdown.\n * Merged with built-in defaults (see `DEFAULT_EXCLUDE`).\n *\n * @example ['\\/dashboard\\/**', '\\/admin\\/**', '\\/api\\/private\\/**']\n */\n exclude?: string[];\n}\n\n/** Paths that are always skipped — framework internals + standard files. */\nconst ALWAYS_SKIP = new Set([\n '/favicon.ico',\n '/robots.txt',\n '/sitemap.xml',\n '/llms.txt',\n '/llms-full.txt',\n]);\n\n/** Default exclude patterns — users can extend but not remove these. */\nconst DEFAULT_EXCLUDE: string[] = [\n '/api/**',\n '/_next/**',\n];\n\n/**\n * Creates a Next.js middleware that:\n * 1. Detects AI bot requests via User-Agent\n * 2. Rewrites AI bot requests to an internal transform API route\n * that converts HTML → Markdown (runs on Node.js runtime)\n * 3. Handles `.md` suffix requests (e.g. `/about.md` → transform `/about`)\n * 4. Sets `Vary: Accept, User-Agent` on all responses\n */\nexport function createAgentSeoMiddleware(options: AgentSeoMiddlewareOptions = {}) {\n const excludePatterns = [...DEFAULT_EXCLUDE, ...(options.exclude || [])];\n\n return function middleware(request: NextRequest) {\n const { pathname } = request.nextUrl;\n\n // Skip standard files (robots.txt, favicon, llms.txt, etc.)\n if (ALWAYS_SKIP.has(pathname)) {\n return NextResponse.next();\n }\n\n // Skip excluded patterns (API routes, admin, dashboard, etc.)\n if (isExcluded(pathname, excludePatterns)) {\n return NextResponse.next();\n }\n\n const ua = request.headers.get('user-agent');\n const accept = request.headers.get('accept');\n const aiCtx = detectAgent(ua, accept);\n\n // Handle explicit .md suffix requests (e.g. /about.md)\n if (pathname.endsWith('.md')) {\n const originalPath = pathname.slice(0, -3) || '/';\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', originalPath);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // If AI bot, rewrite to transform API\n if (aiCtx.isAIBot) {\n const transformUrl = new URL('/api/agent-seo-transform', request.url);\n transformUrl.searchParams.set('path', pathname);\n return setBotHeaders(NextResponse.rewrite(transformUrl));\n }\n\n // Normal request — just add Vary header\n const response = NextResponse.next();\n response.headers.set('Vary', 'Accept, User-Agent');\n return response;\n };\n}\n\n/**\n * Set clean, bot-friendly headers on a rewrite response.\n * Overrides Next.js RSC-related headers that can confuse AI bots.\n */\nfunction setBotHeaders(response: NextResponse): NextResponse {\n response.headers.set('Content-Disposition', 'inline');\n response.headers.set('Vary', 'Accept, User-Agent');\n response.headers.set('X-Robots-Tag', 'all');\n response.headers.delete('x-nextjs-matched-path');\n return response;\n}\n\nfunction isExcluded(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchGlob(pattern, path));\n}\n\nfunction matchGlob(pattern: string, path: string): boolean {\n const regex = pattern\n .replace(/\\*\\*/g, '{{DOUBLESTAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLESTAR}}/g, '.*');\n return new RegExp(`^${regex}$`).test(path);\n}\n","import { generateLlmsTxt, discoverNextRoutes } from '@agent-seo/core';\nimport type { AgentSeoOptions, DiscoverOptions } from '@agent-seo/core';\n\nexport interface LlmsTxtHandlerOptions extends AgentSeoOptions {\n /** Return llms-full.txt instead of llms.txt */\n full?: boolean;\n\n /**\n * Absolute path to the Next.js `app/` directory for automatic route discovery.\n * When set, routes for llms.txt are auto-discovered by scanning page.tsx files.\n * Titles and descriptions are extracted from `export const metadata` in each page.\n *\n * @example\n * ```ts\n * appDir: path.resolve(process.cwd(), 'app')\n * ```\n */\n appDir?: string;\n\n /** Options for route discovery when using appDir */\n discoverOptions?: DiscoverOptions;\n}\n\nexport function createLlmsTxtHandler(options: LlmsTxtHandlerOptions) {\n return async function GET() {\n // Auto-discover routes from app/ directory if no explicit routes are provided\n let routes = options.llmsTxt?.routes || [];\n\n if (routes.length === 0 && options.appDir) {\n routes = discoverNextRoutes(options.appDir, {\n exclude: options.exclude || ['/api'],\n ...options.discoverOptions,\n });\n }\n\n const result = generateLlmsTxt(\n {\n siteName: options.siteName,\n siteDescription: options.siteDescription,\n baseUrl: options.baseUrl,\n ...options.llmsTxt,\n },\n routes,\n );\n\n const content = options.full ? result.llmsFullTxt : result.llmsTxt;\n\n return new Response(content, {\n status: 200,\n headers: {\n 'Content-Type': 'text/plain; charset=utf-8',\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n },\n });\n };\n}\n","export { withAgentSeo } from './plugin.js';\nexport type { WithAgentSeoOptions } from './plugin.js';\nexport { createAgentSeoMiddleware } from './middleware.js';\nexport type { AgentSeoMiddlewareOptions } from './middleware.js';\nexport { createLlmsTxtHandler } from './route-handler.js';\nexport type { LlmsTxtHandlerOptions } from './route-handler.js';\nexport {\n generateLlmsTxt,\n discoverNextRoutes,\n transform,\n} from '@agent-seo/core';\nexport type {\n AgentSeoOptions,\n AIRequestContext,\n TransformResult,\n BotInfo,\n BotPurpose,\n LlmsTxtRoute,\n DiscoverOptions,\n} from '@agent-seo/core';\n"],"mappings":"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","names":["generateLlmsTxt","discoverNextRoutes"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-seo/next",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Next.js plugin for AI-readable websites",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,7 +54,7 @@
54
54
  "dist"
55
55
  ],
56
56
  "dependencies": {
57
- "@agent-seo/core": "1.0.0"
57
+ "@agent-seo/core": "1.0.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0"