third-audience-mdx 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORKLOG.md +10 -0
- package/dist/dashboard/routes/markdown-route.d.mts +2 -2
- package/dist/dashboard/routes/markdown-route.d.ts +2 -2
- package/dist/dashboard/routes/markdown-route.js +182 -4
- package/dist/dashboard/routes/markdown-route.js.map +1 -1
- package/dist/dashboard/routes/markdown-route.mjs +189 -4
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -1
- package/dist/dashboard/routes/okf-route.d.mts +2 -2
- package/dist/dashboard/routes/okf-route.d.ts +2 -2
- package/dist/dashboard/routes/okf-route.js +2 -1
- package/dist/dashboard/routes/okf-route.js.map +1 -1
- package/dist/dashboard/routes/okf-route.mjs +2 -1
- package/dist/dashboard/routes/okf-route.mjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/WORKLOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Newest entries at top.
|
|
4
4
|
|
|
5
|
+
## 2026-06-21
|
|
6
|
+
|
|
7
|
+
**Task:** Add missing `bots-config` route to the package so the init-generated template file resolves correctly.
|
|
8
|
+
|
|
9
|
+
**Done:**
|
|
10
|
+
- Created `src/dashboard/routes/bots-config-route.ts` — GET/POST handler reading/writing `ta-bots-config.json`, auth via `checkApiAuth`
|
|
11
|
+
- Simplified `src/nextjs-app/app/api/third-audience/bots-config/route.ts` to re-export from `third-audience-mdx/routes/bots-config`
|
|
12
|
+
- Added `./routes/bots-config` export to `package.json`, bumped version to `1.0.4`
|
|
13
|
+
- Built dist, committed, published v1.0.4 to npm
|
|
14
|
+
|
|
5
15
|
---
|
|
6
16
|
|
|
7
17
|
## 2026-06-21 (session 6 — git init + push)
|
|
@@ -7,9 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
7
7
|
* app/api/third-audience/markdown/[...slug]/route.ts
|
|
8
8
|
*/
|
|
9
9
|
declare function GET(req: NextRequest, { params }: {
|
|
10
|
-
params: {
|
|
10
|
+
params: Promise<{
|
|
11
11
|
slug: string[];
|
|
12
|
-
}
|
|
12
|
+
}>;
|
|
13
13
|
}): Promise<NextResponse<unknown>>;
|
|
14
14
|
|
|
15
15
|
export { GET };
|
|
@@ -7,9 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
7
7
|
* app/api/third-audience/markdown/[...slug]/route.ts
|
|
8
8
|
*/
|
|
9
9
|
declare function GET(req: NextRequest, { params }: {
|
|
10
|
-
params: {
|
|
10
|
+
params: Promise<{
|
|
11
11
|
slug: string[];
|
|
12
|
-
}
|
|
12
|
+
}>;
|
|
13
13
|
}): Promise<NextResponse<unknown>>;
|
|
14
14
|
|
|
15
15
|
export { GET };
|
|
@@ -34,7 +34,7 @@ __export(markdown_route_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(markdown_route_exports);
|
|
36
36
|
var import_server = require("next/server");
|
|
37
|
-
var
|
|
37
|
+
var import_path4 = __toESM(require("path"));
|
|
38
38
|
|
|
39
39
|
// src/core/mdx-reader.ts
|
|
40
40
|
var import_fs = __toESM(require("fs"));
|
|
@@ -201,17 +201,190 @@ var CacheManager = class {
|
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
+
// src/analytics/visit-tracker.ts
|
|
205
|
+
var import_fs3 = __toESM(require("fs"));
|
|
206
|
+
var import_path3 = __toESM(require("path"));
|
|
207
|
+
|
|
208
|
+
// src/detection/known-patterns.ts
|
|
209
|
+
var KNOWN_BOTS = [
|
|
210
|
+
// AI Crawlers
|
|
211
|
+
{ name: "ClaudeBot", category: "ai_crawler", patterns: [/claudebot/i, /claude-web/i] },
|
|
212
|
+
{ name: "GPTBot", category: "ai_crawler", patterns: [/gptbot/i] },
|
|
213
|
+
{ name: "ChatGPT-User", category: "ai_crawler", patterns: [/chatgpt-user/i] },
|
|
214
|
+
{ name: "PerplexityBot", category: "ai_crawler", patterns: [/perplexitybot/i] },
|
|
215
|
+
{ name: "Googlebot-AI", category: "ai_crawler", patterns: [/google-extended/i, /googleother/i] },
|
|
216
|
+
{ name: "FacebookBot", category: "ai_crawler", patterns: [/facebookbot/i] },
|
|
217
|
+
{ name: "Applebot-Extended", category: "ai_crawler", patterns: [/applebot-extended/i] },
|
|
218
|
+
{ name: "YouBot", category: "ai_crawler", patterns: [/youbot/i] },
|
|
219
|
+
{ name: "CCBot", category: "ai_crawler", patterns: [/ccbot/i] },
|
|
220
|
+
{ name: "CohereCrawler", category: "ai_crawler", patterns: [/cohere-ai/i] },
|
|
221
|
+
{ name: "AI2Bot", category: "ai_crawler", patterns: [/ai2bot/i] },
|
|
222
|
+
{ name: "Bytespider", category: "ai_crawler", patterns: [/bytespider/i] },
|
|
223
|
+
{ name: "Diffbot", category: "ai_crawler", patterns: [/diffbot/i] },
|
|
224
|
+
// Search Engines
|
|
225
|
+
{ name: "Googlebot", category: "search_engine", patterns: [/googlebot/i] },
|
|
226
|
+
{ name: "Bingbot", category: "search_engine", patterns: [/bingbot/i, /msnbot/i] },
|
|
227
|
+
{ name: "DuckDuckBot", category: "search_engine", patterns: [/duckduckbot/i] },
|
|
228
|
+
{ name: "Baiduspider", category: "search_engine", patterns: [/baiduspider/i] },
|
|
229
|
+
{ name: "YandexBot", category: "search_engine", patterns: [/yandexbot/i] },
|
|
230
|
+
{ name: "Sogou", category: "search_engine", patterns: [/sogou/i] },
|
|
231
|
+
{ name: "Exabot", category: "search_engine", patterns: [/exabot/i] },
|
|
232
|
+
{ name: "ia_archiver", category: "search_engine", patterns: [/ia_archiver/i] }
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// src/detection/bot-detection-pipeline.ts
|
|
236
|
+
function detectBot(input) {
|
|
237
|
+
const ua = input.userAgent ?? "";
|
|
238
|
+
for (const bot of KNOWN_BOTS) {
|
|
239
|
+
for (const pattern of bot.patterns) {
|
|
240
|
+
if (pattern.test(ua)) {
|
|
241
|
+
return {
|
|
242
|
+
isBot: true,
|
|
243
|
+
botName: bot.name,
|
|
244
|
+
confidence: "high",
|
|
245
|
+
detectionMethod: "known_pattern",
|
|
246
|
+
category: bot.category,
|
|
247
|
+
rawUserAgent: ua
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const heuristicResult = checkHeuristics(ua, input.headers ?? {});
|
|
253
|
+
if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua };
|
|
254
|
+
if (looksLikeBotUa(ua)) {
|
|
255
|
+
return {
|
|
256
|
+
isBot: true,
|
|
257
|
+
botName: null,
|
|
258
|
+
confidence: "low",
|
|
259
|
+
detectionMethod: "auto_learned",
|
|
260
|
+
category: "unknown_bot",
|
|
261
|
+
rawUserAgent: ua
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
isBot: false,
|
|
266
|
+
botName: null,
|
|
267
|
+
confidence: "high",
|
|
268
|
+
detectionMethod: "none",
|
|
269
|
+
category: "human",
|
|
270
|
+
rawUserAgent: ua
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function checkHeuristics(ua, headers) {
|
|
274
|
+
if (/headlesschrome/i.test(ua)) {
|
|
275
|
+
return { isBot: true, botName: "HeadlessChrome", confidence: "medium", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
276
|
+
}
|
|
277
|
+
if (/phantomjs/i.test(ua)) {
|
|
278
|
+
return { isBot: true, botName: "PhantomJS", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
279
|
+
}
|
|
280
|
+
if (/selenium/i.test(ua)) {
|
|
281
|
+
return { isBot: true, botName: "Selenium", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
282
|
+
}
|
|
283
|
+
if (ua.trim().length < 10) {
|
|
284
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
285
|
+
}
|
|
286
|
+
const hasAcceptLang = !!headers["accept-language"];
|
|
287
|
+
const hasAcceptEncoding = !!headers["accept-encoding"];
|
|
288
|
+
const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua);
|
|
289
|
+
if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {
|
|
290
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
function looksLikeBotUa(ua) {
|
|
295
|
+
return /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) && !/chrome|firefox|safari|edge|opera/i.test(ua);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/analytics/geolocation.ts
|
|
299
|
+
var geoip = null;
|
|
300
|
+
function loadGeoip() {
|
|
301
|
+
if (geoip) return geoip;
|
|
302
|
+
try {
|
|
303
|
+
geoip = require("geoip-lite");
|
|
304
|
+
} catch {
|
|
305
|
+
geoip = null;
|
|
306
|
+
}
|
|
307
|
+
return geoip;
|
|
308
|
+
}
|
|
309
|
+
function getCountry(ip) {
|
|
310
|
+
if (!ip || ip === "unknown" || ip === "127.0.0.1" || ip.startsWith("::")) return null;
|
|
311
|
+
const geo = loadGeoip();
|
|
312
|
+
if (!geo) return null;
|
|
313
|
+
try {
|
|
314
|
+
const result = geo.lookup(ip);
|
|
315
|
+
return result?.country ?? null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/analytics/visit-tracker.ts
|
|
322
|
+
var _VisitTracker = class _VisitTracker {
|
|
323
|
+
constructor(dataDir) {
|
|
324
|
+
this.dataDir = dataDir;
|
|
325
|
+
}
|
|
326
|
+
static getInstance(dataDir = process.env.TA_DATA_DIR ?? "data") {
|
|
327
|
+
if (!_VisitTracker.instance) {
|
|
328
|
+
_VisitTracker.instance = new _VisitTracker(dataDir);
|
|
329
|
+
}
|
|
330
|
+
return _VisitTracker.instance;
|
|
331
|
+
}
|
|
332
|
+
record(req, meta = {}) {
|
|
333
|
+
const ua = req.headers.get("user-agent") ?? "";
|
|
334
|
+
const headers = {};
|
|
335
|
+
req.headers.forEach((value, key) => {
|
|
336
|
+
headers[key] = value;
|
|
337
|
+
});
|
|
338
|
+
const result = detectBot({ userAgent: ua, headers });
|
|
339
|
+
if (!result.isBot) return;
|
|
340
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
341
|
+
const record = {
|
|
342
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
343
|
+
bot_name: result.botName,
|
|
344
|
+
bot_category: result.category,
|
|
345
|
+
detection_method: result.detectionMethod,
|
|
346
|
+
confidence: result.confidence,
|
|
347
|
+
url: req.nextUrl.pathname,
|
|
348
|
+
ip,
|
|
349
|
+
country: getCountry(ip),
|
|
350
|
+
user_agent: ua,
|
|
351
|
+
referer: req.headers.get("referer"),
|
|
352
|
+
response_ms: meta.responseMs ?? null,
|
|
353
|
+
cache_hit: meta.cacheHit ?? false,
|
|
354
|
+
content_length: meta.contentLength ?? null
|
|
355
|
+
};
|
|
356
|
+
this.append("ta-visits.jsonl", record);
|
|
357
|
+
}
|
|
358
|
+
append(filename, record) {
|
|
359
|
+
try {
|
|
360
|
+
const filePath = import_path3.default.join(this.dataDir, filename);
|
|
361
|
+
import_fs3.default.mkdirSync(this.dataDir, { recursive: true });
|
|
362
|
+
import_fs3.default.appendFileSync(filePath, JSON.stringify(record) + "\n", "utf-8");
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
_VisitTracker.instance = null;
|
|
368
|
+
var VisitTracker = _VisitTracker;
|
|
369
|
+
|
|
204
370
|
// src/dashboard/routes/markdown-route.ts
|
|
205
|
-
var reader = new MdxReader({ contentDir:
|
|
371
|
+
var reader = new MdxReader({ contentDir: import_path4.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content") });
|
|
206
372
|
var renderer = new MarkdownRenderer();
|
|
207
373
|
var cache = new CacheManager({
|
|
208
|
-
cacheDir:
|
|
374
|
+
cacheDir: import_path4.default.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")
|
|
209
375
|
});
|
|
210
376
|
async function GET(req, { params }) {
|
|
211
|
-
const
|
|
377
|
+
const startedAt = Date.now();
|
|
378
|
+
const { slug: slugParts } = await params;
|
|
379
|
+
const slug = slugParts.join("/");
|
|
212
380
|
const cacheKey = `markdown:${slug}`;
|
|
213
381
|
const cached = cache.get(cacheKey);
|
|
214
382
|
if (cached) {
|
|
383
|
+
VisitTracker.getInstance().record(req, {
|
|
384
|
+
responseMs: Date.now() - startedAt,
|
|
385
|
+
cacheHit: true,
|
|
386
|
+
contentLength: cached.length
|
|
387
|
+
});
|
|
215
388
|
return new import_server.NextResponse(cached, {
|
|
216
389
|
headers: {
|
|
217
390
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
@@ -225,6 +398,11 @@ async function GET(req, { params }) {
|
|
|
225
398
|
}
|
|
226
399
|
const markdown = renderer.render(file);
|
|
227
400
|
cache.set(cacheKey, markdown);
|
|
401
|
+
VisitTracker.getInstance().record(req, {
|
|
402
|
+
responseMs: Date.now() - startedAt,
|
|
403
|
+
cacheHit: false,
|
|
404
|
+
contentLength: markdown.length
|
|
405
|
+
});
|
|
228
406
|
return new import_server.NextResponse(markdown, {
|
|
229
407
|
headers: {
|
|
230
408
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/markdown-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/cache/cache-manager.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../../core/markdown-renderer.js'\nimport { CacheManager } from '../../cache/cache-manager.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\nconst renderer = new MarkdownRenderer()\nconst cache = new CacheManager({\n cacheDir: path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-cache'),\n})\n\n/**\n * Handler for GET /api/third-audience/markdown/[...slug]\n *\n * Install in your Next.js app at:\n * app/api/third-audience/markdown/[...slug]/route.ts\n */\nexport async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) {\n const slug = params.slug.join('/')\n const cacheKey = `markdown:${slug}`\n\n const cached = cache.get(cacheKey)\n if (cached) {\n return new NextResponse(cached, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'HIT',\n },\n })\n }\n\n const file = reader.read(slug)\n if (!file) {\n return new NextResponse('Not Found', { status: 404 })\n }\n\n const markdown = renderer.render(file)\n cache.set(cacheKey, markdown)\n\n return new NextResponse(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'MISS',\n },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\ninterface CacheEntry {\n content: string\n etag: string\n cachedAt: number\n ttl: number\n}\n\n/**\n * Two-tier cache:\n * 1. In-memory LRU (per Node.js process, instant)\n * 2. File-system cache in data/ta-cache/ (survives restarts)\n */\nexport class CacheManager {\n private memCache = new Map<string, CacheEntry>()\n private cacheDir: string\n private maxMemoryEntries: number\n private defaultTtl: number\n\n constructor(opts: { cacheDir: string; maxMemoryEntries?: number; ttl?: number }) {\n this.cacheDir = opts.cacheDir\n this.maxMemoryEntries = opts.maxMemoryEntries ?? 500\n this.defaultTtl = opts.ttl ?? 3600\n }\n\n get(key: string): string | null {\n // Check memory first\n const mem = this.memCache.get(key)\n if (mem && this.isValid(mem)) return mem.content\n if (mem) this.memCache.delete(key)\n\n // Check file cache\n const file = this.readFileCache(key)\n if (file && this.isValid(file)) {\n this.setMemory(key, file)\n return file.content\n }\n\n return null\n }\n\n set(key: string, content: string, etag = '', ttl = this.defaultTtl): void {\n const entry: CacheEntry = { content, etag, cachedAt: Date.now(), ttl }\n this.setMemory(key, entry)\n this.writeFileCache(key, entry)\n }\n\n /** Invalidate by key prefix — used when source .mdx file changes. */\n invalidate(keyPrefix: string): void {\n for (const k of this.memCache.keys()) {\n if (k.startsWith(keyPrefix)) this.memCache.delete(k)\n }\n const dir = this.cacheDir\n if (!fs.existsSync(dir)) return\n for (const file of fs.readdirSync(dir)) {\n if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {\n fs.unlinkSync(path.join(dir, file))\n }\n }\n }\n\n stats(): { memEntries: number; fsEntries: number } {\n const fsEntries = fs.existsSync(this.cacheDir)\n ? fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length\n : 0\n return { memEntries: this.memCache.size, fsEntries }\n }\n\n private isValid(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt < entry.ttl * 1000\n }\n\n private setMemory(key: string, entry: CacheEntry): void {\n if (this.memCache.size >= this.maxMemoryEntries) {\n // Evict oldest entry\n const firstKey = this.memCache.keys().next().value\n if (firstKey) this.memCache.delete(firstKey)\n }\n this.memCache.set(key, entry)\n }\n\n private hashKey(key: string): string {\n return crypto.createHash('sha256').update(key).digest('hex')\n }\n\n private filePath(key: string): string {\n return path.join(this.cacheDir, `${this.hashKey(key)}.json`)\n }\n\n private readFileCache(key: string): CacheEntry | null {\n const fp = this.filePath(key)\n if (!fs.existsSync(fp)) return null\n try {\n return JSON.parse(fs.readFileSync(fp, 'utf-8')) as CacheEntry\n } catch {\n return null\n }\n }\n\n private writeFileCache(key: string, entry: CacheEntry): void {\n try {\n fs.mkdirSync(this.cacheDir, { recursive: true })\n fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8')\n } catch {\n // Cache writes must never throw\n }\n }\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","import_fs","import_path","fs","path","crypto","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/markdown-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/cache/cache-manager.ts","../../../src/analytics/visit-tracker.ts","../../../src/detection/known-patterns.ts","../../../src/detection/bot-detection-pipeline.ts","../../../src/analytics/geolocation.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../../core/markdown-renderer.js'\nimport { CacheManager } from '../../cache/cache-manager.js'\nimport { VisitTracker } from '../../analytics/visit-tracker.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\nconst renderer = new MarkdownRenderer()\nconst cache = new CacheManager({\n cacheDir: path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-cache'),\n})\n\n/**\n * Handler for GET /api/third-audience/markdown/[...slug]\n *\n * Install in your Next.js app at:\n * app/api/third-audience/markdown/[...slug]/route.ts\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string[] }> }) {\n const startedAt = Date.now()\n const { slug: slugParts } = await params\n const slug = slugParts.join('/')\n const cacheKey = `markdown:${slug}`\n\n const cached = cache.get(cacheKey)\n if (cached) {\n // Record the bot visit (VisitTracker no-ops for non-bot user agents).\n VisitTracker.getInstance().record(req, {\n responseMs: Date.now() - startedAt,\n cacheHit: true,\n contentLength: cached.length,\n })\n return new NextResponse(cached, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'HIT',\n },\n })\n }\n\n const file = reader.read(slug)\n if (!file) {\n return new NextResponse('Not Found', { status: 404 })\n }\n\n const markdown = renderer.render(file)\n cache.set(cacheKey, markdown)\n\n // Record the bot visit (VisitTracker no-ops for non-bot user agents).\n VisitTracker.getInstance().record(req, {\n responseMs: Date.now() - startedAt,\n cacheHit: false,\n contentLength: markdown.length,\n })\n\n return new NextResponse(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'MISS',\n },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\ninterface CacheEntry {\n content: string\n etag: string\n cachedAt: number\n ttl: number\n}\n\n/**\n * Two-tier cache:\n * 1. In-memory LRU (per Node.js process, instant)\n * 2. File-system cache in data/ta-cache/ (survives restarts)\n */\nexport class CacheManager {\n private memCache = new Map<string, CacheEntry>()\n private cacheDir: string\n private maxMemoryEntries: number\n private defaultTtl: number\n\n constructor(opts: { cacheDir: string; maxMemoryEntries?: number; ttl?: number }) {\n this.cacheDir = opts.cacheDir\n this.maxMemoryEntries = opts.maxMemoryEntries ?? 500\n this.defaultTtl = opts.ttl ?? 3600\n }\n\n get(key: string): string | null {\n // Check memory first\n const mem = this.memCache.get(key)\n if (mem && this.isValid(mem)) return mem.content\n if (mem) this.memCache.delete(key)\n\n // Check file cache\n const file = this.readFileCache(key)\n if (file && this.isValid(file)) {\n this.setMemory(key, file)\n return file.content\n }\n\n return null\n }\n\n set(key: string, content: string, etag = '', ttl = this.defaultTtl): void {\n const entry: CacheEntry = { content, etag, cachedAt: Date.now(), ttl }\n this.setMemory(key, entry)\n this.writeFileCache(key, entry)\n }\n\n /** Invalidate by key prefix — used when source .mdx file changes. */\n invalidate(keyPrefix: string): void {\n for (const k of this.memCache.keys()) {\n if (k.startsWith(keyPrefix)) this.memCache.delete(k)\n }\n const dir = this.cacheDir\n if (!fs.existsSync(dir)) return\n for (const file of fs.readdirSync(dir)) {\n if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {\n fs.unlinkSync(path.join(dir, file))\n }\n }\n }\n\n stats(): { memEntries: number; fsEntries: number } {\n const fsEntries = fs.existsSync(this.cacheDir)\n ? fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length\n : 0\n return { memEntries: this.memCache.size, fsEntries }\n }\n\n private isValid(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt < entry.ttl * 1000\n }\n\n private setMemory(key: string, entry: CacheEntry): void {\n if (this.memCache.size >= this.maxMemoryEntries) {\n // Evict oldest entry\n const firstKey = this.memCache.keys().next().value\n if (firstKey) this.memCache.delete(firstKey)\n }\n this.memCache.set(key, entry)\n }\n\n private hashKey(key: string): string {\n return crypto.createHash('sha256').update(key).digest('hex')\n }\n\n private filePath(key: string): string {\n return path.join(this.cacheDir, `${this.hashKey(key)}.json`)\n }\n\n private readFileCache(key: string): CacheEntry | null {\n const fp = this.filePath(key)\n if (!fs.existsSync(fp)) return null\n try {\n return JSON.parse(fs.readFileSync(fp, 'utf-8')) as CacheEntry\n } catch {\n return null\n }\n }\n\n private writeFileCache(key: string, entry: CacheEntry): void {\n try {\n fs.mkdirSync(this.cacheDir, { recursive: true })\n fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8')\n } catch {\n // Cache writes must never throw\n }\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { NextRequest } from 'next/server'\nimport { detectBot } from '../detection/bot-detection-pipeline.js'\nimport { getCountry } from './geolocation.js'\n\nexport interface VisitRecord {\n timestamp: string\n bot_name: string | null\n bot_category: string\n detection_method: string\n confidence: string\n url: string\n ip: string\n country: string | null\n user_agent: string\n referer: string | null\n response_ms: number | null\n cache_hit: boolean\n content_length: number | null\n}\n\nexport class VisitTracker {\n private static instance: VisitTracker | null = null\n private dataDir: string\n\n private constructor(dataDir: string) {\n this.dataDir = dataDir\n }\n\n static getInstance(dataDir = process.env.TA_DATA_DIR ?? 'data'): VisitTracker {\n if (!VisitTracker.instance) {\n VisitTracker.instance = new VisitTracker(dataDir)\n }\n return VisitTracker.instance\n }\n\n record(req: NextRequest, meta: { responseMs?: number; cacheHit?: boolean; contentLength?: number } = {}): void {\n const ua = req.headers.get('user-agent') ?? ''\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => { headers[key] = value })\n const result = detectBot({ userAgent: ua, headers })\n\n if (!result.isBot) return // only track bots\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()\n ?? req.headers.get('x-real-ip')\n ?? 'unknown'\n\n const record: VisitRecord = {\n timestamp: new Date().toISOString(),\n bot_name: result.botName,\n bot_category: result.category,\n detection_method: result.detectionMethod,\n confidence: result.confidence,\n url: req.nextUrl.pathname,\n ip,\n country: getCountry(ip),\n user_agent: ua,\n referer: req.headers.get('referer'),\n response_ms: meta.responseMs ?? null,\n cache_hit: meta.cacheHit ?? false,\n content_length: meta.contentLength ?? null,\n }\n\n this.append('ta-visits.jsonl', record)\n }\n\n private append(filename: string, record: VisitRecord): void {\n try {\n const filePath = path.join(this.dataDir, filename)\n fs.mkdirSync(this.dataDir, { recursive: true })\n fs.appendFileSync(filePath, JSON.stringify(record) + '\\n', 'utf-8')\n } catch {\n // Tracking must never throw\n }\n }\n}\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n","let geoip: typeof import('geoip-lite') | null = null\n\nfunction loadGeoip() {\n if (geoip) return geoip\n try {\n geoip = require('geoip-lite') as typeof import('geoip-lite')\n } catch {\n geoip = null\n }\n return geoip\n}\n\n/** Returns ISO 3166-1 alpha-2 country code, or null if lookup fails. */\nexport function getCountry(ip: string): string | null {\n if (!ip || ip === 'unknown' || ip === '127.0.0.1' || ip.startsWith('::')) return null\n const geo = loadGeoip()\n if (!geo) return null\n try {\n const result = geo.lookup(ip)\n return result?.country ?? null\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","import_fs","import_path","fs","path","crypto","import_fs","import_path","path","fs","path"]}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/dashboard/routes/markdown-route.ts
|
|
2
9
|
import { NextResponse } from "next/server";
|
|
3
|
-
import
|
|
10
|
+
import path4 from "path";
|
|
4
11
|
|
|
5
12
|
// src/core/mdx-reader.ts
|
|
6
13
|
import fs from "fs";
|
|
@@ -167,17 +174,190 @@ var CacheManager = class {
|
|
|
167
174
|
}
|
|
168
175
|
};
|
|
169
176
|
|
|
177
|
+
// src/analytics/visit-tracker.ts
|
|
178
|
+
import fs3 from "fs";
|
|
179
|
+
import path3 from "path";
|
|
180
|
+
|
|
181
|
+
// src/detection/known-patterns.ts
|
|
182
|
+
var KNOWN_BOTS = [
|
|
183
|
+
// AI Crawlers
|
|
184
|
+
{ name: "ClaudeBot", category: "ai_crawler", patterns: [/claudebot/i, /claude-web/i] },
|
|
185
|
+
{ name: "GPTBot", category: "ai_crawler", patterns: [/gptbot/i] },
|
|
186
|
+
{ name: "ChatGPT-User", category: "ai_crawler", patterns: [/chatgpt-user/i] },
|
|
187
|
+
{ name: "PerplexityBot", category: "ai_crawler", patterns: [/perplexitybot/i] },
|
|
188
|
+
{ name: "Googlebot-AI", category: "ai_crawler", patterns: [/google-extended/i, /googleother/i] },
|
|
189
|
+
{ name: "FacebookBot", category: "ai_crawler", patterns: [/facebookbot/i] },
|
|
190
|
+
{ name: "Applebot-Extended", category: "ai_crawler", patterns: [/applebot-extended/i] },
|
|
191
|
+
{ name: "YouBot", category: "ai_crawler", patterns: [/youbot/i] },
|
|
192
|
+
{ name: "CCBot", category: "ai_crawler", patterns: [/ccbot/i] },
|
|
193
|
+
{ name: "CohereCrawler", category: "ai_crawler", patterns: [/cohere-ai/i] },
|
|
194
|
+
{ name: "AI2Bot", category: "ai_crawler", patterns: [/ai2bot/i] },
|
|
195
|
+
{ name: "Bytespider", category: "ai_crawler", patterns: [/bytespider/i] },
|
|
196
|
+
{ name: "Diffbot", category: "ai_crawler", patterns: [/diffbot/i] },
|
|
197
|
+
// Search Engines
|
|
198
|
+
{ name: "Googlebot", category: "search_engine", patterns: [/googlebot/i] },
|
|
199
|
+
{ name: "Bingbot", category: "search_engine", patterns: [/bingbot/i, /msnbot/i] },
|
|
200
|
+
{ name: "DuckDuckBot", category: "search_engine", patterns: [/duckduckbot/i] },
|
|
201
|
+
{ name: "Baiduspider", category: "search_engine", patterns: [/baiduspider/i] },
|
|
202
|
+
{ name: "YandexBot", category: "search_engine", patterns: [/yandexbot/i] },
|
|
203
|
+
{ name: "Sogou", category: "search_engine", patterns: [/sogou/i] },
|
|
204
|
+
{ name: "Exabot", category: "search_engine", patterns: [/exabot/i] },
|
|
205
|
+
{ name: "ia_archiver", category: "search_engine", patterns: [/ia_archiver/i] }
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// src/detection/bot-detection-pipeline.ts
|
|
209
|
+
function detectBot(input) {
|
|
210
|
+
const ua = input.userAgent ?? "";
|
|
211
|
+
for (const bot of KNOWN_BOTS) {
|
|
212
|
+
for (const pattern of bot.patterns) {
|
|
213
|
+
if (pattern.test(ua)) {
|
|
214
|
+
return {
|
|
215
|
+
isBot: true,
|
|
216
|
+
botName: bot.name,
|
|
217
|
+
confidence: "high",
|
|
218
|
+
detectionMethod: "known_pattern",
|
|
219
|
+
category: bot.category,
|
|
220
|
+
rawUserAgent: ua
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const heuristicResult = checkHeuristics(ua, input.headers ?? {});
|
|
226
|
+
if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua };
|
|
227
|
+
if (looksLikeBotUa(ua)) {
|
|
228
|
+
return {
|
|
229
|
+
isBot: true,
|
|
230
|
+
botName: null,
|
|
231
|
+
confidence: "low",
|
|
232
|
+
detectionMethod: "auto_learned",
|
|
233
|
+
category: "unknown_bot",
|
|
234
|
+
rawUserAgent: ua
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
isBot: false,
|
|
239
|
+
botName: null,
|
|
240
|
+
confidence: "high",
|
|
241
|
+
detectionMethod: "none",
|
|
242
|
+
category: "human",
|
|
243
|
+
rawUserAgent: ua
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function checkHeuristics(ua, headers) {
|
|
247
|
+
if (/headlesschrome/i.test(ua)) {
|
|
248
|
+
return { isBot: true, botName: "HeadlessChrome", confidence: "medium", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
249
|
+
}
|
|
250
|
+
if (/phantomjs/i.test(ua)) {
|
|
251
|
+
return { isBot: true, botName: "PhantomJS", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
252
|
+
}
|
|
253
|
+
if (/selenium/i.test(ua)) {
|
|
254
|
+
return { isBot: true, botName: "Selenium", confidence: "high", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
255
|
+
}
|
|
256
|
+
if (ua.trim().length < 10) {
|
|
257
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
258
|
+
}
|
|
259
|
+
const hasAcceptLang = !!headers["accept-language"];
|
|
260
|
+
const hasAcceptEncoding = !!headers["accept-encoding"];
|
|
261
|
+
const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua);
|
|
262
|
+
if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {
|
|
263
|
+
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function looksLikeBotUa(ua) {
|
|
268
|
+
return /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) && !/chrome|firefox|safari|edge|opera/i.test(ua);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/analytics/geolocation.ts
|
|
272
|
+
var geoip = null;
|
|
273
|
+
function loadGeoip() {
|
|
274
|
+
if (geoip) return geoip;
|
|
275
|
+
try {
|
|
276
|
+
geoip = __require("geoip-lite");
|
|
277
|
+
} catch {
|
|
278
|
+
geoip = null;
|
|
279
|
+
}
|
|
280
|
+
return geoip;
|
|
281
|
+
}
|
|
282
|
+
function getCountry(ip) {
|
|
283
|
+
if (!ip || ip === "unknown" || ip === "127.0.0.1" || ip.startsWith("::")) return null;
|
|
284
|
+
const geo = loadGeoip();
|
|
285
|
+
if (!geo) return null;
|
|
286
|
+
try {
|
|
287
|
+
const result = geo.lookup(ip);
|
|
288
|
+
return result?.country ?? null;
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/analytics/visit-tracker.ts
|
|
295
|
+
var _VisitTracker = class _VisitTracker {
|
|
296
|
+
constructor(dataDir) {
|
|
297
|
+
this.dataDir = dataDir;
|
|
298
|
+
}
|
|
299
|
+
static getInstance(dataDir = process.env.TA_DATA_DIR ?? "data") {
|
|
300
|
+
if (!_VisitTracker.instance) {
|
|
301
|
+
_VisitTracker.instance = new _VisitTracker(dataDir);
|
|
302
|
+
}
|
|
303
|
+
return _VisitTracker.instance;
|
|
304
|
+
}
|
|
305
|
+
record(req, meta = {}) {
|
|
306
|
+
const ua = req.headers.get("user-agent") ?? "";
|
|
307
|
+
const headers = {};
|
|
308
|
+
req.headers.forEach((value, key) => {
|
|
309
|
+
headers[key] = value;
|
|
310
|
+
});
|
|
311
|
+
const result = detectBot({ userAgent: ua, headers });
|
|
312
|
+
if (!result.isBot) return;
|
|
313
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
314
|
+
const record = {
|
|
315
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
316
|
+
bot_name: result.botName,
|
|
317
|
+
bot_category: result.category,
|
|
318
|
+
detection_method: result.detectionMethod,
|
|
319
|
+
confidence: result.confidence,
|
|
320
|
+
url: req.nextUrl.pathname,
|
|
321
|
+
ip,
|
|
322
|
+
country: getCountry(ip),
|
|
323
|
+
user_agent: ua,
|
|
324
|
+
referer: req.headers.get("referer"),
|
|
325
|
+
response_ms: meta.responseMs ?? null,
|
|
326
|
+
cache_hit: meta.cacheHit ?? false,
|
|
327
|
+
content_length: meta.contentLength ?? null
|
|
328
|
+
};
|
|
329
|
+
this.append("ta-visits.jsonl", record);
|
|
330
|
+
}
|
|
331
|
+
append(filename, record) {
|
|
332
|
+
try {
|
|
333
|
+
const filePath = path3.join(this.dataDir, filename);
|
|
334
|
+
fs3.mkdirSync(this.dataDir, { recursive: true });
|
|
335
|
+
fs3.appendFileSync(filePath, JSON.stringify(record) + "\n", "utf-8");
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
_VisitTracker.instance = null;
|
|
341
|
+
var VisitTracker = _VisitTracker;
|
|
342
|
+
|
|
170
343
|
// src/dashboard/routes/markdown-route.ts
|
|
171
|
-
var reader = new MdxReader({ contentDir:
|
|
344
|
+
var reader = new MdxReader({ contentDir: path4.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content") });
|
|
172
345
|
var renderer = new MarkdownRenderer();
|
|
173
346
|
var cache = new CacheManager({
|
|
174
|
-
cacheDir:
|
|
347
|
+
cacheDir: path4.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")
|
|
175
348
|
});
|
|
176
349
|
async function GET(req, { params }) {
|
|
177
|
-
const
|
|
350
|
+
const startedAt = Date.now();
|
|
351
|
+
const { slug: slugParts } = await params;
|
|
352
|
+
const slug = slugParts.join("/");
|
|
178
353
|
const cacheKey = `markdown:${slug}`;
|
|
179
354
|
const cached = cache.get(cacheKey);
|
|
180
355
|
if (cached) {
|
|
356
|
+
VisitTracker.getInstance().record(req, {
|
|
357
|
+
responseMs: Date.now() - startedAt,
|
|
358
|
+
cacheHit: true,
|
|
359
|
+
contentLength: cached.length
|
|
360
|
+
});
|
|
181
361
|
return new NextResponse(cached, {
|
|
182
362
|
headers: {
|
|
183
363
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
@@ -191,6 +371,11 @@ async function GET(req, { params }) {
|
|
|
191
371
|
}
|
|
192
372
|
const markdown = renderer.render(file);
|
|
193
373
|
cache.set(cacheKey, markdown);
|
|
374
|
+
VisitTracker.getInstance().record(req, {
|
|
375
|
+
responseMs: Date.now() - startedAt,
|
|
376
|
+
cacheHit: false,
|
|
377
|
+
contentLength: markdown.length
|
|
378
|
+
});
|
|
194
379
|
return new NextResponse(markdown, {
|
|
195
380
|
headers: {
|
|
196
381
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/markdown-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/cache/cache-manager.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../../core/markdown-renderer.js'\nimport { CacheManager } from '../../cache/cache-manager.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\nconst renderer = new MarkdownRenderer()\nconst cache = new CacheManager({\n cacheDir: path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-cache'),\n})\n\n/**\n * Handler for GET /api/third-audience/markdown/[...slug]\n *\n * Install in your Next.js app at:\n * app/api/third-audience/markdown/[...slug]/route.ts\n */\nexport async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) {\n const slug = params.slug.join('/')\n const cacheKey = `markdown:${slug}`\n\n const cached = cache.get(cacheKey)\n if (cached) {\n return new NextResponse(cached, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'HIT',\n },\n })\n }\n\n const file = reader.read(slug)\n if (!file) {\n return new NextResponse('Not Found', { status: 404 })\n }\n\n const markdown = renderer.render(file)\n cache.set(cacheKey, markdown)\n\n return new NextResponse(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'MISS',\n },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\ninterface CacheEntry {\n content: string\n etag: string\n cachedAt: number\n ttl: number\n}\n\n/**\n * Two-tier cache:\n * 1. In-memory LRU (per Node.js process, instant)\n * 2. File-system cache in data/ta-cache/ (survives restarts)\n */\nexport class CacheManager {\n private memCache = new Map<string, CacheEntry>()\n private cacheDir: string\n private maxMemoryEntries: number\n private defaultTtl: number\n\n constructor(opts: { cacheDir: string; maxMemoryEntries?: number; ttl?: number }) {\n this.cacheDir = opts.cacheDir\n this.maxMemoryEntries = opts.maxMemoryEntries ?? 500\n this.defaultTtl = opts.ttl ?? 3600\n }\n\n get(key: string): string | null {\n // Check memory first\n const mem = this.memCache.get(key)\n if (mem && this.isValid(mem)) return mem.content\n if (mem) this.memCache.delete(key)\n\n // Check file cache\n const file = this.readFileCache(key)\n if (file && this.isValid(file)) {\n this.setMemory(key, file)\n return file.content\n }\n\n return null\n }\n\n set(key: string, content: string, etag = '', ttl = this.defaultTtl): void {\n const entry: CacheEntry = { content, etag, cachedAt: Date.now(), ttl }\n this.setMemory(key, entry)\n this.writeFileCache(key, entry)\n }\n\n /** Invalidate by key prefix — used when source .mdx file changes. */\n invalidate(keyPrefix: string): void {\n for (const k of this.memCache.keys()) {\n if (k.startsWith(keyPrefix)) this.memCache.delete(k)\n }\n const dir = this.cacheDir\n if (!fs.existsSync(dir)) return\n for (const file of fs.readdirSync(dir)) {\n if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {\n fs.unlinkSync(path.join(dir, file))\n }\n }\n }\n\n stats(): { memEntries: number; fsEntries: number } {\n const fsEntries = fs.existsSync(this.cacheDir)\n ? fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length\n : 0\n return { memEntries: this.memCache.size, fsEntries }\n }\n\n private isValid(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt < entry.ttl * 1000\n }\n\n private setMemory(key: string, entry: CacheEntry): void {\n if (this.memCache.size >= this.maxMemoryEntries) {\n // Evict oldest entry\n const firstKey = this.memCache.keys().next().value\n if (firstKey) this.memCache.delete(firstKey)\n }\n this.memCache.set(key, entry)\n }\n\n private hashKey(key: string): string {\n return crypto.createHash('sha256').update(key).digest('hex')\n }\n\n private filePath(key: string): string {\n return path.join(this.cacheDir, `${this.hashKey(key)}.json`)\n }\n\n private readFileCache(key: string): CacheEntry | null {\n const fp = this.filePath(key)\n if (!fs.existsSync(fp)) return null\n try {\n return JSON.parse(fs.readFileSync(fp, 'utf-8')) as CacheEntry\n } catch {\n return null\n }\n }\n\n private writeFileCache(key: string, entry: CacheEntry): void {\n try {\n fs.mkdirSync(this.cacheDir, { recursive: true })\n fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8')\n } catch {\n // Cache writes must never throw\n }\n }\n}\n"],"mappings":"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","names":["path","fs","path","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/markdown-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/cache/cache-manager.ts","../../../src/analytics/visit-tracker.ts","../../../src/detection/known-patterns.ts","../../../src/detection/bot-detection-pipeline.ts","../../../src/analytics/geolocation.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../../core/markdown-renderer.js'\nimport { CacheManager } from '../../cache/cache-manager.js'\nimport { VisitTracker } from '../../analytics/visit-tracker.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\nconst renderer = new MarkdownRenderer()\nconst cache = new CacheManager({\n cacheDir: path.join(process.cwd(), process.env.TA_DATA_DIR ?? 'data', 'ta-cache'),\n})\n\n/**\n * Handler for GET /api/third-audience/markdown/[...slug]\n *\n * Install in your Next.js app at:\n * app/api/third-audience/markdown/[...slug]/route.ts\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string[] }> }) {\n const startedAt = Date.now()\n const { slug: slugParts } = await params\n const slug = slugParts.join('/')\n const cacheKey = `markdown:${slug}`\n\n const cached = cache.get(cacheKey)\n if (cached) {\n // Record the bot visit (VisitTracker no-ops for non-bot user agents).\n VisitTracker.getInstance().record(req, {\n responseMs: Date.now() - startedAt,\n cacheHit: true,\n contentLength: cached.length,\n })\n return new NextResponse(cached, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'HIT',\n },\n })\n }\n\n const file = reader.read(slug)\n if (!file) {\n return new NextResponse('Not Found', { status: 404 })\n }\n\n const markdown = renderer.render(file)\n cache.set(cacheKey, markdown)\n\n // Record the bot visit (VisitTracker no-ops for non-bot user agents).\n VisitTracker.getInstance().record(req, {\n responseMs: Date.now() - startedAt,\n cacheHit: false,\n contentLength: markdown.length,\n })\n\n return new NextResponse(markdown, {\n headers: {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'X-Cache': 'MISS',\n },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\ninterface CacheEntry {\n content: string\n etag: string\n cachedAt: number\n ttl: number\n}\n\n/**\n * Two-tier cache:\n * 1. In-memory LRU (per Node.js process, instant)\n * 2. File-system cache in data/ta-cache/ (survives restarts)\n */\nexport class CacheManager {\n private memCache = new Map<string, CacheEntry>()\n private cacheDir: string\n private maxMemoryEntries: number\n private defaultTtl: number\n\n constructor(opts: { cacheDir: string; maxMemoryEntries?: number; ttl?: number }) {\n this.cacheDir = opts.cacheDir\n this.maxMemoryEntries = opts.maxMemoryEntries ?? 500\n this.defaultTtl = opts.ttl ?? 3600\n }\n\n get(key: string): string | null {\n // Check memory first\n const mem = this.memCache.get(key)\n if (mem && this.isValid(mem)) return mem.content\n if (mem) this.memCache.delete(key)\n\n // Check file cache\n const file = this.readFileCache(key)\n if (file && this.isValid(file)) {\n this.setMemory(key, file)\n return file.content\n }\n\n return null\n }\n\n set(key: string, content: string, etag = '', ttl = this.defaultTtl): void {\n const entry: CacheEntry = { content, etag, cachedAt: Date.now(), ttl }\n this.setMemory(key, entry)\n this.writeFileCache(key, entry)\n }\n\n /** Invalidate by key prefix — used when source .mdx file changes. */\n invalidate(keyPrefix: string): void {\n for (const k of this.memCache.keys()) {\n if (k.startsWith(keyPrefix)) this.memCache.delete(k)\n }\n const dir = this.cacheDir\n if (!fs.existsSync(dir)) return\n for (const file of fs.readdirSync(dir)) {\n if (file.startsWith(this.hashKey(keyPrefix).slice(0, 8))) {\n fs.unlinkSync(path.join(dir, file))\n }\n }\n }\n\n stats(): { memEntries: number; fsEntries: number } {\n const fsEntries = fs.existsSync(this.cacheDir)\n ? fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length\n : 0\n return { memEntries: this.memCache.size, fsEntries }\n }\n\n private isValid(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt < entry.ttl * 1000\n }\n\n private setMemory(key: string, entry: CacheEntry): void {\n if (this.memCache.size >= this.maxMemoryEntries) {\n // Evict oldest entry\n const firstKey = this.memCache.keys().next().value\n if (firstKey) this.memCache.delete(firstKey)\n }\n this.memCache.set(key, entry)\n }\n\n private hashKey(key: string): string {\n return crypto.createHash('sha256').update(key).digest('hex')\n }\n\n private filePath(key: string): string {\n return path.join(this.cacheDir, `${this.hashKey(key)}.json`)\n }\n\n private readFileCache(key: string): CacheEntry | null {\n const fp = this.filePath(key)\n if (!fs.existsSync(fp)) return null\n try {\n return JSON.parse(fs.readFileSync(fp, 'utf-8')) as CacheEntry\n } catch {\n return null\n }\n }\n\n private writeFileCache(key: string, entry: CacheEntry): void {\n try {\n fs.mkdirSync(this.cacheDir, { recursive: true })\n fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8')\n } catch {\n // Cache writes must never throw\n }\n }\n}\n","import fs from 'fs'\nimport path from 'path'\nimport type { NextRequest } from 'next/server'\nimport { detectBot } from '../detection/bot-detection-pipeline.js'\nimport { getCountry } from './geolocation.js'\n\nexport interface VisitRecord {\n timestamp: string\n bot_name: string | null\n bot_category: string\n detection_method: string\n confidence: string\n url: string\n ip: string\n country: string | null\n user_agent: string\n referer: string | null\n response_ms: number | null\n cache_hit: boolean\n content_length: number | null\n}\n\nexport class VisitTracker {\n private static instance: VisitTracker | null = null\n private dataDir: string\n\n private constructor(dataDir: string) {\n this.dataDir = dataDir\n }\n\n static getInstance(dataDir = process.env.TA_DATA_DIR ?? 'data'): VisitTracker {\n if (!VisitTracker.instance) {\n VisitTracker.instance = new VisitTracker(dataDir)\n }\n return VisitTracker.instance\n }\n\n record(req: NextRequest, meta: { responseMs?: number; cacheHit?: boolean; contentLength?: number } = {}): void {\n const ua = req.headers.get('user-agent') ?? ''\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => { headers[key] = value })\n const result = detectBot({ userAgent: ua, headers })\n\n if (!result.isBot) return // only track bots\n\n const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()\n ?? req.headers.get('x-real-ip')\n ?? 'unknown'\n\n const record: VisitRecord = {\n timestamp: new Date().toISOString(),\n bot_name: result.botName,\n bot_category: result.category,\n detection_method: result.detectionMethod,\n confidence: result.confidence,\n url: req.nextUrl.pathname,\n ip,\n country: getCountry(ip),\n user_agent: ua,\n referer: req.headers.get('referer'),\n response_ms: meta.responseMs ?? null,\n cache_hit: meta.cacheHit ?? false,\n content_length: meta.contentLength ?? null,\n }\n\n this.append('ta-visits.jsonl', record)\n }\n\n private append(filename: string, record: VisitRecord): void {\n try {\n const filePath = path.join(this.dataDir, filename)\n fs.mkdirSync(this.dataDir, { recursive: true })\n fs.appendFileSync(filePath, JSON.stringify(record) + '\\n', 'utf-8')\n } catch {\n // Tracking must never throw\n }\n }\n}\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n","let geoip: typeof import('geoip-lite') | null = null\n\nfunction loadGeoip() {\n if (geoip) return geoip\n try {\n geoip = require('geoip-lite') as typeof import('geoip-lite')\n } catch {\n geoip = null\n }\n return geoip\n}\n\n/** Returns ISO 3166-1 alpha-2 country code, or null if lookup fails. */\nexport function getCountry(ip: string): string | null {\n if (!ip || ip === 'unknown' || ip === '127.0.0.1' || ip.startsWith('::')) return null\n const geo = loadGeoip()\n if (!geo) return null\n try {\n const result = geo.lookup(ip)\n return result?.country ?? null\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":["path","fs","path","fs","path","path","fs","path"]}
|
|
@@ -5,9 +5,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
5
5
|
* Rewired from middleware to /api/third-audience/okf/[...path]
|
|
6
6
|
*/
|
|
7
7
|
declare function GET(req: NextRequest, { params }: {
|
|
8
|
-
params: {
|
|
8
|
+
params: Promise<{
|
|
9
9
|
path?: string[];
|
|
10
|
-
}
|
|
10
|
+
}>;
|
|
11
11
|
}): Promise<NextResponse<unknown>>;
|
|
12
12
|
|
|
13
13
|
export { GET };
|
|
@@ -5,9 +5,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
5
5
|
* Rewired from middleware to /api/third-audience/okf/[...path]
|
|
6
6
|
*/
|
|
7
7
|
declare function GET(req: NextRequest, { params }: {
|
|
8
|
-
params: {
|
|
8
|
+
params: Promise<{
|
|
9
9
|
path?: string[];
|
|
10
|
-
}
|
|
10
|
+
}>;
|
|
11
11
|
}): Promise<NextResponse<unknown>>;
|
|
12
12
|
|
|
13
13
|
export { GET };
|
|
@@ -164,7 +164,8 @@ var reader = new MdxReader({ contentDir: import_path2.default.join(process.cwd()
|
|
|
164
164
|
async function GET(req, { params }) {
|
|
165
165
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`;
|
|
166
166
|
const allFiles = reader.readAll();
|
|
167
|
-
const
|
|
167
|
+
const { path: pathSegments } = await params;
|
|
168
|
+
const segments = pathSegments ?? [];
|
|
168
169
|
if (segments.length === 0 || segments.length === 1 && (segments[0] === "index.md" || segments[0] === "index")) {
|
|
169
170
|
return new import_server.NextResponse(generateOkfIndex(allFiles, baseUrl), {
|
|
170
171
|
headers: { "Content-Type": "text/markdown; charset=utf-8" }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: { path?: string[] } }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const segments = params.path ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
|
|
@@ -130,7 +130,8 @@ var reader = new MdxReader({ contentDir: path2.join(process.cwd(), process.env.T
|
|
|
130
130
|
async function GET(req, { params }) {
|
|
131
131
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`;
|
|
132
132
|
const allFiles = reader.readAll();
|
|
133
|
-
const
|
|
133
|
+
const { path: pathSegments } = await params;
|
|
134
|
+
const segments = pathSegments ?? [];
|
|
134
135
|
if (segments.length === 0 || segments.length === 1 && (segments[0] === "index.md" || segments[0] === "index")) {
|
|
135
136
|
return new NextResponse(generateOkfIndex(allFiles, baseUrl), {
|
|
136
137
|
headers: { "Content-Type": "text/markdown; charset=utf-8" }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: { path?: string[] } }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const segments = params.path ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":";AAAA,SAAS,oBAAsC;AAC/C,OAAOA,WAAU;;;ACDjB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,YAAY;AAaZ,IAAM,YAAN,MAAgB;AAAA,EAGrB,YAAY,SAA2B;AACrC,SAAK,aAAa,QAAQ;AAAA,EAC5B;AAAA;AAAA,EAGA,KAAK,MAA8B;AACjC,UAAM,aAAa;AAAA,MACjB,KAAK,KAAK,KAAK,YAAY,GAAG,IAAI,MAAM;AAAA,MACxC,KAAK,KAAK,KAAK,YAAY,GAAG,IAAI,KAAK;AAAA,MACvC,KAAK,KAAK,KAAK,YAAY,MAAM,WAAW;AAAA,MAC5C,KAAK,KAAK,KAAK,YAAY,MAAM,UAAU;AAAA,IAC7C;AAEA,eAAW,YAAY,YAAY;AACjC,UAAI,GAAG,WAAW,QAAQ,GAAG;AAC3B,eAAO,KAAK,UAAU,MAAM,QAAQ;AAAA,MACtC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAqB;AACnB,QAAI,CAAC,GAAG,WAAW,KAAK,UAAU,EAAG,QAAO,CAAC;AAC7C,WAAO,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AAAA,EACtD;AAAA,EAEQ,QAAQ,KAAa,MAAyB;AACpD,UAAM,UAAqB,CAAC;AAC5B,eAAW,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,YAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,UAAI,MAAM,YAAY,GAAG;AACvB,gBAAQ,KAAK,GAAG,KAAK,QAAQ,UAAU,IAAI,CAAC;AAAA,MAC9C,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;AACpE,cAAM,WAAW,KAAK,SAAS,MAAM,QAAQ;AAC7C,cAAM,OAAO,SAAS,QAAQ,eAAe,EAAE,EAAE,QAAQ,YAAY,EAAE;AACvE,gBAAQ,KAAK,KAAK,UAAU,MAAM,QAAQ,CAAC;AAAA,MAC7C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,MAAc,UAA2B;AACzD,UAAM,MAAM,GAAG,aAAa,UAAU,OAAO;AAC7C,UAAM,EAAE,MAAM,aAAa,SAAS,WAAW,IAAI,OAAO,GAAG;AAC7D,WAAO,EAAE,MAAM,UAAU,aAAa,WAAW;AAAA,EACnD;AACF;;;ACnDO,IAAM,mBAAN,MAAuB;AAAA,EAC5B,OAAO,MAAuB;AAC5B,UAAM,SAAS,KAAK,uBAAuB,KAAK,WAAW;AAC3D,UAAM,OAAO,KAAK,SAAS,KAAK,UAAU;AAC1C,WAAO,SAAS,GAAG,MAAM;AAAA;AAAA,EAAO,IAAI,KAAK;AAAA,EAC3C;AAAA,EAEQ,uBAAuB,IAAqC;AAClE,UAAM,OAAO,OAAO,KAAK,EAAE;AAC3B,QAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,UAAM,QAAQ,KACX,OAAO,OAAK,GAAG,CAAC,MAAM,UAAa,GAAG,CAAC,MAAM,IAAI,EACjD,IAAI,OAAK,GAAG,CAAC,KAAK,KAAK,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE;AAC5C,WAAO;AAAA,EAAQ,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA,EACjC;AAAA,EAEQ,UAAU,KAAsB;AACtC,QAAI,OAAO,QAAQ,UAAU;AAE3B,aAAO,yBAAyB,KAAK,GAAG,IAAI,IAAI,IAAI,QAAQ,MAAM,KAAK,CAAC,MAAM;AAAA,IAChF;AACA,QAAI,eAAe,KAAM,QAAO,IAAI,YAAY;AAChD,QAAI,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI,IAAI,IAAI,OAAK,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAC7E,WAAO,OAAO,GAAG;AAAA,EACnB;AAAA,EAEQ,SAAS,SAAyB;AACxC,QAAI,MAAM;AAGV,UAAM,IAAI,QAAQ,oCAAoC,EAAE;AAGxD,UAAM,IAAI,QAAQ,kGAAkG,EAAE;AACtH,UAAM,IAAI,QAAQ,4DAA4D,EAAE;AAIhF,UAAM,IAAI,QAAQ,kCAAkC,EAAE;AAItD,UAAM,IAAI,QAAQ,8CAA8C,EAAE;AAGlE,UAAM,IAAI,QAAQ,wBAAwB,EAAE;AAG5C,UAAM,IAAI,QAAQ,WAAW,MAAM;AAEnC,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;;;AChEA,IAAM,WAAW,IAAI,iBAAiB;AAwE/B,SAAS,iBAAiB,OAAkB,SAAyB;AAC1E,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AACtC,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,KAAK,KAAK;AAChB,UAAM,QAAQ,OAAO,GAAG,SAAS,KAAK,IAAI;AAC1C,UAAM,OAAO,GAAG,cAAc,WAAM,OAAO,GAAG,WAAW,CAAC,KAAK;AAC/D,UAAM,KAAK,MAAM,KAAK,KAAK,IAAI,QAAQ,KAAK,IAAI,OAAO,IAAI,EAAE;AAAA,EAC/D;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI;AAC5B;AAGO,SAAS,gBAAgB,MAAe,UAAqB,SAAyB;AAC3F,QAAM,WAAW,SAAS,OAAO,IAAI;AACrC,SAAO,qBAAqB,UAAU,UAAU,OAAO;AACzD;AAMA,SAAS,qBAAqB,UAAkB,UAAqB,SAAyB;AAC5F,QAAM,UAAU,IAAI,IAAI,SAAS,IAAI,OAAK,EAAE,IAAI,CAAC;AACjD,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAEtC,SAAO,SAAS,QAAQ,8BAA8B,CAAC,OAAO,MAAM,SAAS;AAE3E,UAAM,YAAY,KAAK,QAAQ,OAAO,EAAE,EAAE,QAAQ,SAAS,EAAE;AAC7D,QAAI,QAAQ,IAAI,SAAS,GAAG;AAC1B,aAAO,IAAI,IAAI,KAAK,IAAI,QAAQ,SAAS;AAAA,IAC3C;AACA,WAAO;AAAA,EACT,CAAC;AACH;;;AHjHA,IAAM,SAAS,IAAI,UAAU,EAAE,YAAYC,MAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI,kBAAkB,SAAS,EAAE,CAAC;AAM9G,eAAsB,IAAI,KAAkB,EAAE,OAAO,GAAoC;AACvF,QAAM,UAAU,QAAQ,IAAI,wBACvB,GAAG,IAAI,QAAQ,QAAQ,KAAK,IAAI,QAAQ,IAAI;AAEjD,QAAM,WAAW,OAAO,QAAQ;AAChC,QAAM,WAAW,OAAO,QAAQ,CAAC;AAGjC,MAAI,SAAS,WAAW,KAAM,SAAS,WAAW,MAAM,SAAS,CAAC,MAAM,cAAc,SAAS,CAAC,MAAM,UAAW;AAC/G,WAAO,IAAI,aAAa,iBAAiB,UAAU,OAAO,GAAG;AAAA,MAC3D,SAAS,EAAE,gBAAgB,+BAA+B;AAAA,IAC5D,CAAC;AAAA,EACH;AAGA,QAAM,OAAO,SAAS,KAAK,GAAG,EAAE,QAAQ,SAAS,EAAE;AACnD,QAAM,OAAO,SAAS,KAAK,OAAK,EAAE,SAAS,IAAI;AAC/C,MAAI,CAAC,KAAM,QAAO,IAAI,aAAa,aAAa,EAAE,QAAQ,IAAI,CAAC;AAE/D,SAAO,IAAI,aAAa,gBAAgB,MAAM,UAAU,OAAO,GAAG;AAAA,IAChE,SAAS,EAAE,gBAAgB,+BAA+B;AAAA,EAC5D,CAAC;AACH;","names":["path","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["path","path"]}
|
package/dist/index.js
CHANGED
|
@@ -218,7 +218,8 @@ function checkHeuristics(ua, headers) {
|
|
|
218
218
|
}
|
|
219
219
|
const hasAcceptLang = !!headers["accept-language"];
|
|
220
220
|
const hasAcceptEncoding = !!headers["accept-encoding"];
|
|
221
|
-
|
|
221
|
+
const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua);
|
|
222
|
+
if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {
|
|
222
223
|
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
223
224
|
}
|
|
224
225
|
return null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["/**\n * third-audience-mdx\n * Public API surface for the package.\n */\n\nexport { withThirdAudience } from './core/with-third-audience.js'\nexport { thirdAudienceMiddleware } from './core/middleware.js'\nexport { detectBot } from './detection/bot-detection-pipeline.js'\nexport type { ThirdAudienceConfig } from './core/config.js'\nexport type { BotDetectionResult } from './detection/bot-detection-result.js'\n","export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n if (!hasAcceptLang && !hasAcceptEncoding) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["/**\n * third-audience-mdx\n * Public API surface for the package.\n */\n\nexport { withThirdAudience } from './core/with-third-audience.js'\nexport { thirdAudienceMiddleware } from './core/middleware.js'\nexport { detectBot } from './detection/bot-detection-pipeline.js'\nexport type { ThirdAudienceConfig } from './core/config.js'\nexport type { BotDetectionResult } from './detection/bot-detection-result.js'\n","export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -190,7 +190,8 @@ function checkHeuristics(ua, headers) {
|
|
|
190
190
|
}
|
|
191
191
|
const hasAcceptLang = !!headers["accept-language"];
|
|
192
192
|
const hasAcceptEncoding = !!headers["accept-encoding"];
|
|
193
|
-
|
|
193
|
+
const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua);
|
|
194
|
+
if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {
|
|
194
195
|
return { isBot: true, botName: null, confidence: "low", detectionMethod: "heuristic", category: "unknown_bot" };
|
|
195
196
|
}
|
|
196
197
|
return null;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n if (!hasAcceptLang && !hasAcceptEncoding) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/config.ts","../src/core/with-third-audience.ts","../src/core/middleware.ts","../src/detection/known-patterns.ts","../src/detection/bot-detection-pipeline.ts"],"sourcesContent":["export interface ThirdAudienceConfig {\n /** Directory containing .mdx files, relative to project root. Default: 'content' */\n contentDir?: string\n /** Directory for JSONL data files. Default: 'data' */\n dataDir?: string\n /** Mount the /third-audience/ dashboard. Default: true */\n dashboard?: boolean\n /** Secret for dashboard access (HTTP Basic or bearer). Required when dashboard: true */\n dashboardSecret?: string\n notifications?: {\n email?: { smtp: string; to: string; from?: string }\n slack?: { webhookUrl: string }\n }\n bots?: {\n allowlist?: string[]\n blocklist?: string[]\n }\n cache?: {\n /** Cache TTL in seconds. Default: 3600 */\n ttl?: number\n /** Max in-memory entries. Default: 500 */\n maxMemoryEntries?: number\n }\n}\n\nexport const defaultConfig: Required<ThirdAudienceConfig> = {\n contentDir: 'content',\n dataDir: 'data',\n dashboard: true,\n dashboardSecret: '',\n notifications: {},\n bots: { allowlist: [], blocklist: [] },\n cache: { ttl: 3600, maxMemoryEntries: 500 },\n}\n\nexport function resolveConfig(partial: ThirdAudienceConfig = {}): Required<ThirdAudienceConfig> {\n return {\n ...defaultConfig,\n ...partial,\n bots: { ...defaultConfig.bots, ...partial.bots },\n cache: { ...defaultConfig.cache, ...partial.cache },\n notifications: { ...defaultConfig.notifications, ...partial.notifications },\n }\n}\n","import type { NextConfig } from 'next'\nimport { resolveConfig, type ThirdAudienceConfig } from './config.js'\n\n/**\n * Wraps next.config.ts to inject Third Audience rewrites and headers.\n *\n * Usage:\n * import { withThirdAudience } from 'third-audience-mdx'\n * export default withThirdAudience({ contentDir: 'content' })\n */\nexport function withThirdAudience(\n options: ThirdAudienceConfig = {},\n nextConfig: NextConfig = {}\n): NextConfig {\n const config = resolveConfig(options)\n\n return {\n ...nextConfig,\n async headers() {\n const existing = await nextConfig.headers?.() ?? []\n return [\n ...existing,\n {\n source: '/:path*.md',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n {\n source: '/llms.txt',\n headers: [{ key: 'Content-Type', value: 'text/plain; charset=utf-8' }],\n },\n {\n source: '/okf/:path*',\n headers: [{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' }],\n },\n ]\n },\n env: {\n ...nextConfig.env,\n TA_CONTENT_DIR: config.contentDir,\n TA_DATA_DIR: config.dataDir,\n TA_DASHBOARD_ENABLED: String(config.dashboard),\n },\n }\n}\n","import { NextResponse, type NextRequest } from 'next/server'\n\nconst COOKIE_NAME = 'ta_session'\nconst RESET_COOKIE = 'ta_session_reset'\n\n/**\n * Third Audience middleware — Edge-runtime compatible (no Node.js crypto).\n *\n * Auth guard uses cookie presence only; HMAC verification happens in the\n * route handler (Node.js runtime) where crypto is available.\n *\n * Wire up in middleware.ts:\n * export { thirdAudienceMiddleware as middleware } from 'third-audience-mdx'\n * export const config = { matcher: ['/((?!_next|api).*)'] }\n */\nexport function thirdAudienceMiddleware(req: NextRequest): NextResponse | null {\n const { pathname } = req.nextUrl\n const accept = req.headers.get('accept') ?? ''\n\n // Dashboard auth guard — cookie presence check (HMAC verified in route handler)\n if (pathname.startsWith('/third-audience') && !pathname.startsWith('/third-audience/login')) {\n const session = req.cookies.get(COOKIE_NAME)?.value\n if (!session) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // Password reset guard — /third-audience/login?reset=1 requires reset or session cookie\n if (pathname === '/third-audience/login' && req.nextUrl.searchParams.get('reset') === '1') {\n const resetCookie = req.cookies.get(RESET_COOKIE)?.value\n const sessionCookie = req.cookies.get(COOKIE_NAME)?.value\n if (!resetCookie && !sessionCookie) {\n const loginUrl = req.nextUrl.clone()\n loginUrl.pathname = '/third-audience/login'\n loginUrl.search = ''\n return NextResponse.redirect(loginUrl)\n }\n }\n\n // /third-audience/login → rewrite to login route handler (GET/POST)\n if (pathname === '/third-audience/login') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/login'\n return NextResponse.rewrite(url)\n }\n\n // .md URL → rewrite to our internal markdown route handler\n if (pathname.endsWith('.md')) {\n const slug = pathname.slice(0, -3) // strip .md\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${slug}`\n return NextResponse.rewrite(url)\n }\n\n // Accept: text/markdown header → rewrite to markdown route\n if (accept.includes('text/markdown')) {\n const url = req.nextUrl.clone()\n url.pathname = `/api/third-audience/markdown${pathname}`\n return NextResponse.rewrite(url)\n }\n\n // /okf or /okf/* → rewrite to OKF bundle handler\n // [...path] catch-all requires at least one segment, so /okf → /okf/index\n if (pathname.startsWith('/okf')) {\n const url = req.nextUrl.clone()\n const rest = pathname.slice(4) // '' or '/something'\n url.pathname = `/api/third-audience/okf${rest || '/index'}`\n return NextResponse.rewrite(url)\n }\n\n // /llms.txt → rewrite to discovery handler\n if (pathname === '/llms.txt') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/llms-txt'\n return NextResponse.rewrite(url)\n }\n\n // /sitemap-ai.xml → rewrite to AI sitemap handler\n if (pathname === '/sitemap-ai.xml') {\n const url = req.nextUrl.clone()\n url.pathname = '/api/third-audience/sitemap-ai'\n return NextResponse.rewrite(url)\n }\n\n // No match — let the caller's middleware chain continue\n return null\n}\n\n","/** Known AI crawler and search engine user-agent patterns. */\nexport interface KnownBot {\n name: string\n category: 'ai_crawler' | 'search_engine'\n patterns: RegExp[]\n}\n\nexport const KNOWN_BOTS: KnownBot[] = [\n // AI Crawlers\n { name: 'ClaudeBot', category: 'ai_crawler', patterns: [/claudebot/i, /claude-web/i] },\n { name: 'GPTBot', category: 'ai_crawler', patterns: [/gptbot/i] },\n { name: 'ChatGPT-User', category: 'ai_crawler', patterns: [/chatgpt-user/i] },\n { name: 'PerplexityBot', category: 'ai_crawler', patterns: [/perplexitybot/i] },\n { name: 'Googlebot-AI', category: 'ai_crawler', patterns: [/google-extended/i, /googleother/i] },\n { name: 'FacebookBot', category: 'ai_crawler', patterns: [/facebookbot/i] },\n { name: 'Applebot-Extended',category: 'ai_crawler', patterns: [/applebot-extended/i] },\n { name: 'YouBot', category: 'ai_crawler', patterns: [/youbot/i] },\n { name: 'CCBot', category: 'ai_crawler', patterns: [/ccbot/i] },\n { name: 'CohereCrawler', category: 'ai_crawler', patterns: [/cohere-ai/i] },\n { name: 'AI2Bot', category: 'ai_crawler', patterns: [/ai2bot/i] },\n { name: 'Bytespider', category: 'ai_crawler', patterns: [/bytespider/i] },\n { name: 'Diffbot', category: 'ai_crawler', patterns: [/diffbot/i] },\n\n // Search Engines\n { name: 'Googlebot', category: 'search_engine', patterns: [/googlebot/i] },\n { name: 'Bingbot', category: 'search_engine', patterns: [/bingbot/i, /msnbot/i] },\n { name: 'DuckDuckBot', category: 'search_engine', patterns: [/duckduckbot/i] },\n { name: 'Baiduspider', category: 'search_engine', patterns: [/baiduspider/i] },\n { name: 'YandexBot', category: 'search_engine', patterns: [/yandexbot/i] },\n { name: 'Sogou', category: 'search_engine', patterns: [/sogou/i] },\n { name: 'Exabot', category: 'search_engine', patterns: [/exabot/i] },\n { name: 'ia_archiver', category: 'search_engine', patterns: [/ia_archiver/i] },\n]\n","import type { BotDetectionResult } from './bot-detection-result.js'\nimport { KNOWN_BOTS } from './known-patterns.js'\n\nexport interface DetectBotInput {\n userAgent: string\n /** Optional: headers map for heuristic checks */\n headers?: Record<string, string | string[] | undefined>\n /** Optional: IP address */\n ip?: string\n}\n\n/**\n * Three-layer bot detection pipeline:\n * 1. Known pattern matching (O(n) UA string match)\n * 2. Heuristic signals (missing headers, headless indicators)\n * 3. Auto-learner flag (unknown UAs that behave bot-like)\n */\nexport function detectBot(input: DetectBotInput): BotDetectionResult {\n const ua = input.userAgent ?? ''\n\n // Layer 1: known pattern match\n for (const bot of KNOWN_BOTS) {\n for (const pattern of bot.patterns) {\n if (pattern.test(ua)) {\n return {\n isBot: true,\n botName: bot.name,\n confidence: 'high',\n detectionMethod: 'known_pattern',\n category: bot.category,\n rawUserAgent: ua,\n }\n }\n }\n }\n\n // Layer 2: heuristics\n const heuristicResult = checkHeuristics(ua, input.headers ?? {})\n if (heuristicResult) return { ...heuristicResult, rawUserAgent: ua }\n\n // Layer 3: auto-learner — flag suspicious unknown UAs for review\n if (looksLikeBotUa(ua)) {\n return {\n isBot: true,\n botName: null,\n confidence: 'low',\n detectionMethod: 'auto_learned',\n category: 'unknown_bot',\n rawUserAgent: ua,\n }\n }\n\n return {\n isBot: false,\n botName: null,\n confidence: 'high',\n detectionMethod: 'none',\n category: 'human',\n rawUserAgent: ua,\n }\n}\n\nfunction checkHeuristics(\n ua: string,\n headers: Record<string, string | string[] | undefined>\n): Omit<BotDetectionResult, 'rawUserAgent'> | null {\n // Headless Chrome signals\n if (/headlesschrome/i.test(ua)) {\n return { isBot: true, botName: 'HeadlessChrome', confidence: 'medium', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/phantomjs/i.test(ua)) {\n return { isBot: true, botName: 'PhantomJS', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n if (/selenium/i.test(ua)) {\n return { isBot: true, botName: 'Selenium', confidence: 'high', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Empty or very short UA is suspicious\n if (ua.trim().length < 10) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n // Missing typical browser headers — only a bot signal when the UA does NOT\n // present itself as a real browser. Genuine browsers occasionally arrive\n // without accept-language/accept-encoding (privacy extensions, proxies,\n // some CDNs), so we must not flag them on missing headers alone.\n const hasAcceptLang = !!headers['accept-language']\n const hasAcceptEncoding = !!headers['accept-encoding']\n const claimsBrowser = /chrome|firefox|safari|edge|opera|gecko|applewebkit/i.test(ua)\n if (!hasAcceptLang && !hasAcceptEncoding && !claimsBrowser) {\n return { isBot: true, botName: null, confidence: 'low', detectionMethod: 'heuristic', category: 'unknown_bot' }\n }\n\n return null\n}\n\nfunction looksLikeBotUa(ua: string): boolean {\n return (\n /bot|crawler|spider|scraper|fetch|http|python|curl|java|ruby|go-http|node/i.test(ua) &&\n !/chrome|firefox|safari|edge|opera/i.test(ua)\n )\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "third-audience-mdx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Serve AI-optimized Markdown to LLM crawlers from MDX content sites. Track bot visits, citations, and AI discoverability.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|