Rehbere mermaid şema desteği ve 4 yeni tercih dönemi yazısı ekle

Markdown'daki \`\`\`mermaid çitleri artık yazıyı parçalara bölüyor ve şemalar
üründeki ortak el çizimi bileşeniyle (ElCizimiSema) çiziliyor; çitte "%% aria:"
satırı zorunlu, "%% altyazi:" figcaption veriyor. Tercih dönemi aramalarına
dayanan dört yeni yazı: merkezi yerleştirme algoritması, OBP hesabı, üniversite
kayıt rehberi ve yerleşememe/istenmeyen bölüm karar ağacı — her biri risk renk
dilinde birer şemayla.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-02 04:25:58 +03:00
parent 716f7bb3fb
commit 6bd210075f
10 changed files with 1215 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import { ChevronRight } from "lucide-react";
import { getAllRehberSlugs, getAllRehberler, getRehber } from "@/lib/rehber";
import { JsonLd, articleJsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { ElCizimiSema } from "@/components/el-cizimi-sema";
import { RehberKapak } from "@/components/rehber-kapak";
import { SiteFooter } from "@/components/site-footer";
@@ -103,8 +104,23 @@ export default async function RehberYaziPage({
[&_th]:border [&_th]:border-slate-200 [&_th]:bg-slate-50 [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold
[&_td]:border [&_td]:border-slate-200 [&_td]:px-3 [&_td]:py-2
[&_blockquote]:mt-4 [&_blockquote]:border-l-4 [&_blockquote]:border-primary/30 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-slate-600"
dangerouslySetInnerHTML={{ __html: yazi.html }}
/>
>
{yazi.parcalar.map((parca, i) =>
parca.tip === "html" ? (
<div
key={i}
dangerouslySetInnerHTML={{ __html: parca.html }}
/>
) : (
<ElCizimiSema
key={i}
tanim={parca.sema.tanim}
ariaLabel={parca.sema.ariaLabel}
figcaption={parca.sema.altyazi}
/>
),
)}
</div>
</article>
<div className="mt-12">

View File

@@ -0,0 +1,85 @@
"use client";
// Ürün genelindeki "el çizimi" mermaid şemalarının tek kaynağı: /meraklisina
// hunisi de rehber yazılarındaki şemalar da bu bileşenle çizilir, böylece
// görünüm (font, renk teması, çizgi titremesi) her yerde birebir aynı kalır.
// Mermaid ağır bir paket olduğu için useEffect içinde dinamik import edilir;
// sayfa paketine girmez.
import { useEffect, useId, useState } from "react";
import localFont from "next/font/local";
// Şemanın "el çizimi" görünümüne uyan el yazısı fontu (Sweet Cucumber
// Mocktail, Misti's Fonts). Türkçe glifler doğrulandı (ş, ğ, ı, İ… hepsi
// var). DİKKAT: lisans "kişisel kullanım için ücretsiz" — canlıya çıkmadan
// mistifonts.com/sweet-cucumber-mocktail adresinden ticari lisans alınmalı.
const elYazisi = localFont({
src: "../fonts/SweetCucumberMocktail.woff2",
display: "swap",
});
export function ElCizimiSema({
tanim,
ariaLabel,
figcaption,
}: {
/** Mermaid tanımı (flowchart vb.) */
tanim: string;
/** Şemanın tamamını anlatan erişilebilirlik metni */
ariaLabel: string;
figcaption?: React.ReactNode;
}) {
const [svg, setSvg] = useState<string | null>(null);
// mermaid.render'ın istediği DOM id'si: useId'nin ":" karakterleri geçersiz
const semaId = `sema-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
useEffect(() => {
let iptal = false;
import("mermaid").then(async ({ default: mermaid }) => {
mermaid.initialize({
startOnLoad: false,
look: "handDrawn",
handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik)
theme: "base",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
themeVariables: {
primaryColor: "#ffffff",
primaryBorderColor: "#e2e8f0",
primaryTextColor: "#0f172a",
lineColor: "#94a3b8",
edgeLabelBackground: "#ffffff",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
fontSize: "17px", // script fontun x-yüksekliği düşük; okunurluk için büyütüldü
},
});
const sonuc = await mermaid.render(semaId, tanim);
if (!iptal) setSvg(sonuc.svg);
});
return () => {
iptal = true;
};
}, [semaId, tanim]);
return (
<figure className="mt-6">
{svg ? (
<div
role="img"
aria-label={ariaLabel}
className={`${elYazisi.className} overflow-x-auto [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full`}
dangerouslySetInnerHTML={{ __html: svg }}
/>
) : (
<div
aria-hidden
className="h-96 animate-pulse rounded-xl bg-slate-100"
/>
)}
{figcaption ? (
<figcaption className="mt-3 text-xs text-slate-500">
{figcaption}
</figcaption>
) : null}
</figure>
);
}

Binary file not shown.

View File

@@ -9,6 +9,11 @@ import { marked } from "marked";
*
* 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 = {
@@ -19,7 +24,18 @@ export type RehberOzet = {
tarih: string;
};
export type RehberYazi = RehberOzet & { html: 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");
@@ -43,6 +59,51 @@ function parseFrontmatter(raw: string): {
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;
@@ -57,7 +118,7 @@ function oku(slug: string): RehberYazi | null {
baslik: meta.baslik,
aciklama: meta.aciklama,
tarih: meta.tarih,
html: marked.parse(body, { gfm: true, async: false }),
parcalar: parcala(slug, body),
};
}