third-audience-mdx 1.0.6 → 1.0.7
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/dist/dashboard/routes/llms-txt-route.js +16 -4
- package/dist/dashboard/routes/llms-txt-route.js.map +1 -1
- package/dist/dashboard/routes/llms-txt-route.mjs +16 -4
- package/dist/dashboard/routes/llms-txt-route.mjs.map +1 -1
- package/dist/dashboard/routes/markdown-route.js +20 -5
- package/dist/dashboard/routes/markdown-route.js.map +1 -1
- package/dist/dashboard/routes/markdown-route.mjs +20 -5
- package/dist/dashboard/routes/markdown-route.mjs.map +1 -1
- package/dist/dashboard/routes/okf-graph-route.js +16 -4
- package/dist/dashboard/routes/okf-graph-route.js.map +1 -1
- package/dist/dashboard/routes/okf-graph-route.mjs +16 -4
- package/dist/dashboard/routes/okf-graph-route.mjs.map +1 -1
- package/dist/dashboard/routes/okf-route.js +16 -4
- package/dist/dashboard/routes/okf-route.js.map +1 -1
- package/dist/dashboard/routes/okf-route.mjs +16 -4
- package/dist/dashboard/routes/okf-route.mjs.map +1 -1
- package/dist/dashboard/routes/sitemap-ai-route.js +16 -4
- package/dist/dashboard/routes/sitemap-ai-route.js.map +1 -1
- package/dist/dashboard/routes/sitemap-ai-route.mjs +16 -4
- package/dist/dashboard/routes/sitemap-ai-route.mjs.map +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -43,14 +43,26 @@ var import_gray_matter = __toESM(require("gray-matter"));
|
|
|
43
43
|
var MdxReader = class {
|
|
44
44
|
constructor(options) {
|
|
45
45
|
this.contentDir = options.contentDir;
|
|
46
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
50
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
51
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
52
|
+
*/
|
|
53
|
+
applyStrip(slug) {
|
|
54
|
+
if (this.stripSegments.length === 0) return slug;
|
|
55
|
+
const drop = new Set(this.stripSegments);
|
|
56
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
46
57
|
}
|
|
47
58
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
48
59
|
read(slug) {
|
|
60
|
+
const resolved = this.applyStrip(slug);
|
|
49
61
|
const candidates = [
|
|
50
|
-
import_path.default.join(this.contentDir, `${
|
|
51
|
-
import_path.default.join(this.contentDir, `${
|
|
52
|
-
import_path.default.join(this.contentDir,
|
|
53
|
-
import_path.default.join(this.contentDir,
|
|
62
|
+
import_path.default.join(this.contentDir, `${resolved}.mdx`),
|
|
63
|
+
import_path.default.join(this.contentDir, `${resolved}.md`),
|
|
64
|
+
import_path.default.join(this.contentDir, resolved, "index.mdx"),
|
|
65
|
+
import_path.default.join(this.contentDir, resolved, "index.md")
|
|
54
66
|
];
|
|
55
67
|
for (const filePath of candidates) {
|
|
56
68
|
if (import_fs.default.existsSync(filePath)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; 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, `${
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; 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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
|
|
@@ -9,14 +9,26 @@ import matter from "gray-matter";
|
|
|
9
9
|
var MdxReader = class {
|
|
10
10
|
constructor(options) {
|
|
11
11
|
this.contentDir = options.contentDir;
|
|
12
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
16
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
17
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
18
|
+
*/
|
|
19
|
+
applyStrip(slug) {
|
|
20
|
+
if (this.stripSegments.length === 0) return slug;
|
|
21
|
+
const drop = new Set(this.stripSegments);
|
|
22
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
12
23
|
}
|
|
13
24
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
14
25
|
read(slug) {
|
|
26
|
+
const resolved = this.applyStrip(slug);
|
|
15
27
|
const candidates = [
|
|
16
|
-
path.join(this.contentDir, `${
|
|
17
|
-
path.join(this.contentDir, `${
|
|
18
|
-
path.join(this.contentDir,
|
|
19
|
-
path.join(this.contentDir,
|
|
28
|
+
path.join(this.contentDir, `${resolved}.mdx`),
|
|
29
|
+
path.join(this.contentDir, `${resolved}.md`),
|
|
30
|
+
path.join(this.contentDir, resolved, "index.mdx"),
|
|
31
|
+
path.join(this.contentDir, resolved, "index.md")
|
|
20
32
|
];
|
|
21
33
|
for (const filePath of candidates) {
|
|
22
34
|
if (fs.existsSync(filePath)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; 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, `${
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/llms-txt-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/llms-txt.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateLlmsTxt } from '../../discovery/llms-txt.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /llms.txt → rewired to /api/third-audience/llms-txt */\nexport async function GET(req: NextRequest) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const content = generateLlmsTxt(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'text/plain; 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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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 '../core/mdx-reader.js'\n\n/**\n * Generates /llms.txt content from MDX frontmatter.\n * Format: one entry per line — URL, title, description.\n */\nexport function generateLlmsTxt(files: MdxFile[], baseUrl: string): string {\n const lines: string[] = [\n '# LLMs.txt — AI-readable content index',\n `# Generated by third-audience-mdx`,\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const url = `${baseUrl.replace(/\\/$/, '')}/${file.slug}`\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${url})${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n"],"mappings":";AAAA,SAAS,oBAAsC;AAC/C,OAAOA,WAAU;;;ACDjB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,YAAY;AAeZ,IAAM,YAAN,MAAgB;AAAA,EAIrB,YAAY,SAA2B;AACrC,SAAK,aAAa,QAAQ;AAC1B,SAAK,gBAAgB,QAAQ,iBAAiB,CAAC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,WAAW,MAAsB;AACvC,QAAI,KAAK,cAAc,WAAW,EAAG,QAAO;AAC5C,UAAM,OAAO,IAAI,IAAI,KAAK,aAAa;AACvC,WAAO,KACJ,MAAM,GAAG,EACT,OAAO,CAAC,QAAQ,CAAC,KAAK,IAAI,GAAG,CAAC,EAC9B,KAAK,GAAG;AAAA,EACb;AAAA;AAAA,EAGA,KAAK,MAA8B;AACjC,UAAM,WAAW,KAAK,WAAW,IAAI;AACrC,UAAM,aAAa;AAAA,MACjB,KAAK,KAAK,KAAK,YAAY,GAAG,QAAQ,MAAM;AAAA,MAC5C,KAAK,KAAK,KAAK,YAAY,GAAG,QAAQ,KAAK;AAAA,MAC3C,KAAK,KAAK,KAAK,YAAY,UAAU,WAAW;AAAA,MAChD,KAAK,KAAK,KAAK,YAAY,UAAU,UAAU;AAAA,IACjD;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;;;AC/EO,SAAS,gBAAgB,OAAkB,SAAyB;AACzE,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,KAAK,KAAK;AAChB,UAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,IAAI,KAAK,IAAI;AACtD,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,GAAG,IAAI,IAAI,EAAE;AAAA,EAC1C;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI;AAC5B;;;AFjBA,IAAM,SAAS,IAAI,UAAU,EAAE,YAAYC,MAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI,kBAAkB,SAAS,EAAE,CAAC;AAG9G,eAAsB,IAAI,KAAkB;AAC1C,QAAM,UAAU,QAAQ,IAAI,wBACvB,GAAG,IAAI,QAAQ,QAAQ,KAAK,IAAI,QAAQ,IAAI;AAEjD,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,UAAU,gBAAgB,OAAO,OAAO;AAE9C,SAAO,IAAI,aAAa,SAAS;AAAA,IAC/B,SAAS,EAAE,gBAAgB,4BAA4B;AAAA,EACzD,CAAC;AACH;","names":["path","path"]}
|
|
@@ -43,14 +43,26 @@ var import_gray_matter = __toESM(require("gray-matter"));
|
|
|
43
43
|
var MdxReader = class {
|
|
44
44
|
constructor(options) {
|
|
45
45
|
this.contentDir = options.contentDir;
|
|
46
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
50
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
51
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
52
|
+
*/
|
|
53
|
+
applyStrip(slug) {
|
|
54
|
+
if (this.stripSegments.length === 0) return slug;
|
|
55
|
+
const drop = new Set(this.stripSegments);
|
|
56
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
46
57
|
}
|
|
47
58
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
48
59
|
read(slug) {
|
|
60
|
+
const resolved = this.applyStrip(slug);
|
|
49
61
|
const candidates = [
|
|
50
|
-
import_path.default.join(this.contentDir, `${
|
|
51
|
-
import_path.default.join(this.contentDir, `${
|
|
52
|
-
import_path.default.join(this.contentDir,
|
|
53
|
-
import_path.default.join(this.contentDir,
|
|
62
|
+
import_path.default.join(this.contentDir, `${resolved}.mdx`),
|
|
63
|
+
import_path.default.join(this.contentDir, `${resolved}.md`),
|
|
64
|
+
import_path.default.join(this.contentDir, resolved, "index.mdx"),
|
|
65
|
+
import_path.default.join(this.contentDir, resolved, "index.md")
|
|
54
66
|
];
|
|
55
67
|
for (const filePath of candidates) {
|
|
56
68
|
if (import_fs.default.existsSync(filePath)) {
|
|
@@ -368,7 +380,10 @@ _VisitTracker.instance = null;
|
|
|
368
380
|
var VisitTracker = _VisitTracker;
|
|
369
381
|
|
|
370
382
|
// src/dashboard/routes/markdown-route.ts
|
|
371
|
-
var reader = new MdxReader({
|
|
383
|
+
var reader = new MdxReader({
|
|
384
|
+
contentDir: import_path4.default.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content"),
|
|
385
|
+
stripSegments: (process.env.TA_STRIP_SEGMENTS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
386
|
+
});
|
|
372
387
|
var renderer = new MarkdownRenderer();
|
|
373
388
|
var cache = new CacheManager({
|
|
374
389
|
cacheDir: import_path4.default.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")
|
|
@@ -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","../../../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
|
+
{"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({\n contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content'),\n stripSegments: (process.env.TA_STRIP_SEGMENTS ?? '').split(',').map((s) => s.trim()).filter(Boolean),\n})\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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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"]}
|
|
@@ -16,14 +16,26 @@ import matter from "gray-matter";
|
|
|
16
16
|
var MdxReader = class {
|
|
17
17
|
constructor(options) {
|
|
18
18
|
this.contentDir = options.contentDir;
|
|
19
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
23
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
24
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
25
|
+
*/
|
|
26
|
+
applyStrip(slug) {
|
|
27
|
+
if (this.stripSegments.length === 0) return slug;
|
|
28
|
+
const drop = new Set(this.stripSegments);
|
|
29
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
19
30
|
}
|
|
20
31
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
21
32
|
read(slug) {
|
|
33
|
+
const resolved = this.applyStrip(slug);
|
|
22
34
|
const candidates = [
|
|
23
|
-
path.join(this.contentDir, `${
|
|
24
|
-
path.join(this.contentDir, `${
|
|
25
|
-
path.join(this.contentDir,
|
|
26
|
-
path.join(this.contentDir,
|
|
35
|
+
path.join(this.contentDir, `${resolved}.mdx`),
|
|
36
|
+
path.join(this.contentDir, `${resolved}.md`),
|
|
37
|
+
path.join(this.contentDir, resolved, "index.mdx"),
|
|
38
|
+
path.join(this.contentDir, resolved, "index.md")
|
|
27
39
|
];
|
|
28
40
|
for (const filePath of candidates) {
|
|
29
41
|
if (fs.existsSync(filePath)) {
|
|
@@ -341,7 +353,10 @@ _VisitTracker.instance = null;
|
|
|
341
353
|
var VisitTracker = _VisitTracker;
|
|
342
354
|
|
|
343
355
|
// src/dashboard/routes/markdown-route.ts
|
|
344
|
-
var reader = new MdxReader({
|
|
356
|
+
var reader = new MdxReader({
|
|
357
|
+
contentDir: path4.join(process.cwd(), process.env.TA_CONTENT_DIR ?? "content"),
|
|
358
|
+
stripSegments: (process.env.TA_STRIP_SEGMENTS ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
359
|
+
});
|
|
345
360
|
var renderer = new MarkdownRenderer();
|
|
346
361
|
var cache = new CacheManager({
|
|
347
362
|
cacheDir: path4.join(process.cwd(), process.env.TA_DATA_DIR ?? "data", "ta-cache")
|
|
@@ -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","../../../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"]}
|
|
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({\n contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content'),\n stripSegments: (process.env.TA_STRIP_SEGMENTS ?? '').split(',').map((s) => s.trim()).filter(Boolean),\n})\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 /** URL path segments to drop when mapping a request slug to a file. */\n stripSegments?: string[]\n}\n\nexport class MdxReader {\n private contentDir: string\n private stripSegments: string[]\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n this.stripSegments = options.stripSegments ?? []\n }\n\n /**\n * Remove configured URL-only segments from a slug so it maps to the file\n * layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into\n * 'en/hydroponics/x'. Only whole path segments are removed.\n */\n private applyStrip(slug: string): string {\n if (this.stripSegments.length === 0) return slug\n const drop = new Set(this.stripSegments)\n return slug\n .split('/')\n .filter((seg) => !drop.has(seg))\n .join('/')\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const resolved = this.applyStrip(slug)\n const candidates = [\n path.join(this.contentDir, `${resolved}.mdx`),\n path.join(this.contentDir, `${resolved}.md`),\n path.join(this.contentDir, resolved, 'index.mdx'),\n path.join(this.contentDir, resolved, '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"]}
|
|
@@ -43,14 +43,26 @@ var import_gray_matter = __toESM(require("gray-matter"));
|
|
|
43
43
|
var MdxReader = class {
|
|
44
44
|
constructor(options) {
|
|
45
45
|
this.contentDir = options.contentDir;
|
|
46
|
+
this.stripSegments = options.stripSegments ?? [];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove configured URL-only segments from a slug so it maps to the file
|
|
50
|
+
* layout. e.g. stripSegments ['learn'] turns 'en/learn/hydroponics/x' into
|
|
51
|
+
* 'en/hydroponics/x'. Only whole path segments are removed.
|
|
52
|
+
*/
|
|
53
|
+
applyStrip(slug) {
|
|
54
|
+
if (this.stripSegments.length === 0) return slug;
|
|
55
|
+
const drop = new Set(this.stripSegments);
|
|
56
|
+
return slug.split("/").filter((seg) => !drop.has(seg)).join("/");
|
|
46
57
|
}
|
|
47
58
|
/** Read a single MDX file by slug. Returns null if not found. */
|
|
48
59
|
read(slug) {
|
|
60
|
+
const resolved = this.applyStrip(slug);
|
|
49
61
|
const candidates = [
|
|
50
|
-
import_path.default.join(this.contentDir, `${
|
|
51
|
-
import_path.default.join(this.contentDir, `${
|
|
52
|
-
import_path.default.join(this.contentDir,
|
|
53
|
-
import_path.default.join(this.contentDir,
|
|
62
|
+
import_path.default.join(this.contentDir, `${resolved}.mdx`),
|
|
63
|
+
import_path.default.join(this.contentDir, `${resolved}.md`),
|
|
64
|
+
import_path.default.join(this.contentDir, resolved, "index.mdx"),
|
|
65
|
+
import_path.default.join(this.contentDir, resolved, "index.md")
|
|
54
66
|
];
|
|
55
67
|
for (const filePath of candidates) {
|
|
56
68
|
if (import_fs.default.existsSync(filePath)) {
|