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; figcaption?: React.ReactNode;
}) { }) {
const [svg, setSvg] = useState<string | null>(null); 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); const figureRef = useRef<HTMLElement>(null);
// mermaid.render'ın istediği DOM id'si: useId'nin ":" karakterleri geçersiz // mermaid.render'ın istediği DOM id'si: useId'nin ":" karakterleri geçersiz
const semaId = `sema-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`; const semaId = `sema-${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
@@ -52,26 +55,34 @@ export function ElCizimiSema({
io.observe(el); io.observe(el);
function cizdir() { function cizdir() {
import("mermaid").then(async ({ default: mermaid }) => { import("mermaid")
mermaid.initialize({ .then(async ({ default: mermaid }) => {
startOnLoad: false, mermaid.initialize({
look: "handDrawn", startOnLoad: false,
handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik) // Hata olursa mermaid kendi "bomba" SVG'sini basmasın; biz toparlıyoruz
theme: "base", suppressErrorRendering: true,
fontFamily: `${elYazisi.style.fontFamily}, cursive`, look: "handDrawn",
themeVariables: { handDrawnSeed: 7, // her render'da aynı çizgi titremesi (deterministik)
primaryColor: "#ffffff", theme: "base",
primaryBorderColor: "#e2e8f0",
primaryTextColor: "#0f172a",
lineColor: "#94a3b8",
edgeLabelBackground: "#ffffff",
fontFamily: `${elYazisi.style.fontFamily}, cursive`, 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 () => { return () => {
@@ -89,7 +100,7 @@ export function ElCizimiSema({
className={`${elYazisi.className} overflow-x-auto [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full`} className={`${elYazisi.className} overflow-x-auto [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full`}
dangerouslySetInnerHTML={{ __html: svg }} dangerouslySetInnerHTML={{ __html: svg }}
/> />
) : ( ) : hata ? null : (
<div <div
aria-hidden aria-hidden
className="h-96 animate-pulse rounded-xl bg-slate-100" className="h-96 animate-pulse rounded-xl bg-slate-100"
@@ -100,6 +111,18 @@ export function ElCizimiSema({
{figcaption} {figcaption}
</figcaption> </figcaption>
) : null} ) : 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> </figure>
); );
} }