fix(rehber): şema metni SSR'da + mermaid hata toparlama

Şemanın metin karşılığı yalnızca SVG çizilince `aria-label` olarak
basılıyordu; JS çalıştırmayan botlar iskeletten başka bir şey görmüyordu.
Artık aynı `ariaLabel` metni figure içinde bir `<details>` olarak sunucu
HTML'ine girer. mermaid import/render hatası yakalanır: iskelet kalkar,
metin açık gelir, mermaid'in body'de bıraktığı geçici div temizlenir.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 04:48:10 +03:00
parent d9e125c5de
commit 1a75f42f29

View File

@@ -30,6 +30,9 @@ export function ElCizimiSema({
figcaption?: React.ReactNode;
}) {
const [svg, setSvg] = useState<string | null>(null);
// mermaid yüklenemez ya da çizemezse (bazı tarayıcılarda `getBBox` hatası)
// sonsuz iskelet yerine metin karşılığı açık gelir
const [hata, setHata] = useState(false);
const figureRef = useRef<HTMLElement>(null);
// mermaid.render'ın istediği DOM id'si: useId'nin ":" karakterleri geçersiz
const semaId = `sema-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
@@ -52,26 +55,34 @@ export function ElCizimiSema({
io.observe(el);
function cizdir() {
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",
import("mermaid")
.then(async ({ default: mermaid }) => {
mermaid.initialize({
startOnLoad: false,
// Hata olursa mermaid kendi "bomba" SVG'sini basmasın; biz toparlıyoruz
suppressErrorRendering: true,
look: "handDrawn",
handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik)
theme: "base",
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
fontSize: "17px", // script fontun x-yüksekliği düşük; okunurluk için büyütüldü
},
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);
})
.catch(() => {
// mermaid.render atarsa geçici ölçüm div'ini body'de bırakabiliyor
document.getElementById(`d${semaId}`)?.remove();
if (!iptal) setHata(true);
});
const sonuc = await mermaid.render(semaId, tanim);
if (!iptal) setSvg(sonuc.svg);
});
}
return () => {
@@ -89,7 +100,7 @@ export function ElCizimiSema({
className={`${elYazisi.className} overflow-x-auto [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full`}
dangerouslySetInnerHTML={{ __html: svg }}
/>
) : (
) : hata ? null : (
<div
aria-hidden
className="h-96 animate-pulse rounded-xl bg-slate-100"
@@ -100,6 +111,18 @@ export function ElCizimiSema({
{figcaption}
</figcaption>
) : null}
{/* Şemanın metin karşılığı sunucu HTML'inde de durur: JS çalıştırmayan
botlar ve şemayı çizemeyen tarayıcılar aynı içeriği okur. Kaynak
yine tek `ariaLabel` prop'u — ikinci bir metin tutulmaz. */}
<details
open={hata || undefined}
className="mt-2 text-xs text-slate-500"
>
<summary className="cursor-pointer font-medium text-slate-600">
Şemayı metin olarak oku
</summary>
<p className="mt-2 text-sm leading-6 text-slate-600">{ariaLabel}</p>
</details>
</figure>
);
}