From a9ea0bf49b7f3ffcd8b5c88e34b9dda9ba0c011e Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:46:19 +0300 Subject: [PATCH 1/3] =?UTF-8?q?fix(rehber):=20frontmatter=20t=C4=B1rnak=20?= =?UTF-8?q?+=20tablo=20sarmalay=C4=B1c=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontmatter değerinin iki ucundaki tırnak bağımsız siliniyordu; "Alıntı" diye başlayan açıklamalar baştaki tırnağını kaybediyordu. Artık yalnızca tamamı aynı tırnakla sarılı değer soyulur. Makale tabloları yatay kayan, klavyeyle odaklanabilir bir bölgeye sarılır (mobilde sayfa taşması). Co-Authored-By: Claude Fable 5.1 --- .../rehber/components/rehber-makale.tsx | 2 +- src/lib/rehber.ts | 43 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/features/rehber/components/rehber-makale.tsx b/src/features/rehber/components/rehber-makale.tsx index 6177490..bd7612a 100644 --- a/src/features/rehber/components/rehber-makale.tsx +++ b/src/features/rehber/components/rehber-makale.tsx @@ -76,7 +76,7 @@ export function RehberMakale({ slug }: { slug: string }) { [&_strong]:font-semibold [&_strong]:text-slate-900 [&_ul]:mt-3 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ul]:pl-6 [&_ol]:mt-3 [&_ol]:list-decimal [&_ol]:space-y-1.5 [&_ol]:pl-6 - [&_table]:mt-4 [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm + [&_.rehber-tablo]:mt-4 [&_.rehber-tablo]:overflow-x-auto [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm [&_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" diff --git a/src/lib/rehber.ts b/src/lib/rehber.ts index de62431..7610d96 100644 --- a/src/lib/rehber.ts +++ b/src/lib/rehber.ts @@ -9,7 +9,7 @@ import { marked } from "marked"; * 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. + * bir avuç 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 @@ -57,6 +57,18 @@ const globalRehber = globalThis as unknown as { }; const CACHE_AKTIF = process.env.NODE_ENV === "production"; +// Değerin TAMAMI aynı tırnakla sarılıysa soyar. İki ucu bağımsız silmek +// `"Alıntı" diye başlayan cümle` gibi değerlerin yalnızca baştaki tırnağını +// yutuyordu; içinde aynı tırnak geçen değer de tam sarılı sayılmaz. +function tirnakSoy(deger: string): string { + if (deger.length < 2) return deger; + const tirnak = deger[0]; + if (tirnak !== '"' && tirnak !== "'") return deger; + if (deger[deger.length - 1] !== tirnak) return deger; + const ic = deger.slice(1, -1); + return ic.includes(tirnak) ? deger : ic; +} + function parseFrontmatter(raw: string): { meta: Record; body: string; @@ -68,10 +80,7 @@ function parseFrontmatter(raw: string): { const ayrac = satir.indexOf(":"); if (ayrac === -1) continue; const anahtar = satir.slice(0, ayrac).trim(); - const deger = satir - .slice(ayrac + 1) - .trim() - .replace(/^["']|["']$/g, ""); + const deger = tirnakSoy(satir.slice(ayrac + 1).trim()); if (anahtar) meta[anahtar] = deger; } return { meta, body: raw.slice(match[0].length) }; @@ -98,26 +107,34 @@ function parseSema(slug: string, ham: string): RehberSema { return { tanim: satirlar.join("\n").trim(), ariaLabel, altyazi }; } +// Geniş tablolar dar ekranda sayfayı taşırmasın: her tablo yatay kayan, +// klavyeyle odaklanabilir bir bölgeye sarılır (stil rehber-makale.tsx'te). +// marked ``'ı özniteliksiz basar; global `marked.use` ile renderer +// değiştirmek yerine çıktı burada sarılır. +function markdownHtml(md: string): string { + return marked + .parse(md, { gfm: true, async: false }) + .replaceAll( + "
", + '
', + ) + .replaceAll("
", ""); +} + 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: "html", html: markdownHtml(md) }); } 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 }), - }); + parcalar.push({ tip: "html", html: markdownHtml(kalan) }); } return parcalar; } From d9e125c5debfa1e89eba35b6e8bf1e6b4e199272 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:47:29 +0300 Subject: [PATCH 2/3] =?UTF-8?q?feat(rehber):=20guncelleme=20alan=C4=B1=20?= =?UTF-8?q?=E2=86=92=20sitemap/JSON-LD/OG/g=C3=B6r=C3=BCn=C3=BCr=20tarih?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontmatter'a isteğe bağlı `guncelleme` alanı geldi ve uçtan uca akıyor: sitemap lastmod (yazı + /rehber dizini), Article JSON-LD dateModified, og:article modified_time ve makale başlığının altındaki görünür tarih satırı. `tarih`/`guncelleme` YYYY-AA-GG değilse ya da güncelleme yayından önceyse build kırılır. Tarih metni saat diliminden bağımsız `trTarih` ile üretilir. Co-Authored-By: Claude Fable 5.1 --- src/app/sitemap.ts | 28 ++++++++++++--- .../rehber/components/rehber-makale.tsx | 17 ++++++++- src/lib/rehber.ts | 36 +++++++++++++++++-- src/lib/seo.tsx | 3 ++ src/lib/tarih.ts | 30 ++++++++++++++++ 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/lib/tarih.ts diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index c47be30..7864436 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -19,7 +19,6 @@ const staticEntries: MetadataRoute.Sitemap = [ priority: 0.8, changeFrequency: "weekly", }, - { url: `${SITE_URL}/rehber`, priority: 0.8, changeFrequency: "weekly" }, { url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" }, { url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" }, { url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" }, @@ -42,11 +41,32 @@ export default async function sitemap(): Promise { changeFrequency: "monthly", }), ); - const rehberler: MetadataRoute.Sitemap = getAllRehberler().map((y) => ({ + const yazilar = getAllRehberler(); + const rehberler: MetadataRoute.Sitemap = yazilar.map((y) => ({ url: `${SITE_URL}/rehber/${y.slug}`, - lastModified: y.tarih, + lastModified: y.guncelleme ?? y.tarih, priority: 0.7, changeFrequency: "monthly", })); - return [...staticEntries, ...bolumler, ...universiteler, ...rehberler]; + // /rehber dizini en son dokunulan yazı kadar tazedir (ISO tarihler metin + // olarak karşılaştırılabilir). Hiç yazı yoksa lastmod hiç yazılmaz. + const rehberSonDegisiklik = yazilar + .map((y) => y.guncelleme ?? y.tarih) + .reduce( + (enBuyuk, t) => (enBuyuk === undefined || t > enBuyuk ? t : enBuyuk), + undefined, + ); + const rehberDizini: MetadataRoute.Sitemap[number] = { + url: `${SITE_URL}/rehber`, + lastModified: rehberSonDegisiklik, + priority: 0.8, + changeFrequency: "weekly", + }; + return [ + ...staticEntries, + rehberDizini, + ...bolumler, + ...universiteler, + ...rehberler, + ]; } diff --git a/src/features/rehber/components/rehber-makale.tsx b/src/features/rehber/components/rehber-makale.tsx index bd7612a..4443ced 100644 --- a/src/features/rehber/components/rehber-makale.tsx +++ b/src/features/rehber/components/rehber-makale.tsx @@ -8,6 +8,7 @@ import { breadcrumbJsonLd, sayfaMetadata, } from "@/lib/seo"; +import { trTarih } from "@/lib/tarih"; import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form"; import { ElCizimiSema } from "./el-cizimi-sema"; import { RehberKapak } from "./rehber-kapak"; @@ -29,7 +30,11 @@ export function rehberYaziMetadata(slug: string): Metadata { title: yazi.baslik, description: yazi.aciklama, path: `/rehber/${slug}`, - openGraph: { type: "article", publishedTime: yazi.tarih }, + openGraph: { + type: "article", + publishedTime: yazi.tarih, + modifiedTime: yazi.guncelleme ?? yazi.tarih, + }, }); } @@ -49,6 +54,7 @@ export function RehberMakale({ slug }: { slug: string }) { description: yazi.aciklama, path: `/rehber/${slug}`, datePublished: yazi.tarih, + dateModified: yazi.guncelleme, })} /> {yazi.aciklama}

+

+ Yayın: + {yazi.guncelleme ? ( + <> + {" · "}Güncelleme:{" "} + + + ) : null} +

Date: Mon, 21 Sep 2026 04:48:10 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(rehber):=20=C5=9Fema=20metni=20SSR'da?= =?UTF-8?q?=20+=20mermaid=20hata=20toparlama?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ş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 `
` 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 --- .../rehber/components/el-cizimi-sema.tsx | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/features/rehber/components/el-cizimi-sema.tsx b/src/features/rehber/components/el-cizimi-sema.tsx index ad0955f..e373ea9 100644 --- a/src/features/rehber/components/el-cizimi-sema.tsx +++ b/src/features/rehber/components/el-cizimi-sema.tsx @@ -30,6 +30,9 @@ export function ElCizimiSema({ figcaption?: React.ReactNode; }) { const [svg, setSvg] = useState(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(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 : (
) : 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. */} +
+ + Şemayı metin olarak oku + +

{ariaLabel}

+
); }