@ontosdk/next 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/JSON_LD_GUIDE.md +403 -0
- package/LLMS_TXT_GUIDE.md +297 -0
- package/ONTOROVIDER_USAGE.md +89 -0
- package/QUICKSTART.md +71 -0
- package/README.md +253 -0
- package/SCHEMA_QUICKSTART.md +97 -0
- package/dist/OntoHead.d.mts +27 -0
- package/dist/OntoHead.d.ts +27 -0
- package/dist/OntoHead.js +2 -0
- package/dist/OntoHead.js.map +1 -0
- package/dist/OntoHead.mjs +2 -0
- package/dist/OntoHead.mjs.map +1 -0
- package/dist/OntoProvider.d.mts +52 -0
- package/dist/OntoProvider.d.ts +52 -0
- package/dist/OntoProvider.js +2 -0
- package/dist/OntoProvider.js.map +1 -0
- package/dist/OntoProvider.mjs +2 -0
- package/dist/OntoProvider.mjs.map +1 -0
- package/dist/config.d.mts +85 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -0
- package/dist/config.mjs +4 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -4
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +3 -2
- package/dist/middleware.d.ts +3 -2
- package/dist/middleware.js +4 -2
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +4 -2
- package/dist/middleware.mjs.map +1 -1
- package/dist/schemas.d.mts +73 -0
- package/dist/schemas.d.ts +73 -0
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -0
- package/dist/schemas.mjs +2 -0
- package/dist/schemas.mjs.map +1 -0
- package/onto.config.example.ts +111 -0
- package/package.json +28 -2
- package/src/OntoHead.tsx +59 -0
- package/src/OntoProvider.tsx +95 -0
- package/src/bots.ts +6 -3
- package/src/config.ts +151 -0
- package/src/index.ts +14 -0
- package/src/middleware.ts +62 -12
- package/src/schemas.ts +186 -0
- package/tsconfig.json +1 -0
- package/tsup.config.ts +2 -2
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/bots.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\r\nimport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\n\r\nexport async function ontoMiddleware(request: NextRequest) {\r\n const userAgent = request.headers.get('user-agent') || '';\r\n const accept = request.headers.get('accept') || '';\r\n\r\n const matched = matchBot(userAgent);\r\n const isAiBot = !!matched;\r\n const isMarkdownRequested = accept.includes('text/markdown');\r\n\r\n // If traffic is identified as an AI Bot, rewrite the URL\r\n if (isAiBot || isMarkdownRequested) {\r\n const url = request.nextUrl.clone();\r\n\r\n // Ignore internal next.js requests & static assets\r\n if (url.pathname.startsWith('/_next') || url.pathname.includes('.')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Determine the corresponding payload path\r\n let payloadPath = url.pathname;\r\n if (payloadPath === '/' || payloadPath === '') {\r\n payloadPath = '/index';\r\n }\r\n\r\n // Strip trailing slash if present\r\n if (payloadPath.endsWith('/') && payloadPath !== '/') {\r\n payloadPath = payloadPath.slice(0, -1);\r\n }\r\n\r\n // Common response headers for all bot responses\r\n const botHeaders: Record<string, string> = {\r\n 'Content-Type': 'text/markdown; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n };\r\n if (matched) {\r\n botHeaders['X-Onto-Bot'] = `${matched.name} (${matched.company})`;\r\n }\r\n\r\n // --- Onto Control Plane Integration (Premium) ---\r\n const ONTO_API_KEY = process.env.ONTO_API_KEY;\r\n const DASHBOARD_URL = process.env.ONTO_DASHBOARD_URL || 'https://app.buildonto.dev';\r\n\r\n if (ONTO_API_KEY) {\r\n // 1. Fire-and-forget tracking — includes structured bot info\r\n fetch(`${DASHBOARD_URL}/api/track`, {\r\n method: 'POST',\r\n headers: {\r\n 'x-onto-key': ONTO_API_KEY,\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({\r\n route: url.pathname,\r\n userAgent: userAgent,\r\n bot: matched ? matched.name : null,\r\n company: matched ? matched.company : null,\r\n })\r\n }).catch(() => {});\r\n\r\n // 2. Dynamic Context Injection\r\n try {\r\n // Fetch the injection from the Control Plane\r\n const injectRes = await fetch(`${DASHBOARD_URL}/api/sdk/inject?route=${url.pathname}`, {\r\n headers: { 'x-onto-key': ONTO_API_KEY },\r\n // Set a strict timeout to keep edge fast\r\n signal: AbortSignal.timeout(1500)\r\n });\r\n\r\n if (injectRes.ok) {\r\n const { injection } = await injectRes.json();\r\n \r\n if (injection) {\r\n // To inject, we must fetch the local markdown and append\r\n const localMdUrl = `${url.origin}/.onto${payloadPath}.md`;\r\n const mdRes = await fetch(localMdUrl);\r\n \r\n if (mdRes.ok) {\r\n const baseMarkdown = await mdRes.text();\r\n const finalMarkdown = `${baseMarkdown}\\n\\n---\\n\\n${injection}`;\r\n \r\n return new NextResponse(finalMarkdown, {\r\n headers: {\r\n ...botHeaders,\r\n 'X-Onto-Injected': 'true'\r\n }\r\n });\r\n }\r\n }\r\n }\r\n } catch (err) {\r\n console.error('[Onto] Injection failed', err);\r\n }\r\n }\r\n // ------------------------------------------------\r\n\r\n url.pathname = `/.onto${payloadPath}.md`;\r\n\r\n // Rewrite implicitly serves the target URL transparently to the client.\r\n const response = NextResponse.rewrite(url);\r\n // Attach bot identification headers to the rewrite response\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n\r\n return NextResponse.next();\r\n}\r\n\r\n// Re-export the bot registry for consumers who want to extend or inspect it\r\nexport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\nexport type { AiBot } from './bots';\r\n\r\n","/**\n * Comprehensive registry of AI bot user-agent strings.\n * The middleware uses this list to detect AI crawlers and serve optimized markdown.\n */\n\nexport interface AiBot {\n /** The user-agent substring to match against */\n name: string;\n /** The company operating this bot */\n company: string;\n}\n\n/**\n * Structured registry of all known AI bots, grouped by company.\n * Useful for analytics and the Control Plane dashboard.\n */\nexport const AI_BOTS: AiBot[] = [\n // OpenAI\n { name: 'GPTBot', company: 'OpenAI' },\n { name: 'ChatGPT-User', company: 'OpenAI' },\n { name: 'OAI-SearchBot', company: 'OpenAI' },\n\n // Google\n { name: 'Googlebot', company: 'Google' },\n { name: 'Google-CloudVertexBot', company: 'Google' },\n { name: 'Google-Extended', company: 'Google' },\n { name: 'GoogleOther', company: 'Google' },\n\n // Anthropic\n { name: 'ClaudeBot', company: 'Anthropic' },\n { name: 'Claude-User', company: 'Anthropic' },\n { name: 'anthropic-ai', company: 'Anthropic' },\n\n // Perplexity\n { name: 'PerplexityBot', company: 'Perplexity' },\n { name: 'Perplexity-User', company: 'Perplexity' },\n\n // Meta\n { name: 'Meta-ExternalAgent', company: 'Meta' },\n { name: 'Meta-ExternalFetcher', company: 'Meta' },\n { name: 'FacebookBot', company: 'Meta' },\n\n // Common Crawl (used by most smaller AI companies)\n { name: 'CCBot', company: 'Common Crawl' },\n\n // Other notable AI crawlers\n { name: 'Bytespider', company: 'ByteDance' },\n { name: 'Applebot-Extended', company: 'Apple' },\n { name: 'cohere-ai', company: 'Cohere' },\n { name: 'YouBot', company: 'You.com' },\n];\n\n/**\n * Flat list of user-agent substrings for fast matching in the middleware.\n */\nexport const AI_BOT_USER_AGENTS: string[] = AI_BOTS.map(bot => bot.name);\n\n/**\n * Given a raw user-agent string, returns the matched AiBot entry or undefined.\n * Comparison is case-insensitive to handle inconsistent agent casing.\n */\nexport function matchBot(userAgent: string): AiBot | undefined {\n const ua = userAgent.toLowerCase();\n return AI_BOTS.find(bot => ua.includes(bot.name.toLowerCase()));\n}\n"],"mappings":"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","names":["middleware_exports","__export","AI_BOT_USER_AGENTS","matchBot","ontoMiddleware","__toCommonJS","import_server","AI_BOTS","AI_BOT_USER_AGENTS","bot","matchBot","userAgent","ua","ontoMiddleware","request","userAgent","accept","matched","matchBot","isAiBot","isMarkdownRequested","url","payloadPath","botHeaders","ONTO_API_KEY","DASHBOARD_URL","injectRes","injection","localMdUrl","mdRes","finalMarkdown","err","response"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/bots.ts","../src/config.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\r\nimport { matchBot } from './bots';\r\nimport { loadOntoConfig, generateLlmsTxt, OntoConfig } from './config';\r\n\r\nexport async function ontoMiddleware(request: NextRequest, config?: OntoConfig) {\r\n const userAgent = request.headers.get('user-agent');\r\n const url = request.nextUrl.clone();\r\n const matched = matchBot(userAgent);\r\n\r\n const accept = request.headers.get('accept') || '';\r\n\r\n const isAiBot = !!matched;\r\n const isMarkdownRequested = accept.includes('text/markdown');\r\n\r\n // If traffic is identified as an AI Bot or markdown is requested\r\n if (isAiBot || isMarkdownRequested) {\r\n\r\n // Ignore internal next.js requests & static assets (but not llms.txt)\r\n if (url.pathname.startsWith('/_next')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // --- llms.txt Auto-Discovery ---\r\n // Serve the llms.txt manifest to AI agents\r\n if (url.pathname === '/llms.txt') {\r\n try {\r\n // Use provided config or try to load dynamic one\r\n const activeConfig = config || await loadOntoConfig();\r\n\r\n if (activeConfig) {\r\n // Generate llms.txt dynamically from config\r\n const llmsTxtContent = generateLlmsTxt(activeConfig);\r\n const response = new NextResponse(llmsTxtContent, {\r\n headers: {\r\n 'Content-Type': 'text/plain; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n }\r\n });\r\n\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n\r\n return response;\r\n } else {\r\n // Fallback: try to serve static llms.txt from public folder\r\n url.pathname = '/llms.txt';\r\n const response = NextResponse.rewrite(url);\r\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\r\n response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400');\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n } catch (error) {\r\n console.error('[Onto] Failed to generate llms.txt:', error);\r\n // Fallback to static file on error\r\n url.pathname = '/llms.txt';\r\n const response = NextResponse.rewrite(url);\r\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n }\r\n\r\n // Skip other static assets\r\n if (url.pathname.includes('.')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Determine the corresponding payload path\r\n let payloadPath = url.pathname;\r\n if (payloadPath === '/' || payloadPath === '') {\r\n payloadPath = '/index';\r\n }\r\n\r\n // Strip trailing slash if present\r\n if (payloadPath.endsWith('/') && payloadPath !== '/') {\r\n payloadPath = payloadPath.slice(0, -1);\r\n }\r\n\r\n // Common response headers for all bot responses\r\n const botHeaders: Record<string, string> = {\r\n 'Content-Type': 'text/markdown; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n };\r\n if (matched) {\r\n botHeaders['X-Onto-Bot'] = `${matched.name} (${matched.company})`;\r\n }\r\n\r\n // --- Onto Control Plane Integration (Premium) ---\r\n const ONTO_API_KEY = process.env.ONTO_API_KEY;\r\n const DASHBOARD_URL = process.env.ONTO_DASHBOARD_URL || 'https://app.buildonto.dev';\r\n\r\n if (ONTO_API_KEY) {\r\n // 1. Fire-and-forget tracking — includes structured bot info\r\n fetch(`${DASHBOARD_URL}/api/track`, {\r\n method: 'POST',\r\n headers: {\r\n 'x-onto-key': ONTO_API_KEY,\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({\r\n route: url.pathname,\r\n userAgent: userAgent,\r\n bot: matched ? matched.name : null,\r\n company: matched ? matched.company : null,\r\n })\r\n }).catch(() => {});\r\n\r\n // 2. Dynamic Context Injection\r\n try {\r\n const injectRes = await fetch(`${DASHBOARD_URL}/api/sdk/inject?route=${url.pathname}`, {\r\n headers: { 'x-onto-key': ONTO_API_KEY },\r\n signal: AbortSignal.timeout(1500)\r\n });\r\n\r\n if (injectRes.ok) {\r\n const { injection } = await injectRes.json();\r\n \r\n if (injection) {\r\n const localMdUrl = `${url.origin}/.onto${payloadPath}.md`;\r\n const mdRes = await fetch(localMdUrl);\r\n \r\n if (mdRes.ok) {\r\n const baseMarkdown = await mdRes.text();\r\n const finalMarkdown = `${baseMarkdown}\\n\\n---\\n\\n${injection}`;\r\n \r\n return new NextResponse(finalMarkdown, {\r\n headers: {\r\n ...botHeaders,\r\n 'X-Onto-Injected': 'true'\r\n }\r\n });\r\n }\r\n }\r\n }\r\n } catch (err) {\r\n console.error('[Onto] Injection failed', err);\r\n }\r\n }\r\n // ------------------------------------------------\r\n\r\n url.pathname = `/.onto${payloadPath}.md`;\r\n\r\n // Rewrite implicitly serves the target URL transparently to the client.\r\n const response = NextResponse.rewrite(url);\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n\r\n return NextResponse.next();\r\n}\r\n\r\n// Re-export the bot registry for consumers who want to extend or inspect it\r\nexport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\nexport type { AiBot } from './bots';\r\n\r\n\r\n","/**\n * Comprehensive registry of AI bot user-agent strings.\n * The middleware uses this list to detect AI crawlers and serve optimized markdown.\n */\n\nexport interface AiBot {\n /** The user-agent substring to match against */\n name: string;\n /** The company operating this bot */\n company: string;\n}\n\n/**\n * Structured registry of all known AI bots, grouped by company.\n * Useful for analytics and the Control Plane dashboard.\n */\nexport const AI_BOTS: AiBot[] = [\n // OpenAI\n { name: 'GPTBot', company: 'OpenAI' },\n { name: 'ChatGPT-User', company: 'OpenAI' },\n { name: 'OAI-SearchBot', company: 'OpenAI' },\n\n // Google\n { name: 'Googlebot', company: 'Google' },\n { name: 'Google-CloudVertexBot', company: 'Google' },\n { name: 'Google-Extended', company: 'Google' },\n { name: 'GoogleOther', company: 'Google' },\n\n // Anthropic\n { name: 'ClaudeBot', company: 'Anthropic' },\n { name: 'Claude-User', company: 'Anthropic' },\n { name: 'anthropic-ai', company: 'Anthropic' },\n\n // Perplexity\n { name: 'PerplexityBot', company: 'Perplexity' },\n { name: 'Perplexity-User', company: 'Perplexity' },\n\n // Meta\n { name: 'Meta-ExternalAgent', company: 'Meta' },\n { name: 'Meta-ExternalFetcher', company: 'Meta' },\n { name: 'FacebookBot', company: 'Meta' },\n\n // Common Crawl (used by most smaller AI companies)\n { name: 'CCBot', company: 'Common Crawl' },\n\n // Other notable AI crawlers\n { name: 'Bytespider', company: 'ByteDance' },\n { name: 'Applebot-Extended', company: 'Apple' },\n { name: 'cohere-ai', company: 'Cohere' },\n { name: 'YouBot', company: 'You.com' },\n];\n\n/**\n * Flat list of user-agent substrings for fast matching in the middleware.\n */\nexport const AI_BOT_USER_AGENTS: string[] = AI_BOTS.map(bot => bot.name);\n\n/**\n * Given a raw user-agent string, returns the matched AiBot entry or undefined.\n * Comparison is case-insensitive to handle inconsistent agent casing.\n */\nexport function matchBot(userAgent: string | null): AiBot | undefined {\n if (!userAgent) return undefined;\n const lowerUA = userAgent.toLowerCase();\n return AI_BOTS.find(bot => \n lowerUA.includes(bot.name.toLowerCase())\n );\n}\n","/**\r\n * Configuration schema for onto.config.ts\r\n * Used to dynamically generate llms.txt and other AI discovery files\r\n */\r\n\r\nexport type PageType = 'scoring' | 'about' | 'default';\r\n\r\nexport interface OntoRoute {\r\n /**\r\n * The URL path (e.g., '/docs', '/api/reference')\r\n */\r\n path: string;\r\n /**\r\n * Description of what this route contains\r\n */\r\n description: string;\r\n /**\r\n * Optional: Page type for automatic JSON-LD schema injection\r\n * - 'scoring': Injects Methodology schema with AIO scoring weights (40/35/25)\r\n * - 'about': Injects Organization/AboutPage schema\r\n * - 'default': No automatic schema injection\r\n */\r\n pageType?: PageType;\r\n}\r\n\r\nexport interface OntoConfig {\r\n /**\r\n * The name of your project or site (required)\r\n * Used as the H1 heading in llms.txt\r\n */\r\n name: string;\r\n\r\n /**\r\n * A short summary of your project (required)\r\n * Displayed as a blockquote in llms.txt\r\n * Should contain key information necessary for understanding the rest of the file\r\n */\r\n summary: string;\r\n\r\n /**\r\n * The base URL of your site (e.g., 'https://example.com')\r\n */\r\n baseUrl: string;\r\n\r\n /**\r\n * Optional: Additional sections to include in llms.txt\r\n * Each section can contain any markdown content\r\n */\r\n sections?: {\r\n heading: string;\r\n content: string;\r\n }[];\r\n\r\n /**\r\n * Key routes that AI agents should know about\r\n * These will be formatted as a markdown list in llms.txt\r\n */\r\n routes?: OntoRoute[];\r\n\r\n /**\r\n * Optional: Links to external resources (documentation, API references, etc.)\r\n */\r\n externalLinks?: {\r\n title: string;\r\n url: string;\r\n description?: string;\r\n }[];\r\n\r\n /**\r\n * Optional: Organization information for JSON-LD schemas\r\n */\r\n organization?: {\r\n name: string;\r\n description?: string;\r\n url?: string;\r\n logo?: string;\r\n foundingDate?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Load the onto.config.ts file from the user's project\r\n * This is used by the middleware to dynamically generate llms.txt\r\n */\r\nexport async function loadOntoConfig(): Promise<OntoConfig | null> {\r\n try {\r\n // Try to dynamically import the config file from the user's project root\r\n // This runs in the middleware context, so we look in the project root\r\n const config = await import(process.cwd() + '/onto.config');\r\n return config.default || config;\r\n } catch (error) {\r\n // Config file doesn't exist or failed to load\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Generate llms.txt content from OntoConfig\r\n * Follows the llms.txt specification:\r\n * - H1 with project name\r\n * - Blockquote with summary\r\n * - Additional markdown sections\r\n */\r\nexport function generateLlmsTxt(config: OntoConfig): string {\r\n const lines: string[] = [];\r\n\r\n // H1: Project name (required)\r\n lines.push(`# ${config.name}`);\r\n lines.push('');\r\n\r\n // Blockquote: Summary (required)\r\n lines.push(`> ${config.summary}`);\r\n lines.push('');\r\n\r\n // Key Routes section (if provided)\r\n if (config.routes && config.routes.length > 0) {\r\n lines.push('## Key Routes');\r\n lines.push('');\r\n for (const route of config.routes) {\r\n const fullUrl = `${config.baseUrl}${route.path}`;\r\n lines.push(`- [${route.path}](${fullUrl}): ${route.description}`);\r\n }\r\n lines.push('');\r\n }\r\n\r\n // External Links section (if provided)\r\n if (config.externalLinks && config.externalLinks.length > 0) {\r\n lines.push('## Resources');\r\n lines.push('');\r\n for (const link of config.externalLinks) {\r\n if (link.description) {\r\n lines.push(`- [${link.title}](${link.url}): ${link.description}`);\r\n } else {\r\n lines.push(`- [${link.title}](${link.url})`);\r\n }\r\n }\r\n lines.push('');\r\n }\r\n\r\n // Custom sections (if provided)\r\n if (config.sections && config.sections.length > 0) {\r\n for (const section of config.sections) {\r\n lines.push(`## ${section.heading}`);\r\n lines.push('');\r\n lines.push(section.content);\r\n lines.push('');\r\n }\r\n }\r\n\r\n return lines.join('\\n').trim() + '\\n';\r\n}\r\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,wBAAAE,EAAA,aAAAC,EAAA,mBAAAC,IAAA,eAAAC,EAAAL,GAAA,IAAAM,EAA0C,uBCgBnC,IAAMC,EAAmB,CAE5B,CAAE,KAAM,SAAqB,QAAS,QAAS,EAC/C,CAAE,KAAM,eAAoB,QAAS,QAAS,EAC9C,CAAE,KAAM,gBAAoB,QAAS,QAAS,EAG9C,CAAE,KAAM,YAA0B,QAAS,QAAS,EACpD,CAAE,KAAM,wBAA2B,QAAS,QAAS,EACrD,CAAE,KAAM,kBAA2B,QAAS,QAAS,EACrD,CAAE,KAAM,cAA2B,QAAS,QAAS,EAGrD,CAAE,KAAM,YAAmB,QAAS,WAAY,EAChD,CAAE,KAAM,cAAkB,QAAS,WAAY,EAC/C,CAAE,KAAM,eAAkB,QAAS,WAAY,EAG/C,CAAE,KAAM,gBAAmB,QAAS,YAAa,EACjD,CAAE,KAAM,kBAAmB,QAAS,YAAa,EAGjD,CAAE,KAAM,qBAAwB,QAAS,MAAO,EAChD,CAAE,KAAM,uBAAwB,QAAS,MAAO,EAChD,CAAE,KAAM,cAAuB,QAAS,MAAO,EAG/C,CAAE,KAAM,QAAS,QAAS,cAAe,EAGzC,CAAE,KAAM,aAAqB,QAAS,WAAY,EAClD,CAAE,KAAM,oBAAqB,QAAS,OAAQ,EAC9C,CAAE,KAAM,YAAoB,QAAS,QAAS,EAC9C,CAAE,KAAM,SAAoB,QAAS,SAAU,CACnD,EAKaC,EAA+BD,EAAQ,IAAIE,GAAOA,EAAI,IAAI,EAMhE,SAASC,EAASC,EAA6C,CAClE,GAAI,CAACA,EAAW,OAChB,IAAMC,EAAUD,EAAU,YAAY,EACtC,OAAOJ,EAAQ,KAAKE,GAChBG,EAAQ,SAASH,EAAI,KAAK,YAAY,CAAC,CAC3C,CACJ,CCiBA,eAAsBI,GAA6C,CACjE,GAAI,CAGF,IAAMC,EAAS,MAAM,OAAO,QAAQ,IAAI,EAAI,gBAC5C,OAAOA,EAAO,SAAWA,CAC3B,MAAgB,CAEd,OAAO,IACT,CACF,CASO,SAASC,EAAgBD,EAA4B,CAC1D,IAAME,EAAkB,CAAC,EAWzB,GARAA,EAAM,KAAK,KAAKF,EAAO,IAAI,EAAE,EAC7BE,EAAM,KAAK,EAAE,EAGbA,EAAM,KAAK,KAAKF,EAAO,OAAO,EAAE,EAChCE,EAAM,KAAK,EAAE,EAGTF,EAAO,QAAUA,EAAO,OAAO,OAAS,EAAG,CAC7CE,EAAM,KAAK,eAAe,EAC1BA,EAAM,KAAK,EAAE,EACb,QAAWC,KAASH,EAAO,OAAQ,CACjC,IAAMI,EAAU,GAAGJ,EAAO,OAAO,GAAGG,EAAM,IAAI,GAC9CD,EAAM,KAAK,MAAMC,EAAM,IAAI,KAAKC,CAAO,MAAMD,EAAM,WAAW,EAAE,CAClE,CACAD,EAAM,KAAK,EAAE,CACf,CAGA,GAAIF,EAAO,eAAiBA,EAAO,cAAc,OAAS,EAAG,CAC3DE,EAAM,KAAK,cAAc,EACzBA,EAAM,KAAK,EAAE,EACb,QAAWG,KAAQL,EAAO,cACpBK,EAAK,YACPH,EAAM,KAAK,MAAMG,EAAK,KAAK,KAAKA,EAAK,GAAG,MAAMA,EAAK,WAAW,EAAE,EAEhEH,EAAM,KAAK,MAAMG,EAAK,KAAK,KAAKA,EAAK,GAAG,GAAG,EAG/CH,EAAM,KAAK,EAAE,CACf,CAGA,GAAIF,EAAO,UAAYA,EAAO,SAAS,OAAS,EAC9C,QAAWM,KAAWN,EAAO,SAC3BE,EAAM,KAAK,MAAMI,EAAQ,OAAO,EAAE,EAClCJ,EAAM,KAAK,EAAE,EACbA,EAAM,KAAKI,EAAQ,OAAO,EAC1BJ,EAAM,KAAK,EAAE,EAIjB,OAAOA,EAAM,KAAK;AAAA,CAAI,EAAE,KAAK,EAAI;AAAA,CACnC,CFlJA,eAAsBK,EAAeC,EAAsBC,EAAqB,CAC5E,IAAMC,EAAYF,EAAQ,QAAQ,IAAI,YAAY,EAC5CG,EAAMH,EAAQ,QAAQ,MAAM,EAC5BI,EAAUC,EAASH,CAAS,EAE5BI,EAASN,EAAQ,QAAQ,IAAI,QAAQ,GAAK,GAE1CO,EAAU,CAAC,CAACH,EACZI,EAAsBF,EAAO,SAAS,eAAe,EAG3D,GAAIC,GAAWC,EAAqB,CAGhC,GAAIL,EAAI,SAAS,WAAW,QAAQ,EAChC,OAAO,eAAa,KAAK,EAK7B,GAAIA,EAAI,WAAa,YACjB,GAAI,CAEA,IAAMM,EAAeR,GAAU,MAAMS,EAAe,EAEpD,GAAID,EAAc,CAEd,IAAME,EAAiBC,EAAgBH,CAAY,EAC7CI,EAAW,IAAI,eAAaF,EAAgB,CAC9C,QAAS,CACL,eAAgB,4BAChB,gBAAiB,mEACrB,CACJ,CAAC,EAED,OAAIP,GACAS,EAAS,QAAQ,IAAI,aAAc,GAAGT,EAAQ,IAAI,KAAKA,EAAQ,OAAO,GAAG,EAGtES,CACX,KAAO,CAEHV,EAAI,SAAW,YACf,IAAMU,EAAW,eAAa,QAAQV,CAAG,EACzC,OAAAU,EAAS,QAAQ,IAAI,eAAgB,2BAA2B,EAChEA,EAAS,QAAQ,IAAI,gBAAiB,mEAAmE,EACrGT,GACAS,EAAS,QAAQ,IAAI,aAAc,GAAGT,EAAQ,IAAI,KAAKA,EAAQ,OAAO,GAAG,EAEtES,CACX,CACJ,OAASC,EAAO,CACZ,QAAQ,MAAM,sCAAuCA,CAAK,EAE1DX,EAAI,SAAW,YACf,IAAMU,EAAW,eAAa,QAAQV,CAAG,EACzC,OAAAU,EAAS,QAAQ,IAAI,eAAgB,2BAA2B,EAC5DT,GACAS,EAAS,QAAQ,IAAI,aAAc,GAAGT,EAAQ,IAAI,KAAKA,EAAQ,OAAO,GAAG,EAEtES,CACX,CAIJ,GAAIV,EAAI,SAAS,SAAS,GAAG,EACzB,OAAO,eAAa,KAAK,EAI7B,IAAIY,EAAcZ,EAAI,UAClBY,IAAgB,KAAOA,IAAgB,MACvCA,EAAc,UAIdA,EAAY,SAAS,GAAG,GAAKA,IAAgB,MAC7CA,EAAcA,EAAY,MAAM,EAAG,EAAE,GAIzC,IAAMC,EAAqC,CACvC,eAAgB,+BAChB,gBAAiB,mEACrB,EACIZ,IACAY,EAAW,YAAY,EAAI,GAAGZ,EAAQ,IAAI,KAAKA,EAAQ,OAAO,KAIlE,IAAMa,EAAe,QAAQ,IAAI,aAC3BC,EAAgB,QAAQ,IAAI,oBAAsB,4BAExD,GAAID,EAAc,CAEd,MAAM,GAAGC,CAAa,aAAc,CAChC,OAAQ,OACR,QAAS,CACL,aAAcD,EACd,eAAgB,kBACpB,EACA,KAAM,KAAK,UAAU,CACjB,MAAOd,EAAI,SACX,UAAWD,EACX,IAAKE,EAAUA,EAAQ,KAAO,KAC9B,QAASA,EAAUA,EAAQ,QAAU,IACzC,CAAC,CACL,CAAC,EAAE,MAAM,IAAM,CAAC,CAAC,EAGjB,GAAI,CACA,IAAMe,EAAY,MAAM,MAAM,GAAGD,CAAa,yBAAyBf,EAAI,QAAQ,GAAI,CACnF,QAAS,CAAE,aAAcc,CAAa,EACtC,OAAQ,YAAY,QAAQ,IAAI,CACpC,CAAC,EAED,GAAIE,EAAU,GAAI,CACd,GAAM,CAAE,UAAAC,CAAU,EAAI,MAAMD,EAAU,KAAK,EAE3C,GAAIC,EAAW,CACX,IAAMC,EAAa,GAAGlB,EAAI,MAAM,SAASY,CAAW,MAC9CO,EAAQ,MAAM,MAAMD,CAAU,EAEpC,GAAIC,EAAM,GAAI,CAEV,IAAMC,EAAgB,GADD,MAAMD,EAAM,KAAK,CACD;AAAA;AAAA;AAAA;AAAA,EAAcF,CAAS,GAE5D,OAAO,IAAI,eAAaG,EAAe,CACnC,QAAS,CACL,GAAGP,EACH,kBAAmB,MACvB,CACJ,CAAC,CACL,CACJ,CACJ,CACJ,OAASQ,EAAK,CACV,QAAQ,MAAM,0BAA2BA,CAAG,CAChD,CACJ,CAGArB,EAAI,SAAW,SAASY,CAAW,MAGnC,IAAMF,EAAW,eAAa,QAAQV,CAAG,EACzC,OAAIC,GACAS,EAAS,QAAQ,IAAI,aAAc,GAAGT,EAAQ,IAAI,KAAKA,EAAQ,OAAO,GAAG,EAEtES,CACX,CAEA,OAAO,eAAa,KAAK,CAC7B","names":["middleware_exports","__export","AI_BOT_USER_AGENTS","matchBot","ontoMiddleware","__toCommonJS","import_server","AI_BOTS","AI_BOT_USER_AGENTS","bot","matchBot","userAgent","lowerUA","loadOntoConfig","config","generateLlmsTxt","lines","route","fullUrl","link","section","ontoMiddleware","request","config","userAgent","url","matched","matchBot","accept","isAiBot","isMarkdownRequested","activeConfig","loadOntoConfig","llmsTxtContent","generateLlmsTxt","response","error","payloadPath","botHeaders","ONTO_API_KEY","DASHBOARD_URL","injectRes","injection","localMdUrl","mdRes","finalMarkdown","err"]}
|
package/dist/middleware.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import{NextResponse as c}from"next/server";var
|
|
1
|
+
import{NextResponse as c}from"next/server";var y=[{name:"GPTBot",company:"OpenAI"},{name:"ChatGPT-User",company:"OpenAI"},{name:"OAI-SearchBot",company:"OpenAI"},{name:"Googlebot",company:"Google"},{name:"Google-CloudVertexBot",company:"Google"},{name:"Google-Extended",company:"Google"},{name:"GoogleOther",company:"Google"},{name:"ClaudeBot",company:"Anthropic"},{name:"Claude-User",company:"Anthropic"},{name:"anthropic-ai",company:"Anthropic"},{name:"PerplexityBot",company:"Perplexity"},{name:"Perplexity-User",company:"Perplexity"},{name:"Meta-ExternalAgent",company:"Meta"},{name:"Meta-ExternalFetcher",company:"Meta"},{name:"FacebookBot",company:"Meta"},{name:"CCBot",company:"Common Crawl"},{name:"Bytespider",company:"ByteDance"},{name:"Applebot-Extended",company:"Apple"},{name:"cohere-ai",company:"Cohere"},{name:"YouBot",company:"You.com"}],B=y.map(e=>e.name);function l(e){if(!e)return;let t=e.toLowerCase();return y.find(o=>t.includes(o.name.toLowerCase()))}async function f(){try{let e=await import(process.cwd()+"/onto.config");return e.default||e}catch{return null}}function x(e){let t=[];if(t.push(`# ${e.name}`),t.push(""),t.push(`> ${e.summary}`),t.push(""),e.routes&&e.routes.length>0){t.push("## Key Routes"),t.push("");for(let o of e.routes){let a=`${e.baseUrl}${o.path}`;t.push(`- [${o.path}](${a}): ${o.description}`)}t.push("")}if(e.externalLinks&&e.externalLinks.length>0){t.push("## Resources"),t.push("");for(let o of e.externalLinks)o.description?t.push(`- [${o.title}](${o.url}): ${o.description}`):t.push(`- [${o.title}](${o.url})`);t.push("")}if(e.sections&&e.sections.length>0)for(let o of e.sections)t.push(`## ${o.heading}`),t.push(""),t.push(o.content),t.push("");return t.join(`
|
|
2
|
+
`).trim()+`
|
|
3
|
+
`}async function G(e,t){let o=e.headers.get("user-agent"),a=e.nextUrl.clone(),n=l(o),O=e.headers.get("accept")||"",C=!!n,$=O.includes("text/markdown");if(C||$){if(a.pathname.startsWith("/_next"))return c.next();if(a.pathname==="/llms.txt")try{let i=t||await f();if(i){let r=x(i),p=new c(r,{headers:{"Content-Type":"text/plain; charset=utf-8","Cache-Control":"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400"}});return n&&p.headers.set("X-Onto-Bot",`${n.name} (${n.company})`),p}else{a.pathname="/llms.txt";let r=c.rewrite(a);return r.headers.set("Content-Type","text/plain; charset=utf-8"),r.headers.set("Cache-Control","public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400"),n&&r.headers.set("X-Onto-Bot",`${n.name} (${n.company})`),r}}catch(i){console.error("[Onto] Failed to generate llms.txt:",i),a.pathname="/llms.txt";let r=c.rewrite(a);return r.headers.set("Content-Type","text/plain; charset=utf-8"),n&&r.headers.set("X-Onto-Bot",`${n.name} (${n.company})`),r}if(a.pathname.includes("."))return c.next();let s=a.pathname;(s==="/"||s==="")&&(s="/index"),s.endsWith("/")&&s!=="/"&&(s=s.slice(0,-1));let u={"Content-Type":"text/markdown; charset=utf-8","Cache-Control":"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400"};n&&(u["X-Onto-Bot"]=`${n.name} (${n.company})`);let m=process.env.ONTO_API_KEY,h=process.env.ONTO_DASHBOARD_URL||"https://app.buildonto.dev";if(m){fetch(`${h}/api/track`,{method:"POST",headers:{"x-onto-key":m,"Content-Type":"application/json"},body:JSON.stringify({route:a.pathname,userAgent:o,bot:n?n.name:null,company:n?n.company:null})}).catch(()=>{});try{let i=await fetch(`${h}/api/sdk/inject?route=${a.pathname}`,{headers:{"x-onto-key":m},signal:AbortSignal.timeout(1500)});if(i.ok){let{injection:r}=await i.json();if(r){let p=`${a.origin}/.onto${s}.md`,g=await fetch(p);if(g.ok){let A=`${await g.text()}
|
|
2
4
|
|
|
3
5
|
---
|
|
4
6
|
|
|
5
|
-
${
|
|
7
|
+
${r}`;return new c(A,{headers:{...u,"X-Onto-Injected":"true"}})}}}}catch(i){console.error("[Onto] Injection failed",i)}}a.pathname=`/.onto${s}.md`;let d=c.rewrite(a);return n&&d.headers.set("X-Onto-Bot",`${n.name} (${n.company})`),d}return c.next()}export{B as AI_BOT_USER_AGENTS,l as matchBot,G as ontoMiddleware};
|
|
6
8
|
//# sourceMappingURL=middleware.mjs.map
|
package/dist/middleware.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/bots.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\r\nimport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\n\r\nexport async function ontoMiddleware(request: NextRequest) {\r\n const userAgent = request.headers.get('user-agent') || '';\r\n const accept = request.headers.get('accept') || '';\r\n\r\n const matched = matchBot(userAgent);\r\n const isAiBot = !!matched;\r\n const isMarkdownRequested = accept.includes('text/markdown');\r\n\r\n // If traffic is identified as an AI Bot, rewrite the URL\r\n if (isAiBot || isMarkdownRequested) {\r\n const url = request.nextUrl.clone();\r\n\r\n // Ignore internal next.js requests & static assets\r\n if (url.pathname.startsWith('/_next') || url.pathname.includes('.')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Determine the corresponding payload path\r\n let payloadPath = url.pathname;\r\n if (payloadPath === '/' || payloadPath === '') {\r\n payloadPath = '/index';\r\n }\r\n\r\n // Strip trailing slash if present\r\n if (payloadPath.endsWith('/') && payloadPath !== '/') {\r\n payloadPath = payloadPath.slice(0, -1);\r\n }\r\n\r\n // Common response headers for all bot responses\r\n const botHeaders: Record<string, string> = {\r\n 'Content-Type': 'text/markdown; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n };\r\n if (matched) {\r\n botHeaders['X-Onto-Bot'] = `${matched.name} (${matched.company})`;\r\n }\r\n\r\n // --- Onto Control Plane Integration (Premium) ---\r\n const ONTO_API_KEY = process.env.ONTO_API_KEY;\r\n const DASHBOARD_URL = process.env.ONTO_DASHBOARD_URL || 'https://app.buildonto.dev';\r\n\r\n if (ONTO_API_KEY) {\r\n // 1. Fire-and-forget tracking — includes structured bot info\r\n fetch(`${DASHBOARD_URL}/api/track`, {\r\n method: 'POST',\r\n headers: {\r\n 'x-onto-key': ONTO_API_KEY,\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({\r\n route: url.pathname,\r\n userAgent: userAgent,\r\n bot: matched ? matched.name : null,\r\n company: matched ? matched.company : null,\r\n })\r\n }).catch(() => {});\r\n\r\n // 2. Dynamic Context Injection\r\n try {\r\n // Fetch the injection from the Control Plane\r\n const injectRes = await fetch(`${DASHBOARD_URL}/api/sdk/inject?route=${url.pathname}`, {\r\n headers: { 'x-onto-key': ONTO_API_KEY },\r\n // Set a strict timeout to keep edge fast\r\n signal: AbortSignal.timeout(1500)\r\n });\r\n\r\n if (injectRes.ok) {\r\n const { injection } = await injectRes.json();\r\n \r\n if (injection) {\r\n // To inject, we must fetch the local markdown and append\r\n const localMdUrl = `${url.origin}/.onto${payloadPath}.md`;\r\n const mdRes = await fetch(localMdUrl);\r\n \r\n if (mdRes.ok) {\r\n const baseMarkdown = await mdRes.text();\r\n const finalMarkdown = `${baseMarkdown}\\n\\n---\\n\\n${injection}`;\r\n \r\n return new NextResponse(finalMarkdown, {\r\n headers: {\r\n ...botHeaders,\r\n 'X-Onto-Injected': 'true'\r\n }\r\n });\r\n }\r\n }\r\n }\r\n } catch (err) {\r\n console.error('[Onto] Injection failed', err);\r\n }\r\n }\r\n // ------------------------------------------------\r\n\r\n url.pathname = `/.onto${payloadPath}.md`;\r\n\r\n // Rewrite implicitly serves the target URL transparently to the client.\r\n const response = NextResponse.rewrite(url);\r\n // Attach bot identification headers to the rewrite response\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n\r\n return NextResponse.next();\r\n}\r\n\r\n// Re-export the bot registry for consumers who want to extend or inspect it\r\nexport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\nexport type { AiBot } from './bots';\r\n\r\n","/**\n * Comprehensive registry of AI bot user-agent strings.\n * The middleware uses this list to detect AI crawlers and serve optimized markdown.\n */\n\nexport interface AiBot {\n /** The user-agent substring to match against */\n name: string;\n /** The company operating this bot */\n company: string;\n}\n\n/**\n * Structured registry of all known AI bots, grouped by company.\n * Useful for analytics and the Control Plane dashboard.\n */\nexport const AI_BOTS: AiBot[] = [\n // OpenAI\n { name: 'GPTBot', company: 'OpenAI' },\n { name: 'ChatGPT-User', company: 'OpenAI' },\n { name: 'OAI-SearchBot', company: 'OpenAI' },\n\n // Google\n { name: 'Googlebot', company: 'Google' },\n { name: 'Google-CloudVertexBot', company: 'Google' },\n { name: 'Google-Extended', company: 'Google' },\n { name: 'GoogleOther', company: 'Google' },\n\n // Anthropic\n { name: 'ClaudeBot', company: 'Anthropic' },\n { name: 'Claude-User', company: 'Anthropic' },\n { name: 'anthropic-ai', company: 'Anthropic' },\n\n // Perplexity\n { name: 'PerplexityBot', company: 'Perplexity' },\n { name: 'Perplexity-User', company: 'Perplexity' },\n\n // Meta\n { name: 'Meta-ExternalAgent', company: 'Meta' },\n { name: 'Meta-ExternalFetcher', company: 'Meta' },\n { name: 'FacebookBot', company: 'Meta' },\n\n // Common Crawl (used by most smaller AI companies)\n { name: 'CCBot', company: 'Common Crawl' },\n\n // Other notable AI crawlers\n { name: 'Bytespider', company: 'ByteDance' },\n { name: 'Applebot-Extended', company: 'Apple' },\n { name: 'cohere-ai', company: 'Cohere' },\n { name: 'YouBot', company: 'You.com' },\n];\n\n/**\n * Flat list of user-agent substrings for fast matching in the middleware.\n */\nexport const AI_BOT_USER_AGENTS: string[] = AI_BOTS.map(bot => bot.name);\n\n/**\n * Given a raw user-agent string, returns the matched AiBot entry or undefined.\n * Comparison is case-insensitive to handle inconsistent agent casing.\n */\nexport function matchBot(userAgent: string): AiBot | undefined {\n const ua = userAgent.toLowerCase();\n return AI_BOTS.find(bot => ua.includes(bot.name.toLowerCase()));\n}\n"],"mappings":"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","names":["NextResponse","AI_BOTS","AI_BOT_USER_AGENTS","bot","matchBot","userAgent","ua","ontoMiddleware","request","userAgent","accept","matched","matchBot","isAiBot","isMarkdownRequested","url","NextResponse","payloadPath","botHeaders","ONTO_API_KEY","DASHBOARD_URL","injectRes","injection","localMdUrl","mdRes","finalMarkdown","err","response"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/bots.ts","../src/config.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\r\nimport { matchBot } from './bots';\r\nimport { loadOntoConfig, generateLlmsTxt, OntoConfig } from './config';\r\n\r\nexport async function ontoMiddleware(request: NextRequest, config?: OntoConfig) {\r\n const userAgent = request.headers.get('user-agent');\r\n const url = request.nextUrl.clone();\r\n const matched = matchBot(userAgent);\r\n\r\n const accept = request.headers.get('accept') || '';\r\n\r\n const isAiBot = !!matched;\r\n const isMarkdownRequested = accept.includes('text/markdown');\r\n\r\n // If traffic is identified as an AI Bot or markdown is requested\r\n if (isAiBot || isMarkdownRequested) {\r\n\r\n // Ignore internal next.js requests & static assets (but not llms.txt)\r\n if (url.pathname.startsWith('/_next')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // --- llms.txt Auto-Discovery ---\r\n // Serve the llms.txt manifest to AI agents\r\n if (url.pathname === '/llms.txt') {\r\n try {\r\n // Use provided config or try to load dynamic one\r\n const activeConfig = config || await loadOntoConfig();\r\n\r\n if (activeConfig) {\r\n // Generate llms.txt dynamically from config\r\n const llmsTxtContent = generateLlmsTxt(activeConfig);\r\n const response = new NextResponse(llmsTxtContent, {\r\n headers: {\r\n 'Content-Type': 'text/plain; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n }\r\n });\r\n\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n\r\n return response;\r\n } else {\r\n // Fallback: try to serve static llms.txt from public folder\r\n url.pathname = '/llms.txt';\r\n const response = NextResponse.rewrite(url);\r\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\r\n response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400');\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n } catch (error) {\r\n console.error('[Onto] Failed to generate llms.txt:', error);\r\n // Fallback to static file on error\r\n url.pathname = '/llms.txt';\r\n const response = NextResponse.rewrite(url);\r\n response.headers.set('Content-Type', 'text/plain; charset=utf-8');\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n }\r\n\r\n // Skip other static assets\r\n if (url.pathname.includes('.')) {\r\n return NextResponse.next();\r\n }\r\n\r\n // Determine the corresponding payload path\r\n let payloadPath = url.pathname;\r\n if (payloadPath === '/' || payloadPath === '') {\r\n payloadPath = '/index';\r\n }\r\n\r\n // Strip trailing slash if present\r\n if (payloadPath.endsWith('/') && payloadPath !== '/') {\r\n payloadPath = payloadPath.slice(0, -1);\r\n }\r\n\r\n // Common response headers for all bot responses\r\n const botHeaders: Record<string, string> = {\r\n 'Content-Type': 'text/markdown; charset=utf-8',\r\n 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\r\n };\r\n if (matched) {\r\n botHeaders['X-Onto-Bot'] = `${matched.name} (${matched.company})`;\r\n }\r\n\r\n // --- Onto Control Plane Integration (Premium) ---\r\n const ONTO_API_KEY = process.env.ONTO_API_KEY;\r\n const DASHBOARD_URL = process.env.ONTO_DASHBOARD_URL || 'https://app.buildonto.dev';\r\n\r\n if (ONTO_API_KEY) {\r\n // 1. Fire-and-forget tracking — includes structured bot info\r\n fetch(`${DASHBOARD_URL}/api/track`, {\r\n method: 'POST',\r\n headers: {\r\n 'x-onto-key': ONTO_API_KEY,\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({\r\n route: url.pathname,\r\n userAgent: userAgent,\r\n bot: matched ? matched.name : null,\r\n company: matched ? matched.company : null,\r\n })\r\n }).catch(() => {});\r\n\r\n // 2. Dynamic Context Injection\r\n try {\r\n const injectRes = await fetch(`${DASHBOARD_URL}/api/sdk/inject?route=${url.pathname}`, {\r\n headers: { 'x-onto-key': ONTO_API_KEY },\r\n signal: AbortSignal.timeout(1500)\r\n });\r\n\r\n if (injectRes.ok) {\r\n const { injection } = await injectRes.json();\r\n \r\n if (injection) {\r\n const localMdUrl = `${url.origin}/.onto${payloadPath}.md`;\r\n const mdRes = await fetch(localMdUrl);\r\n \r\n if (mdRes.ok) {\r\n const baseMarkdown = await mdRes.text();\r\n const finalMarkdown = `${baseMarkdown}\\n\\n---\\n\\n${injection}`;\r\n \r\n return new NextResponse(finalMarkdown, {\r\n headers: {\r\n ...botHeaders,\r\n 'X-Onto-Injected': 'true'\r\n }\r\n });\r\n }\r\n }\r\n }\r\n } catch (err) {\r\n console.error('[Onto] Injection failed', err);\r\n }\r\n }\r\n // ------------------------------------------------\r\n\r\n url.pathname = `/.onto${payloadPath}.md`;\r\n\r\n // Rewrite implicitly serves the target URL transparently to the client.\r\n const response = NextResponse.rewrite(url);\r\n if (matched) {\r\n response.headers.set('X-Onto-Bot', `${matched.name} (${matched.company})`);\r\n }\r\n return response;\r\n }\r\n\r\n return NextResponse.next();\r\n}\r\n\r\n// Re-export the bot registry for consumers who want to extend or inspect it\r\nexport { AI_BOT_USER_AGENTS, matchBot } from './bots';\r\nexport type { AiBot } from './bots';\r\n\r\n\r\n","/**\n * Comprehensive registry of AI bot user-agent strings.\n * The middleware uses this list to detect AI crawlers and serve optimized markdown.\n */\n\nexport interface AiBot {\n /** The user-agent substring to match against */\n name: string;\n /** The company operating this bot */\n company: string;\n}\n\n/**\n * Structured registry of all known AI bots, grouped by company.\n * Useful for analytics and the Control Plane dashboard.\n */\nexport const AI_BOTS: AiBot[] = [\n // OpenAI\n { name: 'GPTBot', company: 'OpenAI' },\n { name: 'ChatGPT-User', company: 'OpenAI' },\n { name: 'OAI-SearchBot', company: 'OpenAI' },\n\n // Google\n { name: 'Googlebot', company: 'Google' },\n { name: 'Google-CloudVertexBot', company: 'Google' },\n { name: 'Google-Extended', company: 'Google' },\n { name: 'GoogleOther', company: 'Google' },\n\n // Anthropic\n { name: 'ClaudeBot', company: 'Anthropic' },\n { name: 'Claude-User', company: 'Anthropic' },\n { name: 'anthropic-ai', company: 'Anthropic' },\n\n // Perplexity\n { name: 'PerplexityBot', company: 'Perplexity' },\n { name: 'Perplexity-User', company: 'Perplexity' },\n\n // Meta\n { name: 'Meta-ExternalAgent', company: 'Meta' },\n { name: 'Meta-ExternalFetcher', company: 'Meta' },\n { name: 'FacebookBot', company: 'Meta' },\n\n // Common Crawl (used by most smaller AI companies)\n { name: 'CCBot', company: 'Common Crawl' },\n\n // Other notable AI crawlers\n { name: 'Bytespider', company: 'ByteDance' },\n { name: 'Applebot-Extended', company: 'Apple' },\n { name: 'cohere-ai', company: 'Cohere' },\n { name: 'YouBot', company: 'You.com' },\n];\n\n/**\n * Flat list of user-agent substrings for fast matching in the middleware.\n */\nexport const AI_BOT_USER_AGENTS: string[] = AI_BOTS.map(bot => bot.name);\n\n/**\n * Given a raw user-agent string, returns the matched AiBot entry or undefined.\n * Comparison is case-insensitive to handle inconsistent agent casing.\n */\nexport function matchBot(userAgent: string | null): AiBot | undefined {\n if (!userAgent) return undefined;\n const lowerUA = userAgent.toLowerCase();\n return AI_BOTS.find(bot => \n lowerUA.includes(bot.name.toLowerCase())\n );\n}\n","/**\r\n * Configuration schema for onto.config.ts\r\n * Used to dynamically generate llms.txt and other AI discovery files\r\n */\r\n\r\nexport type PageType = 'scoring' | 'about' | 'default';\r\n\r\nexport interface OntoRoute {\r\n /**\r\n * The URL path (e.g., '/docs', '/api/reference')\r\n */\r\n path: string;\r\n /**\r\n * Description of what this route contains\r\n */\r\n description: string;\r\n /**\r\n * Optional: Page type for automatic JSON-LD schema injection\r\n * - 'scoring': Injects Methodology schema with AIO scoring weights (40/35/25)\r\n * - 'about': Injects Organization/AboutPage schema\r\n * - 'default': No automatic schema injection\r\n */\r\n pageType?: PageType;\r\n}\r\n\r\nexport interface OntoConfig {\r\n /**\r\n * The name of your project or site (required)\r\n * Used as the H1 heading in llms.txt\r\n */\r\n name: string;\r\n\r\n /**\r\n * A short summary of your project (required)\r\n * Displayed as a blockquote in llms.txt\r\n * Should contain key information necessary for understanding the rest of the file\r\n */\r\n summary: string;\r\n\r\n /**\r\n * The base URL of your site (e.g., 'https://example.com')\r\n */\r\n baseUrl: string;\r\n\r\n /**\r\n * Optional: Additional sections to include in llms.txt\r\n * Each section can contain any markdown content\r\n */\r\n sections?: {\r\n heading: string;\r\n content: string;\r\n }[];\r\n\r\n /**\r\n * Key routes that AI agents should know about\r\n * These will be formatted as a markdown list in llms.txt\r\n */\r\n routes?: OntoRoute[];\r\n\r\n /**\r\n * Optional: Links to external resources (documentation, API references, etc.)\r\n */\r\n externalLinks?: {\r\n title: string;\r\n url: string;\r\n description?: string;\r\n }[];\r\n\r\n /**\r\n * Optional: Organization information for JSON-LD schemas\r\n */\r\n organization?: {\r\n name: string;\r\n description?: string;\r\n url?: string;\r\n logo?: string;\r\n foundingDate?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Load the onto.config.ts file from the user's project\r\n * This is used by the middleware to dynamically generate llms.txt\r\n */\r\nexport async function loadOntoConfig(): Promise<OntoConfig | null> {\r\n try {\r\n // Try to dynamically import the config file from the user's project root\r\n // This runs in the middleware context, so we look in the project root\r\n const config = await import(process.cwd() + '/onto.config');\r\n return config.default || config;\r\n } catch (error) {\r\n // Config file doesn't exist or failed to load\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Generate llms.txt content from OntoConfig\r\n * Follows the llms.txt specification:\r\n * - H1 with project name\r\n * - Blockquote with summary\r\n * - Additional markdown sections\r\n */\r\nexport function generateLlmsTxt(config: OntoConfig): string {\r\n const lines: string[] = [];\r\n\r\n // H1: Project name (required)\r\n lines.push(`# ${config.name}`);\r\n lines.push('');\r\n\r\n // Blockquote: Summary (required)\r\n lines.push(`> ${config.summary}`);\r\n lines.push('');\r\n\r\n // Key Routes section (if provided)\r\n if (config.routes && config.routes.length > 0) {\r\n lines.push('## Key Routes');\r\n lines.push('');\r\n for (const route of config.routes) {\r\n const fullUrl = `${config.baseUrl}${route.path}`;\r\n lines.push(`- [${route.path}](${fullUrl}): ${route.description}`);\r\n }\r\n lines.push('');\r\n }\r\n\r\n // External Links section (if provided)\r\n if (config.externalLinks && config.externalLinks.length > 0) {\r\n lines.push('## Resources');\r\n lines.push('');\r\n for (const link of config.externalLinks) {\r\n if (link.description) {\r\n lines.push(`- [${link.title}](${link.url}): ${link.description}`);\r\n } else {\r\n lines.push(`- [${link.title}](${link.url})`);\r\n }\r\n }\r\n lines.push('');\r\n }\r\n\r\n // Custom sections (if provided)\r\n if (config.sections && config.sections.length > 0) {\r\n for (const section of config.sections) {\r\n lines.push(`## ${section.heading}`);\r\n lines.push('');\r\n lines.push(section.content);\r\n lines.push('');\r\n }\r\n }\r\n\r\n return lines.join('\\n').trim() + '\\n';\r\n}\r\n"],"mappings":"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","names":["NextResponse","AI_BOTS","AI_BOT_USER_AGENTS","bot","matchBot","userAgent","lowerUA","loadOntoConfig","config","generateLlmsTxt","lines","route","fullUrl","link","section","ontoMiddleware","request","config","userAgent","url","matched","matchBot","accept","isAiBot","isMarkdownRequested","NextResponse","activeConfig","loadOntoConfig","llmsTxtContent","generateLlmsTxt","response","error","payloadPath","botHeaders","ONTO_API_KEY","DASHBOARD_URL","injectRes","injection","localMdUrl","mdRes","finalMarkdown","err"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { OntoConfig } from './config.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON-LD Schema generators for automatic structured data injection
|
|
5
|
+
* Follows Schema.org standards for AI-friendly metadata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Standard AIO (AI Optimization) scoring methodology
|
|
10
|
+
* Based on the Onto scoring algorithm:
|
|
11
|
+
* - Content Negotiation: 40% (30 points)
|
|
12
|
+
* - React Tax / Token Efficiency: 35% (30 points)
|
|
13
|
+
* - Structured Data: 25% (25 points)
|
|
14
|
+
* - Semantic HTML: Bonus (15 points)
|
|
15
|
+
*/
|
|
16
|
+
interface AIOMethodologySchema {
|
|
17
|
+
'@context': 'https://schema.org';
|
|
18
|
+
'@type': 'HowTo';
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
step: Array<{
|
|
22
|
+
'@type': 'HowToStep';
|
|
23
|
+
name: string;
|
|
24
|
+
text: string;
|
|
25
|
+
position: number;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate AIO Scoring Methodology JSON-LD schema
|
|
30
|
+
* This explains to AI agents how the scoring system works
|
|
31
|
+
*/
|
|
32
|
+
declare function generateAIOMethodologySchema(config: OntoConfig, pageUrl: string): AIOMethodologySchema;
|
|
33
|
+
/**
|
|
34
|
+
* Organization schema for About pages
|
|
35
|
+
*/
|
|
36
|
+
interface OrganizationSchema {
|
|
37
|
+
'@context': 'https://schema.org';
|
|
38
|
+
'@type': 'Organization';
|
|
39
|
+
name: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
logo?: string;
|
|
43
|
+
foundingDate?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate Organization JSON-LD schema for About pages
|
|
47
|
+
*/
|
|
48
|
+
declare function generateOrganizationSchema(config: OntoConfig, pageUrl: string): OrganizationSchema | null;
|
|
49
|
+
/**
|
|
50
|
+
* AboutPage schema combining Organization and WebPage
|
|
51
|
+
*/
|
|
52
|
+
interface AboutPageSchema {
|
|
53
|
+
'@context': 'https://schema.org';
|
|
54
|
+
'@type': 'AboutPage';
|
|
55
|
+
name: string;
|
|
56
|
+
url: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
mainEntity?: OrganizationSchema;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Generate AboutPage JSON-LD schema
|
|
62
|
+
*/
|
|
63
|
+
declare function generateAboutPageSchema(config: OntoConfig, pageUrl: string): AboutPageSchema;
|
|
64
|
+
/**
|
|
65
|
+
* Determine which schema to generate based on page type
|
|
66
|
+
*/
|
|
67
|
+
declare function generateSchemaForPageType(pageType: 'scoring' | 'about' | 'default', config: OntoConfig, pageUrl: string): any | null;
|
|
68
|
+
/**
|
|
69
|
+
* Serialize schema to JSON-LD script tag content
|
|
70
|
+
*/
|
|
71
|
+
declare function serializeSchema(schema: any | null): string | null;
|
|
72
|
+
|
|
73
|
+
export { type AIOMethodologySchema, type AboutPageSchema, type OrganizationSchema, generateAIOMethodologySchema, generateAboutPageSchema, generateOrganizationSchema, generateSchemaForPageType, serializeSchema };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { OntoConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON-LD Schema generators for automatic structured data injection
|
|
5
|
+
* Follows Schema.org standards for AI-friendly metadata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Standard AIO (AI Optimization) scoring methodology
|
|
10
|
+
* Based on the Onto scoring algorithm:
|
|
11
|
+
* - Content Negotiation: 40% (30 points)
|
|
12
|
+
* - React Tax / Token Efficiency: 35% (30 points)
|
|
13
|
+
* - Structured Data: 25% (25 points)
|
|
14
|
+
* - Semantic HTML: Bonus (15 points)
|
|
15
|
+
*/
|
|
16
|
+
interface AIOMethodologySchema {
|
|
17
|
+
'@context': 'https://schema.org';
|
|
18
|
+
'@type': 'HowTo';
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
step: Array<{
|
|
22
|
+
'@type': 'HowToStep';
|
|
23
|
+
name: string;
|
|
24
|
+
text: string;
|
|
25
|
+
position: number;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate AIO Scoring Methodology JSON-LD schema
|
|
30
|
+
* This explains to AI agents how the scoring system works
|
|
31
|
+
*/
|
|
32
|
+
declare function generateAIOMethodologySchema(config: OntoConfig, pageUrl: string): AIOMethodologySchema;
|
|
33
|
+
/**
|
|
34
|
+
* Organization schema for About pages
|
|
35
|
+
*/
|
|
36
|
+
interface OrganizationSchema {
|
|
37
|
+
'@context': 'https://schema.org';
|
|
38
|
+
'@type': 'Organization';
|
|
39
|
+
name: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
logo?: string;
|
|
43
|
+
foundingDate?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate Organization JSON-LD schema for About pages
|
|
47
|
+
*/
|
|
48
|
+
declare function generateOrganizationSchema(config: OntoConfig, pageUrl: string): OrganizationSchema | null;
|
|
49
|
+
/**
|
|
50
|
+
* AboutPage schema combining Organization and WebPage
|
|
51
|
+
*/
|
|
52
|
+
interface AboutPageSchema {
|
|
53
|
+
'@context': 'https://schema.org';
|
|
54
|
+
'@type': 'AboutPage';
|
|
55
|
+
name: string;
|
|
56
|
+
url: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
mainEntity?: OrganizationSchema;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Generate AboutPage JSON-LD schema
|
|
62
|
+
*/
|
|
63
|
+
declare function generateAboutPageSchema(config: OntoConfig, pageUrl: string): AboutPageSchema;
|
|
64
|
+
/**
|
|
65
|
+
* Determine which schema to generate based on page type
|
|
66
|
+
*/
|
|
67
|
+
declare function generateSchemaForPageType(pageType: 'scoring' | 'about' | 'default', config: OntoConfig, pageUrl: string): any | null;
|
|
68
|
+
/**
|
|
69
|
+
* Serialize schema to JSON-LD script tag content
|
|
70
|
+
*/
|
|
71
|
+
declare function serializeSchema(schema: any | null): string | null;
|
|
72
|
+
|
|
73
|
+
export { type AIOMethodologySchema, type AboutPageSchema, type OrganizationSchema, generateAIOMethodologySchema, generateAboutPageSchema, generateOrganizationSchema, generateSchemaForPageType, serializeSchema };
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var a=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var m=(t,e)=>{for(var n in e)a(t,n,{get:e[n],enumerable:!0})},l=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of p(e))!u.call(t,i)&&i!==n&&a(t,i,{get:()=>e[i],enumerable:!(o=g(e,i))||o.enumerable});return t};var h=t=>l(a({},"__esModule",{value:!0}),t);var y={};m(y,{generateAIOMethodologySchema:()=>r,generateAboutPageSchema:()=>c,generateOrganizationSchema:()=>s,generateSchemaForPageType:()=>f,serializeSchema:()=>d});module.exports=h(y);function r(t,e){return{"@context":"https://schema.org","@type":"HowTo",name:"AIO Score Calculation Methodology",description:"AI Optimization (AIO) Score measures how well a website is optimized for AI agents and LLM crawlers. Scored out of 100 points based on four key metrics.",step:[{"@type":"HowToStep",name:"Content Negotiation",text:"Check if the site responds to Accept: text/markdown header. Weight: 40%. Penalty: -30 points if missing. This ensures AI bots receive optimized content instead of heavy HTML.",position:1},{"@type":"HowToStep",name:"Token Efficiency (React Tax)",text:"Measure the ratio of visible text to total HTML size. Weight: 35%. Penalty: -30 points if HTML > 100KB but text < 1KB. Detects JavaScript-heavy sites that are difficult for AI to parse.",position:2},{"@type":"HowToStep",name:"Structured Data",text:"Verify presence of JSON-LD structured data (Schema.org). Weight: 25%. Penalty: -25 points if missing. Enables AI to confidently extract pricing, products, and entities.",position:3},{"@type":"HowToStep",name:"Semantic HTML",text:"Check for semantic tags like <main> and <article>. Bonus: +15 points if present. Helps AI agents separate navigation from core content.",position:4}]}}function s(t,e){if(!t.organization)return null;let n={"@context":"https://schema.org","@type":"Organization",name:t.organization.name};return t.organization.url&&(n.url=t.organization.url),t.organization.description&&(n.description=t.organization.description),t.organization.logo&&(n.logo=t.organization.logo),t.organization.foundingDate&&(n.foundingDate=t.organization.foundingDate),n}function c(t,e){let n=s(t,e),o={"@context":"https://schema.org","@type":"AboutPage",name:`About ${t.name}`,url:e};return t.summary&&(o.description=t.summary),n&&(o.mainEntity=n),o}function f(t,e,n){switch(t){case"scoring":return r(e,n);case"about":return c(e,n);default:return null}}function d(t){return t?JSON.stringify(t,null,2):null}0&&(module.exports={generateAIOMethodologySchema,generateAboutPageSchema,generateOrganizationSchema,generateSchemaForPageType,serializeSchema});
|
|
2
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas.ts"],"sourcesContent":["/**\r\n * JSON-LD Schema generators for automatic structured data injection\r\n * Follows Schema.org standards for AI-friendly metadata\r\n */\r\n\r\nimport { OntoConfig } from './config';\r\n\r\n/**\r\n * Standard AIO (AI Optimization) scoring methodology\r\n * Based on the Onto scoring algorithm:\r\n * - Content Negotiation: 40% (30 points)\r\n * - React Tax / Token Efficiency: 35% (30 points)\r\n * - Structured Data: 25% (25 points)\r\n * - Semantic HTML: Bonus (15 points)\r\n */\r\nexport interface AIOMethodologySchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'HowTo';\r\n name: string;\r\n description: string;\r\n step: Array<{\r\n '@type': 'HowToStep';\r\n name: string;\r\n text: string;\r\n position: number;\r\n }>;\r\n}\r\n\r\n/**\r\n * Generate AIO Scoring Methodology JSON-LD schema\r\n * This explains to AI agents how the scoring system works\r\n */\r\nexport function generateAIOMethodologySchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): AIOMethodologySchema {\r\n return {\r\n '@context': 'https://schema.org',\r\n '@type': 'HowTo',\r\n name: 'AIO Score Calculation Methodology',\r\n description: 'AI Optimization (AIO) Score measures how well a website is optimized for AI agents and LLM crawlers. Scored out of 100 points based on four key metrics.',\r\n step: [\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Content Negotiation',\r\n text: 'Check if the site responds to Accept: text/markdown header. Weight: 40%. Penalty: -30 points if missing. This ensures AI bots receive optimized content instead of heavy HTML.',\r\n position: 1\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Token Efficiency (React Tax)',\r\n text: 'Measure the ratio of visible text to total HTML size. Weight: 35%. Penalty: -30 points if HTML > 100KB but text < 1KB. Detects JavaScript-heavy sites that are difficult for AI to parse.',\r\n position: 2\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Structured Data',\r\n text: 'Verify presence of JSON-LD structured data (Schema.org). Weight: 25%. Penalty: -25 points if missing. Enables AI to confidently extract pricing, products, and entities.',\r\n position: 3\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Semantic HTML',\r\n text: 'Check for semantic tags like <main> and <article>. Bonus: +15 points if present. Helps AI agents separate navigation from core content.',\r\n position: 4\r\n }\r\n ]\r\n };\r\n}\r\n\r\n/**\r\n * Organization schema for About pages\r\n */\r\nexport interface OrganizationSchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'Organization';\r\n name: string;\r\n url?: string;\r\n description?: string;\r\n logo?: string;\r\n foundingDate?: string;\r\n}\r\n\r\n/**\r\n * Generate Organization JSON-LD schema for About pages\r\n */\r\nexport function generateOrganizationSchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): OrganizationSchema | null {\r\n if (!config.organization) {\r\n return null;\r\n }\r\n\r\n const schema: OrganizationSchema = {\r\n '@context': 'https://schema.org',\r\n '@type': 'Organization',\r\n name: config.organization.name\r\n };\r\n\r\n if (config.organization.url) {\r\n schema.url = config.organization.url;\r\n }\r\n\r\n if (config.organization.description) {\r\n schema.description = config.organization.description;\r\n }\r\n\r\n if (config.organization.logo) {\r\n schema.logo = config.organization.logo;\r\n }\r\n\r\n if (config.organization.foundingDate) {\r\n schema.foundingDate = config.organization.foundingDate;\r\n }\r\n\r\n return schema;\r\n}\r\n\r\n/**\r\n * AboutPage schema combining Organization and WebPage\r\n */\r\nexport interface AboutPageSchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'AboutPage';\r\n name: string;\r\n url: string;\r\n description?: string;\r\n mainEntity?: OrganizationSchema;\r\n}\r\n\r\n/**\r\n * Generate AboutPage JSON-LD schema\r\n */\r\nexport function generateAboutPageSchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): AboutPageSchema {\r\n const orgSchema = generateOrganizationSchema(config, pageUrl);\r\n\r\n const schema: AboutPageSchema = {\r\n '@context': 'https://schema.org',\r\n '@type': 'AboutPage',\r\n name: `About ${config.name}`,\r\n url: pageUrl\r\n };\r\n\r\n if (config.summary) {\r\n schema.description = config.summary;\r\n }\r\n\r\n if (orgSchema) {\r\n schema.mainEntity = orgSchema;\r\n }\r\n\r\n return schema;\r\n}\r\n\r\n/**\r\n * Determine which schema to generate based on page type\r\n */\r\nexport function generateSchemaForPageType(\r\n pageType: 'scoring' | 'about' | 'default',\r\n config: OntoConfig,\r\n pageUrl: string\r\n): any | null {\r\n switch (pageType) {\r\n case 'scoring':\r\n return generateAIOMethodologySchema(config, pageUrl);\r\n case 'about':\r\n return generateAboutPageSchema(config, pageUrl);\r\n case 'default':\r\n default:\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Serialize schema to JSON-LD script tag content\r\n */\r\nexport function serializeSchema(schema: any | null): string | null {\r\n if (!schema) {\r\n return null;\r\n }\r\n return JSON.stringify(schema, null, 2);\r\n}\r\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,kCAAAE,EAAA,4BAAAC,EAAA,+BAAAC,EAAA,8BAAAC,EAAA,oBAAAC,IAAA,eAAAC,EAAAP,GAgCO,SAASE,EACdM,EACAC,EACsB,CACtB,MAAO,CACL,WAAY,qBACZ,QAAS,QACT,KAAM,oCACN,YAAa,2JACb,KAAM,CACJ,CACE,QAAS,YACT,KAAM,sBACN,KAAM,iLACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,+BACN,KAAM,4LACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,kBACN,KAAM,2KACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,gBACN,KAAM,0IACN,SAAU,CACZ,CACF,CACF,CACF,CAkBO,SAASL,EACdI,EACAC,EAC2B,CAC3B,GAAI,CAACD,EAAO,aACV,OAAO,KAGT,IAAME,EAA6B,CACjC,WAAY,qBACZ,QAAS,eACT,KAAMF,EAAO,aAAa,IAC5B,EAEA,OAAIA,EAAO,aAAa,MACtBE,EAAO,IAAMF,EAAO,aAAa,KAG/BA,EAAO,aAAa,cACtBE,EAAO,YAAcF,EAAO,aAAa,aAGvCA,EAAO,aAAa,OACtBE,EAAO,KAAOF,EAAO,aAAa,MAGhCA,EAAO,aAAa,eACtBE,EAAO,aAAeF,EAAO,aAAa,cAGrCE,CACT,CAiBO,SAASP,EACdK,EACAC,EACiB,CACjB,IAAME,EAAYP,EAA2BI,EAAQC,CAAO,EAEtDC,EAA0B,CAC9B,WAAY,qBACZ,QAAS,YACT,KAAM,SAASF,EAAO,IAAI,GAC1B,IAAKC,CACP,EAEA,OAAID,EAAO,UACTE,EAAO,YAAcF,EAAO,SAG1BG,IACFD,EAAO,WAAaC,GAGfD,CACT,CAKO,SAASL,EACdO,EACAJ,EACAC,EACY,CACZ,OAAQG,EAAU,CAChB,IAAK,UACH,OAAOV,EAA6BM,EAAQC,CAAO,EACrD,IAAK,QACH,OAAON,EAAwBK,EAAQC,CAAO,EAEhD,QACE,OAAO,IACX,CACF,CAKO,SAASH,EAAgBI,EAAmC,CACjE,OAAKA,EAGE,KAAK,UAAUA,EAAQ,KAAM,CAAC,EAF5B,IAGX","names":["schemas_exports","__export","generateAIOMethodologySchema","generateAboutPageSchema","generateOrganizationSchema","generateSchemaForPageType","serializeSchema","__toCommonJS","config","pageUrl","schema","orgSchema","pageType"]}
|
package/dist/schemas.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function i(t,n){return{"@context":"https://schema.org","@type":"HowTo",name:"AIO Score Calculation Methodology",description:"AI Optimization (AIO) Score measures how well a website is optimized for AI agents and LLM crawlers. Scored out of 100 points based on four key metrics.",step:[{"@type":"HowToStep",name:"Content Negotiation",text:"Check if the site responds to Accept: text/markdown header. Weight: 40%. Penalty: -30 points if missing. This ensures AI bots receive optimized content instead of heavy HTML.",position:1},{"@type":"HowToStep",name:"Token Efficiency (React Tax)",text:"Measure the ratio of visible text to total HTML size. Weight: 35%. Penalty: -30 points if HTML > 100KB but text < 1KB. Detects JavaScript-heavy sites that are difficult for AI to parse.",position:2},{"@type":"HowToStep",name:"Structured Data",text:"Verify presence of JSON-LD structured data (Schema.org). Weight: 25%. Penalty: -25 points if missing. Enables AI to confidently extract pricing, products, and entities.",position:3},{"@type":"HowToStep",name:"Semantic HTML",text:"Check for semantic tags like <main> and <article>. Bonus: +15 points if present. Helps AI agents separate navigation from core content.",position:4}]}}function a(t,n){if(!t.organization)return null;let e={"@context":"https://schema.org","@type":"Organization",name:t.organization.name};return t.organization.url&&(e.url=t.organization.url),t.organization.description&&(e.description=t.organization.description),t.organization.logo&&(e.logo=t.organization.logo),t.organization.foundingDate&&(e.foundingDate=t.organization.foundingDate),e}function r(t,n){let e=a(t,n),o={"@context":"https://schema.org","@type":"AboutPage",name:`About ${t.name}`,url:n};return t.summary&&(o.description=t.summary),e&&(o.mainEntity=e),o}function s(t,n,e){switch(t){case"scoring":return i(n,e);case"about":return r(n,e);default:return null}}function c(t){return t?JSON.stringify(t,null,2):null}export{i as generateAIOMethodologySchema,r as generateAboutPageSchema,a as generateOrganizationSchema,s as generateSchemaForPageType,c as serializeSchema};
|
|
2
|
+
//# sourceMappingURL=schemas.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas.ts"],"sourcesContent":["/**\r\n * JSON-LD Schema generators for automatic structured data injection\r\n * Follows Schema.org standards for AI-friendly metadata\r\n */\r\n\r\nimport { OntoConfig } from './config';\r\n\r\n/**\r\n * Standard AIO (AI Optimization) scoring methodology\r\n * Based on the Onto scoring algorithm:\r\n * - Content Negotiation: 40% (30 points)\r\n * - React Tax / Token Efficiency: 35% (30 points)\r\n * - Structured Data: 25% (25 points)\r\n * - Semantic HTML: Bonus (15 points)\r\n */\r\nexport interface AIOMethodologySchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'HowTo';\r\n name: string;\r\n description: string;\r\n step: Array<{\r\n '@type': 'HowToStep';\r\n name: string;\r\n text: string;\r\n position: number;\r\n }>;\r\n}\r\n\r\n/**\r\n * Generate AIO Scoring Methodology JSON-LD schema\r\n * This explains to AI agents how the scoring system works\r\n */\r\nexport function generateAIOMethodologySchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): AIOMethodologySchema {\r\n return {\r\n '@context': 'https://schema.org',\r\n '@type': 'HowTo',\r\n name: 'AIO Score Calculation Methodology',\r\n description: 'AI Optimization (AIO) Score measures how well a website is optimized for AI agents and LLM crawlers. Scored out of 100 points based on four key metrics.',\r\n step: [\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Content Negotiation',\r\n text: 'Check if the site responds to Accept: text/markdown header. Weight: 40%. Penalty: -30 points if missing. This ensures AI bots receive optimized content instead of heavy HTML.',\r\n position: 1\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Token Efficiency (React Tax)',\r\n text: 'Measure the ratio of visible text to total HTML size. Weight: 35%. Penalty: -30 points if HTML > 100KB but text < 1KB. Detects JavaScript-heavy sites that are difficult for AI to parse.',\r\n position: 2\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Structured Data',\r\n text: 'Verify presence of JSON-LD structured data (Schema.org). Weight: 25%. Penalty: -25 points if missing. Enables AI to confidently extract pricing, products, and entities.',\r\n position: 3\r\n },\r\n {\r\n '@type': 'HowToStep',\r\n name: 'Semantic HTML',\r\n text: 'Check for semantic tags like <main> and <article>. Bonus: +15 points if present. Helps AI agents separate navigation from core content.',\r\n position: 4\r\n }\r\n ]\r\n };\r\n}\r\n\r\n/**\r\n * Organization schema for About pages\r\n */\r\nexport interface OrganizationSchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'Organization';\r\n name: string;\r\n url?: string;\r\n description?: string;\r\n logo?: string;\r\n foundingDate?: string;\r\n}\r\n\r\n/**\r\n * Generate Organization JSON-LD schema for About pages\r\n */\r\nexport function generateOrganizationSchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): OrganizationSchema | null {\r\n if (!config.organization) {\r\n return null;\r\n }\r\n\r\n const schema: OrganizationSchema = {\r\n '@context': 'https://schema.org',\r\n '@type': 'Organization',\r\n name: config.organization.name\r\n };\r\n\r\n if (config.organization.url) {\r\n schema.url = config.organization.url;\r\n }\r\n\r\n if (config.organization.description) {\r\n schema.description = config.organization.description;\r\n }\r\n\r\n if (config.organization.logo) {\r\n schema.logo = config.organization.logo;\r\n }\r\n\r\n if (config.organization.foundingDate) {\r\n schema.foundingDate = config.organization.foundingDate;\r\n }\r\n\r\n return schema;\r\n}\r\n\r\n/**\r\n * AboutPage schema combining Organization and WebPage\r\n */\r\nexport interface AboutPageSchema {\r\n '@context': 'https://schema.org';\r\n '@type': 'AboutPage';\r\n name: string;\r\n url: string;\r\n description?: string;\r\n mainEntity?: OrganizationSchema;\r\n}\r\n\r\n/**\r\n * Generate AboutPage JSON-LD schema\r\n */\r\nexport function generateAboutPageSchema(\r\n config: OntoConfig,\r\n pageUrl: string\r\n): AboutPageSchema {\r\n const orgSchema = generateOrganizationSchema(config, pageUrl);\r\n\r\n const schema: AboutPageSchema = {\r\n '@context': 'https://schema.org',\r\n '@type': 'AboutPage',\r\n name: `About ${config.name}`,\r\n url: pageUrl\r\n };\r\n\r\n if (config.summary) {\r\n schema.description = config.summary;\r\n }\r\n\r\n if (orgSchema) {\r\n schema.mainEntity = orgSchema;\r\n }\r\n\r\n return schema;\r\n}\r\n\r\n/**\r\n * Determine which schema to generate based on page type\r\n */\r\nexport function generateSchemaForPageType(\r\n pageType: 'scoring' | 'about' | 'default',\r\n config: OntoConfig,\r\n pageUrl: string\r\n): any | null {\r\n switch (pageType) {\r\n case 'scoring':\r\n return generateAIOMethodologySchema(config, pageUrl);\r\n case 'about':\r\n return generateAboutPageSchema(config, pageUrl);\r\n case 'default':\r\n default:\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Serialize schema to JSON-LD script tag content\r\n */\r\nexport function serializeSchema(schema: any | null): string | null {\r\n if (!schema) {\r\n return null;\r\n }\r\n return JSON.stringify(schema, null, 2);\r\n}\r\n"],"mappings":"AAgCO,SAASA,EACdC,EACAC,EACsB,CACtB,MAAO,CACL,WAAY,qBACZ,QAAS,QACT,KAAM,oCACN,YAAa,2JACb,KAAM,CACJ,CACE,QAAS,YACT,KAAM,sBACN,KAAM,iLACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,+BACN,KAAM,4LACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,kBACN,KAAM,2KACN,SAAU,CACZ,EACA,CACE,QAAS,YACT,KAAM,gBACN,KAAM,0IACN,SAAU,CACZ,CACF,CACF,CACF,CAkBO,SAASC,EACdF,EACAC,EAC2B,CAC3B,GAAI,CAACD,EAAO,aACV,OAAO,KAGT,IAAMG,EAA6B,CACjC,WAAY,qBACZ,QAAS,eACT,KAAMH,EAAO,aAAa,IAC5B,EAEA,OAAIA,EAAO,aAAa,MACtBG,EAAO,IAAMH,EAAO,aAAa,KAG/BA,EAAO,aAAa,cACtBG,EAAO,YAAcH,EAAO,aAAa,aAGvCA,EAAO,aAAa,OACtBG,EAAO,KAAOH,EAAO,aAAa,MAGhCA,EAAO,aAAa,eACtBG,EAAO,aAAeH,EAAO,aAAa,cAGrCG,CACT,CAiBO,SAASC,EACdJ,EACAC,EACiB,CACjB,IAAMI,EAAYH,EAA2BF,EAAQC,CAAO,EAEtDE,EAA0B,CAC9B,WAAY,qBACZ,QAAS,YACT,KAAM,SAASH,EAAO,IAAI,GAC1B,IAAKC,CACP,EAEA,OAAID,EAAO,UACTG,EAAO,YAAcH,EAAO,SAG1BK,IACFF,EAAO,WAAaE,GAGfF,CACT,CAKO,SAASG,EACdC,EACAP,EACAC,EACY,CACZ,OAAQM,EAAU,CAChB,IAAK,UACH,OAAOR,EAA6BC,EAAQC,CAAO,EACrD,IAAK,QACH,OAAOG,EAAwBJ,EAAQC,CAAO,EAEhD,QACE,OAAO,IACX,CACF,CAKO,SAASO,EAAgBL,EAAmC,CACjE,OAAKA,EAGE,KAAK,UAAUA,EAAQ,KAAM,CAAC,EAF5B,IAGX","names":["generateAIOMethodologySchema","config","pageUrl","generateOrganizationSchema","schema","generateAboutPageSchema","orgSchema","generateSchemaForPageType","pageType","serializeSchema"]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { OntoConfig } from './src/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example onto.config.ts file
|
|
5
|
+
*
|
|
6
|
+
* Place this file in your project root as `onto.config.ts` to enable
|
|
7
|
+
* dynamic llms.txt generation.
|
|
8
|
+
*
|
|
9
|
+
* The middleware will automatically read this config and generate
|
|
10
|
+
* /llms.txt on the fly when AI agents request it.
|
|
11
|
+
*/
|
|
12
|
+
const config: OntoConfig = {
|
|
13
|
+
// Required: The name of your project/site
|
|
14
|
+
name: 'My Awesome Project',
|
|
15
|
+
|
|
16
|
+
// Required: A concise summary for AI agents
|
|
17
|
+
summary: 'A modern web application for managing tasks and projects. Built with Next.js, TypeScript, and React. Provides a RESTful API and interactive UI.',
|
|
18
|
+
|
|
19
|
+
// Required: Base URL of your site
|
|
20
|
+
baseUrl: 'https://example.com',
|
|
21
|
+
|
|
22
|
+
// Optional: Key routes that AI agents should know about
|
|
23
|
+
routes: [
|
|
24
|
+
{
|
|
25
|
+
path: '/',
|
|
26
|
+
description: 'Homepage with product overview and key features',
|
|
27
|
+
pageType: 'default'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: '/score',
|
|
31
|
+
description: 'AIO Score Calculator - check your AI optimization score',
|
|
32
|
+
pageType: 'scoring' // Automatically injects Methodology JSON-LD schema
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
path: '/about',
|
|
36
|
+
description: 'About our company and mission',
|
|
37
|
+
pageType: 'about' // Automatically injects Organization/AboutPage JSON-LD schema
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
path: '/docs',
|
|
41
|
+
description: 'Complete API documentation and integration guides'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/docs/getting-started',
|
|
45
|
+
description: 'Quick start guide for new users'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: '/api/reference',
|
|
49
|
+
description: 'REST API reference with all endpoints and schemas'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
path: '/blog',
|
|
53
|
+
description: 'Technical blog posts and product updates'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: '/pricing',
|
|
57
|
+
description: 'Pricing plans and feature comparison'
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
// Optional: External resources
|
|
62
|
+
externalLinks: [
|
|
63
|
+
{
|
|
64
|
+
title: 'GitHub Repository',
|
|
65
|
+
url: 'https://github.com/example/project',
|
|
66
|
+
description: 'Open-source code and issue tracker'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'API Status',
|
|
70
|
+
url: 'https://status.example.com',
|
|
71
|
+
description: 'Real-time API status and incident reports'
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
// Optional: Organization info for JSON-LD schemas (used on 'about' pages)
|
|
76
|
+
organization: {
|
|
77
|
+
name: 'Example Corp',
|
|
78
|
+
description: 'Building the future of AI-optimized web applications',
|
|
79
|
+
url: 'https://example.com',
|
|
80
|
+
logo: 'https://example.com/logo.png',
|
|
81
|
+
foundingDate: '2024-01-01'
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Optional: Custom sections with any markdown content
|
|
85
|
+
sections: [
|
|
86
|
+
{
|
|
87
|
+
heading: 'About',
|
|
88
|
+
content: `This project helps teams collaborate more effectively by providing
|
|
89
|
+
a unified workspace for tasks, projects, and documentation. It integrates with
|
|
90
|
+
popular tools like GitHub, Slack, and Jira.`
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
heading: 'Key Features',
|
|
94
|
+
content: `- **Task Management**: Create, assign, and track tasks across teams
|
|
95
|
+
- **Real-time Collaboration**: WebSocket-based updates for instant sync
|
|
96
|
+
- **Rich API**: RESTful API with comprehensive documentation
|
|
97
|
+
- **Integrations**: Connect with 50+ external services
|
|
98
|
+
- **Security**: SOC 2 Type II certified with end-to-end encryption`
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
heading: 'Use Cases',
|
|
102
|
+
content: `**For Development Teams**: Track sprints, manage backlogs, and integrate with CI/CD pipelines.
|
|
103
|
+
|
|
104
|
+
**For Content Teams**: Plan editorial calendars, collaborate on drafts, and publish content.
|
|
105
|
+
|
|
106
|
+
**For Product Teams**: Gather feedback, prioritize features, and communicate roadmaps.`
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontosdk/next",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Extracts semantic Markdown from React/Next.js pages for AI Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -18,6 +18,21 @@
|
|
|
18
18
|
"require": "./dist/middleware.js",
|
|
19
19
|
"import": "./dist/middleware.mjs",
|
|
20
20
|
"types": "./dist/middleware.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./components": {
|
|
23
|
+
"require": "./dist/OntoHead.js",
|
|
24
|
+
"import": "./dist/OntoHead.mjs",
|
|
25
|
+
"types": "./dist/OntoHead.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./provider": {
|
|
28
|
+
"require": "./dist/OntoProvider.js",
|
|
29
|
+
"import": "./dist/OntoProvider.mjs",
|
|
30
|
+
"types": "./dist/OntoProvider.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./config": {
|
|
33
|
+
"require": "./dist/config.js",
|
|
34
|
+
"import": "./dist/config.mjs",
|
|
35
|
+
"types": "./dist/config.d.ts"
|
|
21
36
|
}
|
|
22
37
|
},
|
|
23
38
|
"bin": {
|
|
@@ -43,11 +58,22 @@
|
|
|
43
58
|
"picocolors": "^1.0.0",
|
|
44
59
|
"turndown": "^7.1.3"
|
|
45
60
|
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"next": ">=14.0.0",
|
|
63
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"react": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
46
70
|
"devDependencies": {
|
|
47
71
|
"@types/node": "^20.11.24",
|
|
72
|
+
"@types/react": "^19.2.14",
|
|
48
73
|
"@types/turndown": "^5.0.4",
|
|
49
74
|
"next": "^16.1.6",
|
|
75
|
+
"react": "^19.2.4",
|
|
50
76
|
"tsup": "^8.0.2",
|
|
51
77
|
"typescript": "^5.3.3"
|
|
52
78
|
}
|
|
53
|
-
}
|
|
79
|
+
}
|
package/src/OntoHead.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OntoHead — Auto-Discovery component for AI agents.
|
|
7
|
+
*
|
|
8
|
+
* Injects `<link rel="alternate">` tags into the page `<head>` so AI crawlers
|
|
9
|
+
* can discover the optimized markdown endpoint for the current route.
|
|
10
|
+
*
|
|
11
|
+
* Usage in a Next.js App Router layout:
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { OntoHead } from '@ontosdk/next/components';
|
|
14
|
+
*
|
|
15
|
+
* export default function RootLayout({ children }) {
|
|
16
|
+
* return (
|
|
17
|
+
* <html>
|
|
18
|
+
* <head>
|
|
19
|
+
* <OntoHead />
|
|
20
|
+
* </head>
|
|
21
|
+
* <body>{children}</body>
|
|
22
|
+
* </html>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function OntoHead() {
|
|
28
|
+
const pathname = usePathname();
|
|
29
|
+
|
|
30
|
+
// Map current route to its .onto markdown payload path
|
|
31
|
+
let payloadPath = pathname;
|
|
32
|
+
if (payloadPath === '/' || payloadPath === '') {
|
|
33
|
+
payloadPath = '/index';
|
|
34
|
+
}
|
|
35
|
+
if (payloadPath.endsWith('/') && payloadPath !== '/') {
|
|
36
|
+
payloadPath = payloadPath.slice(0, -1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const markdownHref = `/.onto${payloadPath}.md`;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{/* Per-page markdown alternate for AI agents */}
|
|
44
|
+
<link
|
|
45
|
+
rel="alternate"
|
|
46
|
+
type="text/markdown"
|
|
47
|
+
href={markdownHref}
|
|
48
|
+
title="AI-optimized Markdown version"
|
|
49
|
+
/>
|
|
50
|
+
{/* Site-wide llms.txt manifest */}
|
|
51
|
+
<link
|
|
52
|
+
rel="alternate"
|
|
53
|
+
type="text/plain"
|
|
54
|
+
href="/llms.txt"
|
|
55
|
+
title="LLM-readable site manifest"
|
|
56
|
+
/>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|