third-audience-mdx 1.0.6 → 1.0.8
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/admin-store.d.mts +28 -0
- package/dist/dashboard/admin-store.d.ts +28 -0
- package/dist/dashboard/admin-store.js +191 -0
- package/dist/dashboard/admin-store.js.map +1 -0
- package/dist/dashboard/admin-store.mjs +142 -0
- package/dist/dashboard/admin-store.mjs.map +1 -0
- 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/dashboard/ui/components/Card.d.mts +11 -0
- package/dist/dashboard/ui/components/Card.d.ts +11 -0
- package/dist/dashboard/ui/components/Card.js +40 -0
- package/dist/dashboard/ui/components/Card.js.map +1 -0
- package/dist/dashboard/ui/components/Card.mjs +15 -0
- package/dist/dashboard/ui/components/Card.mjs.map +1 -0
- package/dist/dashboard/ui/components/HeroCard.d.mts +13 -0
- package/dist/dashboard/ui/components/HeroCard.d.ts +13 -0
- package/dist/dashboard/ui/components/HeroCard.js +41 -0
- package/dist/dashboard/ui/components/HeroCard.js.map +1 -0
- package/dist/dashboard/ui/components/HeroCard.mjs +16 -0
- package/dist/dashboard/ui/components/HeroCard.mjs.map +1 -0
- package/dist/dashboard/ui/components/VisitsChart.d.mts +13 -0
- package/dist/dashboard/ui/components/VisitsChart.d.ts +13 -0
- package/dist/dashboard/ui/components/VisitsChart.js +82 -0
- package/dist/dashboard/ui/components/VisitsChart.js.map +1 -0
- package/dist/dashboard/ui/components/VisitsChart.mjs +58 -0
- package/dist/dashboard/ui/components/VisitsChart.mjs.map +1 -0
- 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 +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/dashboard/routes/okf-graph-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { buildOkfGraph } from '../../okf/okf-bundle.js'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** GET /api/third-audience/okf-graph — returns graph JSON for the OKF dashboard viewer */\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const graph = buildOkfGraph(files, baseUrl)\n\n return NextResponse.json({\n graph,\n stats: {\n pages: files.length,\n nodes: graph.nodes.length,\n edges: graph.edges.length,\n },\n indexUrl: `${baseUrl}/okf`,\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n","import type { NextRequest } from 'next/server'\nimport { NextResponse } from 'next/server'\nimport { verifySession, verifyApiKey } from './admin-store.js'\n\nconst SESSION_COOKIE = 'ta_session'\n\n/**\n * Authenticate an API route request. Accepts (in order):\n * 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)\n * 2. Authorization: Bearer <api-key> — same key, different transport\n * 3. Valid ta_session cookie — browser dashboard session\n */\nexport function checkApiAuth(req: NextRequest): boolean {\n // 1. X-TA-Api-Key header (WP-style headless key)\n const apiKeyHeader = req.headers.get('x-ta-api-key')\n if (apiKeyHeader) return verifyApiKey(apiKeyHeader)\n\n // 2. Bearer token (treat as api key)\n const auth = req.headers.get('authorization') ?? ''\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7)\n return verifyApiKey(token)\n }\n\n // 3. Browser session cookie\n const session = req.cookies.get(SESSION_COOKIE)?.value\n if (session) return verifySession(session)\n\n return false\n}\n\n/**\n * Returns a 401 JSON response with the correct WWW-Authenticate header.\n * Use as: if (!checkApiAuth(req)) return unauthorizedResponse()\n */\nexport function unauthorizedResponse(): NextResponse {\n return NextResponse.json(\n { error: 'Unauthorized. Provide X-TA-Api-Key header or a valid session cookie.' },\n {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer realm=\"Third Audience API\"' },\n }\n )\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\nexport interface AdminRecord {\n passwordHash: string // sha256(secret + password)\n isDefaultPassword: boolean\n createdAt: string\n lastLoginAt: string | null\n apiKey?: string // AES-256-GCM encrypted, for headless/external API callers\n}\n\nfunction adminFilePath(): string {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n return path.join(process.cwd(), dataDir, 'ta-admin.json')\n}\n\nexport function generateDefaultPassword(): string {\n return crypto.randomBytes(6).toString('hex') // 12-char hex, easy to type\n}\n\nexport function hashPassword(password: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n return crypto.createHash('sha256').update(secret + password).digest('hex')\n}\n\nexport function loadAdmin(): AdminRecord | null {\n const filePath = adminFilePath()\n if (!fs.existsSync(filePath)) return null\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AdminRecord\n } catch {\n return null\n }\n}\n\nexport function saveAdmin(record: AdminRecord): void {\n const filePath = adminFilePath()\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8')\n}\n\nexport const DEFAULT_PASSWORD = 'Chang3M3Now!'\n\nexport function initAdmin(): { password: string; apiKey: string; isNew: boolean } {\n const existing = loadAdmin()\n if (existing) return { password: '', apiKey: '', isNew: false }\n\n const apiKey = generateApiKey()\n saveAdmin({\n passwordHash: hashPassword(DEFAULT_PASSWORD),\n isDefaultPassword: true,\n createdAt: new Date().toISOString(),\n lastLoginAt: null,\n apiKey: encryptApiKey(apiKey),\n })\n return { password: DEFAULT_PASSWORD, apiKey, isNew: true }\n}\n\nexport function verifyPassword(password: string): boolean {\n const record = loadAdmin()\n if (!record) return false\n return record.passwordHash === hashPassword(password)\n}\n\nexport function updatePassword(newPassword: string): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({\n ...record,\n passwordHash: hashPassword(newPassword),\n isDefaultPassword: false,\n })\n}\n\nexport function recordLogin(): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({ ...record, lastLoginAt: new Date().toISOString() })\n}\n\n// ---------------------------------------------------------------------------\n// API key — AES-256-GCM encrypted at rest, mirroring WP's SECURE_AUTH_KEY approach\n// ---------------------------------------------------------------------------\n\nconst CIPHER = 'aes-256-gcm'\n\nfunction getEncryptionKey(): Buffer {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-fallback-key-change-me'\n // Derive a 32-byte key from the secret using SHA-256\n return crypto.createHash('sha256').update(secret).digest()\n}\n\nfunction encryptApiKey(plaintext: string): string {\n const iv = crypto.randomBytes(12)\n const key = getEncryptionKey()\n const cipher = crypto.createCipheriv(CIPHER, key, iv) as crypto.CipherGCM\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n // Format: iv(24 hex) + tag(32 hex) + encrypted(hex)\n return iv.toString('hex') + tag.toString('hex') + encrypted.toString('hex')\n}\n\nfunction decryptApiKey(encoded: string): string | null {\n try {\n const iv = Buffer.from(encoded.slice(0, 24), 'hex')\n const tag = Buffer.from(encoded.slice(24, 56), 'hex')\n const encrypted = Buffer.from(encoded.slice(56), 'hex')\n const key = getEncryptionKey()\n const decipher = crypto.createDecipheriv(CIPHER, key, iv) as crypto.DecipherGCM\n decipher.setAuthTag(tag)\n return decipher.update(encrypted) + decipher.final('utf8')\n } catch {\n return null\n }\n}\n\nexport function generateApiKey(): string {\n return 'ta_' + crypto.randomBytes(24).toString('hex') // 51-char key\n}\n\nexport function getApiKey(): string | null {\n const record = loadAdmin()\n if (!record?.apiKey) return null\n return decryptApiKey(record.apiKey)\n}\n\nexport function rotateApiKey(): string {\n const record = loadAdmin()\n if (!record) throw new Error('Admin store not initialised')\n const newKey = generateApiKey()\n saveAdmin({ ...record, apiKey: encryptApiKey(newKey) })\n return newKey\n}\n\nexport function verifyApiKey(key: string): boolean {\n const stored = getApiKey()\n if (!stored) return false\n if (key.length !== stored.length) return false\n return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored))\n}\n\n// ---------------------------------------------------------------------------\n// Session cookie: HMAC-SHA256(secret, userId + timestamp) — stateless, no DB\n// ---------------------------------------------------------------------------\nexport function signSession(payload: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): boolean {\n const lastDot = token.lastIndexOf('.')\n if (lastDot === -1) return false\n const payload = token.slice(0, lastDot)\n const sig = token.slice(lastDot + 1)\n const expected = crypto.createHmac('sha256', process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt')\n .update(payload).digest('hex')\n // Constant-time comparison\n if (sig.length !== expected.length) return false\n return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))\n}\n"],"mappings":"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","names":["NextResponse","path","fs","path","fs","path","NextResponse"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/okf-graph-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts","../../../src/dashboard/auth.ts","../../../src/dashboard/admin-store.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { buildOkfGraph } from '../../okf/okf-bundle.js'\nimport { checkApiAuth, unauthorizedResponse } from '../auth.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** GET /api/third-audience/okf-graph — returns graph JSON for the OKF dashboard viewer */\nexport async function GET(req: NextRequest): Promise<NextResponse> {\n if (!checkApiAuth(req)) return unauthorizedResponse()\n\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const files = reader.readAll()\n const graph = buildOkfGraph(files, baseUrl)\n\n return NextResponse.json({\n graph,\n stats: {\n pages: files.length,\n nodes: graph.nodes.length,\n edges: graph.edges.length,\n },\n indexUrl: `${baseUrl}/okf`,\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 type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n","import type { NextRequest } from 'next/server'\nimport { NextResponse } from 'next/server'\nimport { verifySession, verifyApiKey } from './admin-store.js'\n\nconst SESSION_COOKIE = 'ta_session'\n\n/**\n * Authenticate an API route request. Accepts (in order):\n * 1. X-TA-Api-Key header — for headless/external callers (mirrors WP's approach)\n * 2. Authorization: Bearer <api-key> — same key, different transport\n * 3. Valid ta_session cookie — browser dashboard session\n */\nexport function checkApiAuth(req: NextRequest): boolean {\n // 1. X-TA-Api-Key header (WP-style headless key)\n const apiKeyHeader = req.headers.get('x-ta-api-key')\n if (apiKeyHeader) return verifyApiKey(apiKeyHeader)\n\n // 2. Bearer token (treat as api key)\n const auth = req.headers.get('authorization') ?? ''\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7)\n return verifyApiKey(token)\n }\n\n // 3. Browser session cookie\n const session = req.cookies.get(SESSION_COOKIE)?.value\n if (session) return verifySession(session)\n\n return false\n}\n\n/**\n * Returns a 401 JSON response with the correct WWW-Authenticate header.\n * Use as: if (!checkApiAuth(req)) return unauthorizedResponse()\n */\nexport function unauthorizedResponse(): NextResponse {\n return NextResponse.json(\n { error: 'Unauthorized. Provide X-TA-Api-Key header or a valid session cookie.' },\n {\n status: 401,\n headers: { 'WWW-Authenticate': 'Bearer realm=\"Third Audience API\"' },\n }\n )\n}\n","import fs from 'fs'\nimport path from 'path'\nimport crypto from 'crypto'\n\nexport interface AdminRecord {\n passwordHash: string // sha256(secret + password)\n isDefaultPassword: boolean\n createdAt: string\n lastLoginAt: string | null\n apiKey?: string // AES-256-GCM encrypted, for headless/external API callers\n}\n\nfunction adminFilePath(): string {\n const dataDir = process.env.TA_DATA_DIR ?? 'data'\n return path.join(process.cwd(), dataDir, 'ta-admin.json')\n}\n\nexport function generateDefaultPassword(): string {\n return crypto.randomBytes(6).toString('hex') // 12-char hex, easy to type\n}\n\nexport function hashPassword(password: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n return crypto.createHash('sha256').update(secret + password).digest('hex')\n}\n\nexport function loadAdmin(): AdminRecord | null {\n const filePath = adminFilePath()\n if (!fs.existsSync(filePath)) return null\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AdminRecord\n } catch {\n return null\n }\n}\n\nexport function saveAdmin(record: AdminRecord): void {\n const filePath = adminFilePath()\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8')\n}\n\nexport const DEFAULT_PASSWORD = 'Chang3M3Now!'\n\nexport function initAdmin(): { password: string; apiKey: string; isNew: boolean } {\n const existing = loadAdmin()\n if (existing) return { password: '', apiKey: '', isNew: false }\n\n const apiKey = generateApiKey()\n saveAdmin({\n passwordHash: hashPassword(DEFAULT_PASSWORD),\n isDefaultPassword: true,\n createdAt: new Date().toISOString(),\n lastLoginAt: null,\n apiKey: encryptApiKey(apiKey),\n })\n return { password: DEFAULT_PASSWORD, apiKey, isNew: true }\n}\n\nexport function verifyPassword(password: string): boolean {\n const record = loadAdmin()\n if (!record) return false\n return record.passwordHash === hashPassword(password)\n}\n\nexport function updatePassword(newPassword: string): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({\n ...record,\n passwordHash: hashPassword(newPassword),\n isDefaultPassword: false,\n })\n}\n\nexport function recordLogin(): void {\n const record = loadAdmin()\n if (!record) return\n saveAdmin({ ...record, lastLoginAt: new Date().toISOString() })\n}\n\n// ---------------------------------------------------------------------------\n// API key — AES-256-GCM encrypted at rest, mirroring WP's SECURE_AUTH_KEY approach\n// ---------------------------------------------------------------------------\n\nconst CIPHER = 'aes-256-gcm'\n\nfunction getEncryptionKey(): Buffer {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-fallback-key-change-me'\n // Derive a 32-byte key from the secret using SHA-256\n return crypto.createHash('sha256').update(secret).digest()\n}\n\nfunction encryptApiKey(plaintext: string): string {\n const iv = crypto.randomBytes(12)\n const key = getEncryptionKey()\n const cipher = crypto.createCipheriv(CIPHER, key, iv) as crypto.CipherGCM\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n // Format: iv(24 hex) + tag(32 hex) + encrypted(hex)\n return iv.toString('hex') + tag.toString('hex') + encrypted.toString('hex')\n}\n\nfunction decryptApiKey(encoded: string): string | null {\n try {\n const iv = Buffer.from(encoded.slice(0, 24), 'hex')\n const tag = Buffer.from(encoded.slice(24, 56), 'hex')\n const encrypted = Buffer.from(encoded.slice(56), 'hex')\n const key = getEncryptionKey()\n const decipher = crypto.createDecipheriv(CIPHER, key, iv) as crypto.DecipherGCM\n decipher.setAuthTag(tag)\n return decipher.update(encrypted) + decipher.final('utf8')\n } catch {\n return null\n }\n}\n\nexport function generateApiKey(): string {\n return 'ta_' + crypto.randomBytes(24).toString('hex') // 51-char key\n}\n\nexport function getApiKey(): string | null {\n const record = loadAdmin()\n if (!record?.apiKey) return null\n return decryptApiKey(record.apiKey)\n}\n\nexport function rotateApiKey(): string {\n const record = loadAdmin()\n if (!record) throw new Error('Admin store not initialised')\n const newKey = generateApiKey()\n saveAdmin({ ...record, apiKey: encryptApiKey(newKey) })\n return newKey\n}\n\nexport function verifyApiKey(key: string): boolean {\n const stored = getApiKey()\n if (!stored) return false\n if (key.length !== stored.length) return false\n return crypto.timingSafeEqual(Buffer.from(key), Buffer.from(stored))\n}\n\n// ---------------------------------------------------------------------------\n// Session cookie: HMAC-SHA256(secret, userId + timestamp) — stateless, no DB\n// ---------------------------------------------------------------------------\nexport function signSession(payload: string): string {\n const secret = process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt'\n const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex')\n return `${payload}.${sig}`\n}\n\nexport function verifySession(token: string): boolean {\n const lastDot = token.lastIndexOf('.')\n if (lastDot === -1) return false\n const payload = token.slice(0, lastDot)\n const sig = token.slice(lastDot + 1)\n const expected = crypto.createHmac('sha256', process.env.THIRD_AUDIENCE_SECRET ?? 'ta-salt')\n .update(payload).digest('hex')\n // Constant-time comparison\n if (sig.length !== expected.length) return false\n return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))\n}\n"],"mappings":";AAAA,SAAS,gBAAAA,qBAAsC;AAC/C,OAAOC,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;;;ACtEO,IAAM,mBAAN,MAAuB;AAAA,EAC5B,OAAO,MAAuB;AAC5B,UAAM,SAAS,KAAK,uBAAuB,KAAK,WAAW;AAC3D,UAAM,OAAO,KAAK,SAAS,KAAK,UAAU;AAC1C,WAAO,SAAS,GAAG,MAAM;AAAA;AAAA,EAAO,IAAI,KAAK;AAAA,EAC3C;AAAA,EAEQ,uBAAuB,IAAqC;AAClE,UAAM,OAAO,OAAO,KAAK,EAAE;AAC3B,QAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,UAAM,QAAQ,KACX,OAAO,OAAK,GAAG,CAAC,MAAM,UAAa,GAAG,CAAC,MAAM,IAAI,EACjD,IAAI,OAAK,GAAG,CAAC,KAAK,KAAK,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE;AAC5C,WAAO;AAAA,EAAQ,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA,EACjC;AAAA,EAEQ,UAAU,KAAsB;AACtC,QAAI,OAAO,QAAQ,UAAU;AAE3B,aAAO,yBAAyB,KAAK,GAAG,IAAI,IAAI,IAAI,QAAQ,MAAM,KAAK,CAAC,MAAM;AAAA,IAChF;AACA,QAAI,eAAe,KAAM,QAAO,IAAI,YAAY;AAChD,QAAI,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI,IAAI,IAAI,OAAK,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAC7E,WAAO,OAAO,GAAG;AAAA,EACnB;AAAA,EAEQ,SAAS,SAAyB;AACxC,QAAI,MAAM;AAGV,UAAM,IAAI,QAAQ,oCAAoC,EAAE;AAGxD,UAAM,IAAI,QAAQ,kGAAkG,EAAE;AACtH,UAAM,IAAI,QAAQ,4DAA4D,EAAE;AAIhF,UAAM,IAAI,QAAQ,kCAAkC,EAAE;AAItD,UAAM,IAAI,QAAQ,8CAA8C,EAAE;AAGlE,UAAM,IAAI,QAAQ,wBAAwB,EAAE;AAG5C,UAAM,IAAI,QAAQ,WAAW,MAAM;AAEnC,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;;;AChEA,IAAM,WAAW,IAAI,iBAAiB;AAwB/B,SAAS,cAAc,OAAkB,SAA+B;AAC7E,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AACtC,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,IAAI,CAAC;AAG9C,QAAM,QAAQ,oBAAI,IAAoB;AACtC,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,KAAK,MAAM,SAAS,OAAO,IAAI,CAAC;AAAA,EAC5C;AAGA,QAAM,UAAU,IAAI,IAAoB,MAAM,IAAI,OAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACnE,QAAM,WAA2B,CAAC;AAElC,aAAW,QAAQ,OAAO;AACxB,UAAM,KAAK,MAAM,IAAI,KAAK,IAAI,KAAK;AACnC,UAAM,SAAS;AACf,QAAI;AACJ,YAAQ,IAAI,OAAO,KAAK,EAAE,OAAO,MAAM;AACrC,YAAM,YAAY,EAAE,CAAC,EAAE,QAAQ,OAAO,EAAE,EAAE,QAAQ,SAAS,EAAE;AAC7D,UAAI,QAAQ,IAAI,SAAS,KAAK,cAAc,KAAK,MAAM;AACrD,iBAAS,KAAK,EAAE,QAAQ,KAAK,MAAM,QAAQ,UAAU,CAAC;AACtD,gBAAQ,IAAI,KAAK,OAAO,QAAQ,IAAI,KAAK,IAAI,KAAK,KAAK,CAAC;AACxD,gBAAQ,IAAI,YAAY,QAAQ,IAAI,SAAS,KAAK,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAGA,QAAM,SAAS,MACZ,MAAM,EACN,KAAK,CAAC,GAAG,OAAO,QAAQ,IAAI,EAAE,IAAI,KAAK,MAAM,QAAQ,IAAI,EAAE,IAAI,KAAK,EAAE,EACtE,MAAM,GAAG,GAAG;AACf,QAAM,SAAS,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,IAAI,CAAC;AAE9C,QAAM,QAAwB,OAAO,IAAI,QAAM;AAAA,IAC7C,IAAI,EAAE;AAAA,IACN,OAAO,OAAO,EAAE,YAAY,SAAS,EAAE,IAAI;AAAA,IAC3C,MAAM,OAAO,EAAE,YAAY,QAAQ,SAAS;AAAA,IAC5C,KAAK,GAAG,IAAI,IAAI,EAAE,IAAI;AAAA,EACxB,EAAE;AAEF,QAAM,QAAQ,SAAS,OAAO,OAAK,OAAO,IAAI,EAAE,MAAM,KAAK,OAAO,IAAI,EAAE,MAAM,CAAC;AAE/E,SAAO,EAAE,OAAO,MAAM;AACxB;;;ACvEA,SAAS,oBAAoB;;;ACD7B,OAAOC,SAAQ;AACf,OAAOC,WAAU;AACjB,OAAO,YAAY;AAUnB,SAAS,gBAAwB;AAC/B,QAAM,UAAU,QAAQ,IAAI,eAAe;AAC3C,SAAOA,MAAK,KAAK,QAAQ,IAAI,GAAG,SAAS,eAAe;AAC1D;AAWO,SAAS,YAAgC;AAC9C,QAAM,WAAW,cAAc;AAC/B,MAAI,CAACC,IAAG,WAAW,QAAQ,EAAG,QAAO;AACrC,MAAI;AACF,WAAO,KAAK,MAAMA,IAAG,aAAa,UAAU,OAAO,CAAC;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAoDA,IAAM,SAAS;AAEf,SAAS,mBAA2B;AAClC,QAAM,SAAS,QAAQ,IAAI,yBAAyB;AAEpD,SAAO,OAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO;AAC3D;AAYA,SAAS,cAAc,SAAgC;AACrD,MAAI;AACF,UAAM,KAAK,OAAO,KAAK,QAAQ,MAAM,GAAG,EAAE,GAAG,KAAK;AAClD,UAAM,MAAM,OAAO,KAAK,QAAQ,MAAM,IAAI,EAAE,GAAG,KAAK;AACpD,UAAM,YAAY,OAAO,KAAK,QAAQ,MAAM,EAAE,GAAG,KAAK;AACtD,UAAM,MAAM,iBAAiB;AAC7B,UAAM,WAAW,OAAO,iBAAiB,QAAQ,KAAK,EAAE;AACxD,aAAS,WAAW,GAAG;AACvB,WAAO,SAAS,OAAO,SAAS,IAAI,SAAS,MAAM,MAAM;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,YAA2B;AACzC,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,cAAc,OAAO,MAAM;AACpC;AAUO,SAAS,aAAa,KAAsB;AACjD,QAAM,SAAS,UAAU;AACzB,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,IAAI,WAAW,OAAO,OAAQ,QAAO;AACzC,SAAO,OAAO,gBAAgB,OAAO,KAAK,GAAG,GAAG,OAAO,KAAK,MAAM,CAAC;AACrE;AAWO,SAAS,cAAc,OAAwB;AACpD,QAAM,UAAU,MAAM,YAAY,GAAG;AACrC,MAAI,YAAY,GAAI,QAAO;AAC3B,QAAM,UAAU,MAAM,MAAM,GAAG,OAAO;AACtC,QAAM,MAAM,MAAM,MAAM,UAAU,CAAC;AACnC,QAAM,WAAW,OAAO,WAAW,UAAU,QAAQ,IAAI,yBAAyB,SAAS,EACxF,OAAO,OAAO,EAAE,OAAO,KAAK;AAE/B,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,SAAO,OAAO,gBAAgB,OAAO,KAAK,KAAK,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,CAAC;AACrF;;;AD9JA,IAAM,iBAAiB;AAQhB,SAAS,aAAa,KAA2B;AAEtD,QAAM,eAAe,IAAI,QAAQ,IAAI,cAAc;AACnD,MAAI,aAAc,QAAO,aAAa,YAAY;AAGlD,QAAM,OAAO,IAAI,QAAQ,IAAI,eAAe,KAAK;AACjD,MAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,UAAM,QAAQ,KAAK,MAAM,CAAC;AAC1B,WAAO,aAAa,KAAK;AAAA,EAC3B;AAGA,QAAM,UAAU,IAAI,QAAQ,IAAI,cAAc,GAAG;AACjD,MAAI,QAAS,QAAO,cAAc,OAAO;AAEzC,SAAO;AACT;AAMO,SAAS,uBAAqC;AACnD,SAAO,aAAa;AAAA,IAClB,EAAE,OAAO,uEAAuE;AAAA,IAChF;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,oBAAoB,oCAAoC;AAAA,IACrE;AAAA,EACF;AACF;;;AJrCA,IAAM,SAAS,IAAI,UAAU,EAAE,YAAYC,MAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,IAAI,kBAAkB,SAAS,EAAE,CAAC;AAG9G,eAAsB,IAAI,KAAyC;AACjE,MAAI,CAAC,aAAa,GAAG,EAAG,QAAO,qBAAqB;AAEpD,QAAM,UAAU,QAAQ,IAAI,wBACvB,GAAG,IAAI,QAAQ,QAAQ,KAAK,IAAI,QAAQ,IAAI;AAEjD,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,QAAQ,cAAc,OAAO,OAAO;AAE1C,SAAOC,cAAa,KAAK;AAAA,IACvB;AAAA,IACA,OAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,OAAO,MAAM,MAAM;AAAA,MACnB,OAAO,MAAM,MAAM;AAAA,IACrB;AAAA,IACA,UAAU,GAAG,OAAO;AAAA,EACtB,CAAC;AACH;","names":["NextResponse","path","fs","path","fs","path","NextResponse"]}
|
|
@@ -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/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n /** 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 type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["import_path","path","fs","matter","path"]}
|
|
@@ -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/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n}\n\nexport class MdxReader {\n private contentDir: string\n\n constructor(options: MdxReaderOptions) {\n this.contentDir = options.contentDir\n }\n\n /** Read a single MDX file by slug. Returns null if not found. */\n read(slug: string): MdxFile | null {\n const candidates = [\n path.join(this.contentDir, `${slug}.mdx`),\n path.join(this.contentDir, `${slug}.md`),\n path.join(this.contentDir, slug, 'index.mdx'),\n path.join(this.contentDir, slug, 'index.md'),\n ]\n\n for (const filePath of candidates) {\n if (fs.existsSync(filePath)) {\n return this.parseFile(slug, filePath)\n }\n }\n\n return null\n }\n\n /** Read all MDX files recursively. */\n readAll(): MdxFile[] {\n if (!fs.existsSync(this.contentDir)) return []\n return this.walkDir(this.contentDir, this.contentDir)\n }\n\n private walkDir(dir: string, root: string): MdxFile[] {\n const results: MdxFile[] = []\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n results.push(...this.walkDir(fullPath, root))\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n const relative = path.relative(root, fullPath)\n const slug = relative.replace(/\\.(mdx|md)$/, '').replace(/\\/index$/, '')\n results.push(this.parseFile(slug, fullPath))\n }\n }\n return results\n }\n\n private parseFile(slug: string, filePath: string): MdxFile {\n const raw = fs.readFileSync(filePath, 'utf-8')\n const { data: frontmatter, content: rawContent } = matter(raw)\n return { slug, filePath, frontmatter, rawContent }\n }\n}\n","import type { MdxFile } from './mdx-reader.js'\n\n/**\n * Strips JSX from MDX content and returns clean Markdown\n * suitable for AI crawlers.\n *\n * Removes:\n * - import/export statements\n * - JSX component tags (<ComponentName ... /> and <ComponentName>...</ComponentName>)\n * - Inline expressions {variable} that aren't standard Markdown\n *\n * Preserves:\n * - All standard Markdown (headings, lists, code blocks, links, images)\n * - Frontmatter (serialized as YAML header)\n */\nexport class MarkdownRenderer {\n render(file: MdxFile): string {\n const header = this.buildFrontmatterHeader(file.frontmatter)\n const body = this.stripJsx(file.rawContent)\n return header ? `${header}\\n\\n${body}` : body\n }\n\n private buildFrontmatterHeader(fm: Record<string, unknown>): string {\n const keys = Object.keys(fm)\n if (keys.length === 0) return ''\n const lines = keys\n .filter(k => fm[k] !== undefined && fm[k] !== null)\n .map(k => `${k}: ${this.yamlValue(fm[k])}`)\n return `---\\n${lines.join('\\n')}\\n---`\n }\n\n private yamlValue(val: unknown): string {\n if (typeof val === 'string') {\n // Quote strings containing special YAML chars\n return /[:#\\[\\]{},&*?|<>=!%@`]/.test(val) ? `\"${val.replace(/\"/g, '\\\\\"')}\"` : val\n }\n if (val instanceof Date) return val.toISOString()\n if (Array.isArray(val)) return `[${val.map(v => this.yamlValue(v)).join(', ')}]`\n return String(val)\n }\n\n private stripJsx(content: string): string {\n let out = content\n\n // Remove import statements: import Foo from '...' / import { Foo } from '...'\n out = out.replace(/^import\\s+.*?['\"].*?['\"]\\s*\\n?/gm, '')\n\n // Remove export statements at line start (export const, export default, export { })\n out = out.replace(/^export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+[\\s\\S]*?(?=\\n(?=[^{]|\\n)|\\n{2,})/gm, '')\n out = out.replace(/^export\\s*\\{[^}]*\\}\\s*(?:from\\s+['\"][^'\"]*['\"])?\\s*\\n?/gm, '')\n\n // Remove self-closing JSX tags: <Component ... />\n // Must not match HTML img/br/hr which are valid Markdown\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*\\/>/g, '')\n\n // Remove JSX block tags: <Component ...>...</Component>\n // Greedy but bounded by matching closing tag\n out = out.replace(/<([A-Z][A-Za-z0-9.]*)[^>]*>[\\s\\S]*?<\\/\\1>/g, '')\n\n // Remove JSX expression blocks { expression } that span a whole line\n out = out.replace(/^\\s*\\{[^}]+\\}\\s*\\n/gm, '')\n\n // Collapse multiple blank lines to two\n out = out.replace(/\\n{3,}/g, '\\n\\n')\n\n return out.trim()\n }\n}\n","import type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["path","path"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/dashboard/routes/okf-route.ts","../../../src/core/mdx-reader.ts","../../../src/core/markdown-renderer.ts","../../../src/okf/okf-bundle.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateOkfIndex, generateOkfPage } from '../../okf/okf-bundle.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/**\n * Handler for /okf/ and /okf/[...slug].md\n * Rewired from middleware to /api/third-audience/okf/[...path]\n */\nexport async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) {\n const baseUrl = process.env.NEXT_PUBLIC_SITE_URL\n ?? `${req.nextUrl.protocol}//${req.nextUrl.host}`\n\n const allFiles = reader.readAll()\n const { path: pathSegments } = await params\n const segments = pathSegments ?? []\n\n // /okf/ or /okf/index or /okf/index.md → manifest\n if (segments.length === 0 || (segments.length === 1 && (segments[0] === 'index.md' || segments[0] === 'index'))) {\n return new NextResponse(generateOkfIndex(allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n }\n\n // /okf/[slug].md → individual page\n const slug = segments.join('/').replace(/\\.md$/, '')\n const file = allFiles.find(f => f.slug === slug)\n if (!file) return new NextResponse('Not Found', { status: 404 })\n\n return new NextResponse(generateOkfPage(file, allFiles, baseUrl), {\n headers: { 'Content-Type': 'text/markdown; charset=utf-8' },\n })\n}\n","import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\n\nexport interface MdxFile {\n slug: string // relative path without extension, e.g. 'blog/my-post'\n filePath: string // absolute path to .mdx file\n frontmatter: Record<string, unknown>\n rawContent: string // body after frontmatter\n}\n\nexport interface MdxReaderOptions {\n contentDir: string // absolute path to content directory\n /** 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 type { MdxFile } from '../core/mdx-reader.js'\nimport { MarkdownRenderer } from '../core/markdown-renderer.js'\n\nconst renderer = new MarkdownRenderer()\n\nexport interface OkfGraphNode {\n id: string\n title: string\n type: string\n url: string\n}\n\nexport interface OkfGraphEdge {\n source: string\n target: string\n}\n\nexport interface OkfGraphData {\n nodes: OkfGraphNode[]\n edges: OkfGraphEdge[]\n}\n\n/**\n * Builds the knowledge graph data for the OKF viewer.\n * Nodes = content pages; edges = internal links between them.\n * Trims to top 100 most-connected nodes (matching WP plugin behaviour).\n */\nexport function buildOkfGraph(files: MdxFile[], baseUrl: string): OkfGraphData {\n const base = baseUrl.replace(/\\/$/, '')\n const slugSet = new Set(files.map(f => f.slug))\n\n // Build slug → markdown map for link extraction\n const mdMap = new Map<string, string>()\n for (const file of files) {\n mdMap.set(file.slug, renderer.render(file))\n }\n\n // Count degrees to pick top 100\n const degrees = new Map<string, number>(files.map(f => [f.slug, 0]))\n const rawEdges: OkfGraphEdge[] = []\n\n for (const file of files) {\n const md = mdMap.get(file.slug) ?? ''\n const linkRe = /\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g\n let m: RegExpExecArray | null\n while ((m = linkRe.exec(md)) !== null) {\n const candidate = m[2].replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate) && candidate !== file.slug) {\n rawEdges.push({ source: file.slug, target: candidate })\n degrees.set(file.slug, (degrees.get(file.slug) ?? 0) + 1)\n degrees.set(candidate, (degrees.get(candidate) ?? 0) + 1)\n }\n }\n }\n\n // Top 100 nodes by degree\n const top100 = files\n .slice()\n .sort((a, b) => (degrees.get(b.slug) ?? 0) - (degrees.get(a.slug) ?? 0))\n .slice(0, 100)\n const topSet = new Set(top100.map(f => f.slug))\n\n const nodes: OkfGraphNode[] = top100.map(f => ({\n id: f.slug,\n title: String(f.frontmatter.title ?? f.slug),\n type: String(f.frontmatter.type ?? 'WebPage'),\n url: `${base}/${f.slug}`,\n }))\n\n const edges = rawEdges.filter(e => topSet.has(e.source) && topSet.has(e.target))\n\n return { nodes, edges }\n}\n\n/** Generates the /okf/index.md manifest listing all content. */\nexport function generateOkfIndex(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const lines = [\n '# Open Knowledge Format (OKF) Bundle',\n '',\n 'This bundle contains all content as clean Markdown files for AI consumption.',\n '',\n '## Contents',\n '',\n ]\n\n for (const file of files) {\n const fm = file.frontmatter\n const title = String(fm.title ?? file.slug)\n const desc = fm.description ? ` — ${String(fm.description)}` : ''\n lines.push(`- [${title}](${base}/okf/${file.slug}.md)${desc}`)\n }\n\n return lines.join('\\n') + '\\n'\n}\n\n/** Renders a single MDX file for OKF, with internal links rewritten to .md siblings. */\nexport function generateOkfPage(file: MdxFile, allFiles: MdxFile[], baseUrl: string): string {\n const markdown = renderer.render(file)\n return rewriteInternalLinks(markdown, allFiles, baseUrl)\n}\n\n/**\n * Rewrites internal links to point at sibling .md files in the OKF bundle.\n * e.g. [link](/blog/post) → [link](/okf/blog/post.md)\n */\nfunction rewriteInternalLinks(markdown: string, allFiles: MdxFile[], baseUrl: string): string {\n const slugSet = new Set(allFiles.map(f => f.slug))\n const base = baseUrl.replace(/\\/$/, '')\n\n return markdown.replace(/\\[([^\\]]+)\\]\\((\\/[^)]+)\\)/g, (match, text, href) => {\n // Strip leading slash and any trailing .md to get candidate slug\n const candidate = href.replace(/^\\//, '').replace(/\\.md$/, '')\n if (slugSet.has(candidate)) {\n return `[${text}](${base}/okf/${candidate}.md)`\n }\n return match\n })\n}\n"],"mappings":"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","names":["path","path"]}
|
|
@@ -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/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\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 = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; 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/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\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 = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; 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/** Generates /sitemap-ai.xml from MDX files. */\nexport function generateAiSitemap(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const urls = files.map(file => {\n const fm = file.frontmatter\n const loc = `${base}/${file.slug}`\n const lastmod = fm.date ? new Date(fm.date as string).toISOString().slice(0, 10) : ''\n const title = fm.title ? `\\n <title>${escapeXml(String(fm.title))}</title>` : ''\n const desc = fm.description ? `\\n <description>${escapeXml(String(fm.description))}</description>` : ''\n return [\n ' <url>',\n ` <loc>${escapeXml(loc)}</loc>`,\n lastmod ? ` <lastmod>${lastmod}</lastmod>` : '',\n ` <changefreq>weekly</changefreq>`,\n title,\n desc,\n ' </url>',\n ].filter(Boolean).join('\\n')\n })\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">',\n ...urls,\n '</urlset>',\n ].join('\\n') + '\\n'\n}\n\nfunction escapeXml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"')\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/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\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 = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; 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/sitemap-ai-route.ts","../../../src/core/mdx-reader.ts","../../../src/discovery/sitemap-ai.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server'\nimport path from 'path'\nimport { MdxReader } from '../../core/mdx-reader.js'\nimport { generateAiSitemap } from '../../discovery/sitemap-ai.js'\n\nconst reader = new MdxReader({ contentDir: path.join(process.cwd(), process.env.TA_CONTENT_DIR ?? 'content') })\n\n/** Handler for GET /sitemap-ai.xml → rewired to /api/third-audience/sitemap-ai */\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 = generateAiSitemap(files, baseUrl)\n\n return new NextResponse(content, {\n headers: { 'Content-Type': 'application/xml; 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/** Generates /sitemap-ai.xml from MDX files. */\nexport function generateAiSitemap(files: MdxFile[], baseUrl: string): string {\n const base = baseUrl.replace(/\\/$/, '')\n const urls = files.map(file => {\n const fm = file.frontmatter\n const loc = `${base}/${file.slug}`\n const lastmod = fm.date ? new Date(fm.date as string).toISOString().slice(0, 10) : ''\n const title = fm.title ? `\\n <title>${escapeXml(String(fm.title))}</title>` : ''\n const desc = fm.description ? `\\n <description>${escapeXml(String(fm.description))}</description>` : ''\n return [\n ' <url>',\n ` <loc>${escapeXml(loc)}</loc>`,\n lastmod ? ` <lastmod>${lastmod}</lastmod>` : '',\n ` <changefreq>weekly</changefreq>`,\n title,\n desc,\n ' </url>',\n ].filter(Boolean).join('\\n')\n })\n\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">',\n ...urls,\n '</urlset>',\n ].join('\\n') + '\\n'\n}\n\nfunction escapeXml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"')\n}\n"],"mappings":"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","names":["path","path"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface CardProps {
|
|
5
|
+
title: string;
|
|
6
|
+
action?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
declare function Card({ title, action, children }: CardProps): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { Card };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface CardProps {
|
|
5
|
+
title: string;
|
|
6
|
+
action?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
declare function Card({ title, action, children }: CardProps): react.JSX.Element;
|
|
10
|
+
|
|
11
|
+
export { Card };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/dashboard/ui/components/Card.tsx
|
|
21
|
+
var Card_exports = {};
|
|
22
|
+
__export(Card_exports, {
|
|
23
|
+
Card: () => Card
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(Card_exports);
|
|
26
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
27
|
+
function Card({ title, action, children }) {
|
|
28
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ta-card ta-section", children: [
|
|
29
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ta-card-header", children: [
|
|
30
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: title }),
|
|
31
|
+
action
|
|
32
|
+
] }),
|
|
33
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ta-card-body", children })
|
|
34
|
+
] });
|
|
35
|
+
}
|
|
36
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
37
|
+
0 && (module.exports = {
|
|
38
|
+
Card
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=Card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/ui/components/Card.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\n\ninterface CardProps {\n title: string\n action?: ReactNode\n children: ReactNode\n}\n\nexport function Card({ title, action, children }: CardProps) {\n return (\n <div className=\"ta-card ta-section\">\n <div className=\"ta-card-header\">\n <h2>{title}</h2>\n {action}\n </div>\n <div className=\"ta-card-body\">{children}</div>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWM;AAHC,SAAS,KAAK,EAAE,OAAO,QAAQ,SAAS,GAAc;AAC3D,SACE,6CAAC,SAAI,WAAU,sBACb;AAAA,iDAAC,SAAI,WAAU,kBACb;AAAA,kDAAC,QAAI,iBAAM;AAAA,MACV;AAAA,OACH;AAAA,IACA,4CAAC,SAAI,WAAU,gBAAgB,UAAS;AAAA,KAC1C;AAEJ;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/dashboard/ui/components/Card.tsx
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
function Card({ title, action, children }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", { className: "ta-card ta-section", children: [
|
|
5
|
+
/* @__PURE__ */ jsxs("div", { className: "ta-card-header", children: [
|
|
6
|
+
/* @__PURE__ */ jsx("h2", { children: title }),
|
|
7
|
+
action
|
|
8
|
+
] }),
|
|
9
|
+
/* @__PURE__ */ jsx("div", { className: "ta-card-body", children })
|
|
10
|
+
] });
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
Card
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=Card.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/dashboard/ui/components/Card.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\n\ninterface CardProps {\n title: string\n action?: ReactNode\n children: ReactNode\n}\n\nexport function Card({ title, action, children }: CardProps) {\n return (\n <div className=\"ta-card ta-section\">\n <div className=\"ta-card-header\">\n <h2>{title}</h2>\n {action}\n </div>\n <div className=\"ta-card-body\">{children}</div>\n </div>\n )\n}\n"],"mappings":";AAWM,SACE,KADF;AAHC,SAAS,KAAK,EAAE,OAAO,QAAQ,SAAS,GAAc;AAC3D,SACE,qBAAC,SAAI,WAAU,sBACb;AAAA,yBAAC,SAAI,WAAU,kBACb;AAAA,0BAAC,QAAI,iBAAM;AAAA,MACV;AAAA,OACH;AAAA,IACA,oBAAC,SAAI,WAAU,gBAAgB,UAAS;AAAA,KAC1C;AAEJ;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface HeroCardProps {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string | number;
|
|
7
|
+
meta?: string;
|
|
8
|
+
color?: 'blue' | 'green' | 'orange' | 'teal';
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
declare function HeroCard({ label, value, meta, color, icon }: HeroCardProps): react.JSX.Element;
|
|
12
|
+
|
|
13
|
+
export { HeroCard };
|