import fs from "node:fs"; import path from "node:path"; import { marked } from "marked"; /** * Rehber makaleleri: content/rehber/*.md dosyaları build sırasında okunur ve * statik HTML'e gömülür (sayfalar generateStaticParams + dynamicParams=false * ile tamamen prerender edilir; runtime'da fs erişimi olmaz). * * Frontmatter elle parse edilir (yalnızca düz `anahtar: değer` satırları) — * 10 makale için gray-matter bağımlılığına gerek yok. * * ```mermaid çitleri HTML'e gömülmez: makale parçalara bölünür ve şemalar * sayfada ürünün ortak el çizimi bileşeniyle (ElCizimiSema) çizilir. Çitin * içinde `%% aria:` satırı zorunludur (erişilebilirlik metni), `%% altyazi:` * isteğe bağlı figcaption verir; iki satır da mermaid tanımından çıkarılır. */ export type RehberOzet = { slug: string; baslik: string; aciklama: string; /** ISO tarih — Article JSON-LD datePublished */ tarih: string; }; export type RehberSema = { /** Mermaid tanımı (aria/altyazi yönergeleri ayıklanmış hâli) */ tanim: string; ariaLabel: string; altyazi?: string; }; export type RehberParca = | { tip: "html"; html: string } | { tip: "sema"; sema: RehberSema }; export type RehberYazi = RehberOzet & { parcalar: RehberParca[] }; const REHBER_DIZIN = path.join(process.cwd(), "content", "rehber"); function parseFrontmatter(raw: string): { meta: Record; body: string; } { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); if (!match) return { meta: {}, body: raw }; const meta: Record = {}; for (const satir of match[1].split(/\r?\n/)) { const ayrac = satir.indexOf(":"); if (ayrac === -1) continue; const anahtar = satir.slice(0, ayrac).trim(); const deger = satir .slice(ayrac + 1) .trim() .replace(/^["']|["']$/g, ""); if (anahtar) meta[anahtar] = deger; } return { meta, body: raw.slice(match[0].length) }; } const SEMA_CITI = /^```mermaid[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gm; function parseSema(slug: string, ham: string): RehberSema { let ariaLabel = ""; let altyazi: string | undefined; const satirlar: string[] = []; for (const satir of ham.split(/\r?\n/)) { const aria = satir.match(/^%%\s*aria:\s*(.+)$/); const alt = satir.match(/^%%\s*altyazi:\s*(.+)$/); if (aria) ariaLabel = aria[1].trim(); else if (alt) altyazi = alt[1].trim(); else satirlar.push(satir); } if (!ariaLabel) { throw new Error( `rehber: ${slug}.md içindeki mermaid şemasında "%% aria: ..." satırı zorunlu`, ); } return { tanim: satirlar.join("\n").trim(), ariaLabel, altyazi }; } function parcala(slug: string, body: string): RehberParca[] { const parcalar: RehberParca[] = []; let son = 0; for (const eslesme of body.matchAll(SEMA_CITI)) { const md = body.slice(son, eslesme.index).trim(); if (md) { parcalar.push({ tip: "html", html: marked.parse(md, { gfm: true, async: false }), }); } parcalar.push({ tip: "sema", sema: parseSema(slug, eslesme[1]) }); son = eslesme.index + eslesme[0].length; } const kalan = body.slice(son).trim(); if (kalan) { parcalar.push({ tip: "html", html: marked.parse(kalan, { gfm: true, async: false }), }); } return parcalar; } function oku(slug: string): RehberYazi | null { const dosya = path.join(REHBER_DIZIN, `${slug}.md`); if (!fs.existsSync(dosya)) return null; const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8")); if (!meta.baslik || !meta.aciklama || !meta.tarih) { throw new Error( `rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`, ); } return { slug, baslik: meta.baslik, aciklama: meta.aciklama, tarih: meta.tarih, parcalar: parcala(slug, body), }; } export function getAllRehberSlugs(): string[] { if (!fs.existsSync(REHBER_DIZIN)) return []; return fs .readdirSync(REHBER_DIZIN) .filter((f) => f.endsWith(".md")) .map((f) => f.replace(/\.md$/, "")) .sort(); } export function getRehber(slug: string): RehberYazi | null { // path traversal koruması: slug yalnızca dosya adı olabilir if (!/^[a-z0-9-]+$/.test(slug)) return null; return oku(slug); } export function getAllRehberler(): RehberOzet[] { return getAllRehberSlugs() .map((slug) => oku(slug)) .filter((y): y is RehberYazi => y !== null) .map(({ slug, baslik, aciklama, tarih }) => ({ slug, baslik, aciklama, tarih, })) .sort((a, b) => b.tarih.localeCompare(a.tarih)); }