Skip to content

Commit 2daa27a

Browse files
DC-5820 AI Agents Served Markdown (#7237)
* ai crawler check successful * ai crawlers checked * update * added anthropic * middleware update * improve detection --------- Co-authored-by: Mike Hartington <mikehartington@gmail.com>
1 parent ddfc555 commit 2daa27a

5 files changed

Lines changed: 382 additions & 5 deletions

File tree

docusaurus.config.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { themes as prismThemes } from "prism-react-renderer";
44

55
import type { Config, RouteConfig } from "@docusaurus/types";
66
import type * as Preset from "@docusaurus/preset-classic";
7+
import type { PluginOptions } from "@signalwire/docusaurus-plugin-llms-txt/public";
78

89
const DOCUSAURUS_BASE_URL = process.env.DOCUSAURUS_BASE_URL ?? "/";
910

@@ -91,6 +92,21 @@ const config: Config = {
9192
enableInDevelopment: false,
9293
},
9394
],
95+
[
96+
"@signalwire/docusaurus-plugin-llms-txt",
97+
{
98+
// Enable with defaults
99+
generate: {
100+
enableMarkdownFiles: true,
101+
enableLlmsFullTxt: false,
102+
},
103+
include: {
104+
includeBlog: false,
105+
includePages: false,
106+
includeDocs: true,
107+
},
108+
} satisfies PluginOptions,
109+
],
94110
async function pluginLlmsTxt(context) {
95111
return {
96112
name: "llms-txt-plugin",
@@ -297,31 +313,31 @@ const config: Config = {
297313
to: "/postgres/database/prisma-studio",
298314
label: "Studio",
299315
sub: "Explore and manipulate your data",
300-
icon: "fa-regular fa-table"
316+
icon: "fa-regular fa-table",
301317
},
302318
{
303319
type: "docSidebar",
304320
sidebarId: "optimizeSidebar",
305321
className: "teal",
306322
label: "Optimize",
307323
sub: "AI-driven query analysis",
308-
icon: "fa-regular fa-magnifying-glass-chart"
324+
icon: "fa-regular fa-magnifying-glass-chart",
309325
},
310326
{
311327
type: "docSidebar",
312328
sidebarId: "accelerateSidebar",
313329
className: "teal",
314330
label: "Accelerate",
315331
sub: "Make your database global",
316-
icon: "fa-regular fa-bolt"
332+
icon: "fa-regular fa-bolt",
317333
},
318334
{
319335
type: "docSidebar",
320336
className: "teal",
321337
sidebarId: "aiSidebar",
322338
label: "Prisma + AI",
323339
sub: "Build faster with Prisma + AI",
324-
icon: "fa-regular fa-robot"
340+
icon: "fa-regular fa-robot",
325341
},
326342
],
327343
},

functions/_middleware.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
interface Env {
2+
ASSETS: {
3+
fetch: typeof fetch;
4+
};
5+
}
6+
7+
const AI_CRAWLER_PATTERNS = [
8+
'GPTBot',
9+
'OAI-SearchBot',
10+
'ChatGPT-User',
11+
'ChatGPT-User 2.0',
12+
'anthropic-ai',
13+
'OpenAI',
14+
'ClaudeBot',
15+
'claude-web',
16+
'Claude/',
17+
'Claude-User',
18+
'anthropic',
19+
'PerplexityBot',
20+
'Perplexity-User',
21+
'Google-Extended',
22+
'BingBot',
23+
'Amazonbot',
24+
'Applebot',
25+
'Applebot-Extended',
26+
'FacebookBot',
27+
'meta-externalagent',
28+
'LinkedInBot',
29+
'Bytespider',
30+
'AI2Bot',
31+
'CCBot',
32+
'Diffbot',
33+
'DuckAssistBot',
34+
'DuckDuckGo',
35+
'DeepSeekBot',
36+
'cohere-ai',
37+
'omgili',
38+
'TimpiBot',
39+
'YouBot',
40+
'MistralAI-User',
41+
'ImagesiftBot',
42+
'Scrapy',
43+
44+
'chatgpt',
45+
'openai',
46+
'gpt',
47+
'claude',
48+
'anthropic',
49+
'cursor',
50+
'windsurf',
51+
'perplexity',
52+
'copilot',
53+
'ai-agent',
54+
'llm-agent',
55+
];
56+
57+
function isAICrawler(request: Request): boolean {
58+
// if (!userAgent) return false;
59+
60+
const userAgent = request.headers.get('user-agent') || '';
61+
const accept = request.headers.get('accept') || '';
62+
63+
const hasHtml = accept.includes('text/html');
64+
65+
const prefersNonHtml =
66+
!hasHtml &&
67+
(accept.includes('application/json') ||
68+
accept.includes('text/plain') ||
69+
accept.includes('application/xml'));
70+
71+
const hasAiUserAgent = AI_CRAWLER_PATTERNS.some((pattern) => userAgent.toLowerCase().includes(pattern.toLowerCase()));
72+
73+
return prefersNonHtml || hasAiUserAgent
74+
75+
// const normalizedUA = userAgent.toLowerCase();
76+
// return AI_CRAWLER_PATTERNS.some((pattern) => normalizedUA.includes(pattern.toLowerCase()));
77+
}
78+
79+
export const onRequest: PagesFunction<Env> = async (context) => {
80+
const { request, next } = context;
81+
82+
if (isAICrawler(request)) {
83+
const url = new URL(request.url);
84+
const urlPath = url.pathname;
85+
86+
const markdownPath = `${urlPath}.md`;
87+
88+
const markdownRequest = new Request(new URL(markdownPath, url.origin), {
89+
method: request.method,
90+
headers: request.headers,
91+
});
92+
93+
const response = await context.env.ASSETS.fetch(markdownRequest);
94+
95+
if (response.ok) {
96+
// Check what content type we actually got
97+
const actualContentType = response.headers.get('content-type');
98+
console.log(`Fetched ${markdownPath}, got content-type: ${actualContentType}`);
99+
100+
return new Response(response.body, {
101+
status: 200,
102+
headers: {
103+
'Content-Type': 'text/markdown; charset=utf-8',
104+
'Cache-Control': 'public, max-age=3600',
105+
},
106+
});
107+
}
108+
109+
return next();
110+
}
111+
112+
return next();
113+
};

functions/types.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// TypeScript definitions for Cloudflare Pages Functions
2+
interface EventContext<Env = unknown> {
3+
request: Request;
4+
env: Env;
5+
params: Record<string, string>;
6+
data: Record<string, unknown>;
7+
next: () => Promise<Response>;
8+
waitUntil: (promise: Promise<unknown>) => void;
9+
passThroughOnException: () => void;
10+
}
11+
12+
type PagesFunction<Env = unknown> = (context: EventContext<Env>) => Response | Promise<Response>;

0 commit comments

Comments
 (0)