merge: origin/gece/p1-rehber-altyapi (gece vardiyası 2026-09-21)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,6 @@ const staticEntries: MetadataRoute.Sitemap = [
|
|||||||
priority: 0.8,
|
priority: 0.8,
|
||||||
changeFrequency: "weekly",
|
changeFrequency: "weekly",
|
||||||
},
|
},
|
||||||
{ url: `${SITE_URL}/rehber`, priority: 0.8, changeFrequency: "weekly" },
|
|
||||||
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
|
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
|
||||||
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
|
||||||
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
|
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
|
||||||
@@ -42,11 +41,32 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
changeFrequency: "monthly",
|
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}`,
|
url: `${SITE_URL}/rehber/${y.slug}`,
|
||||||
lastModified: y.tarih,
|
lastModified: y.guncelleme ?? y.tarih,
|
||||||
priority: 0.7,
|
priority: 0.7,
|
||||||
changeFrequency: "monthly",
|
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<string | undefined>(
|
||||||
|
(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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
breadcrumbJsonLd,
|
breadcrumbJsonLd,
|
||||||
sayfaMetadata,
|
sayfaMetadata,
|
||||||
} from "@/lib/seo";
|
} from "@/lib/seo";
|
||||||
|
import { trTarih } from "@/lib/tarih";
|
||||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||||
import { ElCizimiSema } from "./el-cizimi-sema";
|
import { ElCizimiSema } from "./el-cizimi-sema";
|
||||||
import { RehberKapak } from "./rehber-kapak";
|
import { RehberKapak } from "./rehber-kapak";
|
||||||
@@ -29,7 +30,11 @@ export function rehberYaziMetadata(slug: string): Metadata {
|
|||||||
title: yazi.baslik,
|
title: yazi.baslik,
|
||||||
description: yazi.aciklama,
|
description: yazi.aciklama,
|
||||||
path: `/rehber/${slug}`,
|
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,
|
description: yazi.aciklama,
|
||||||
path: `/rehber/${slug}`,
|
path: `/rehber/${slug}`,
|
||||||
datePublished: yazi.tarih,
|
datePublished: yazi.tarih,
|
||||||
|
dateModified: yazi.guncelleme,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<JsonLd
|
<JsonLd
|
||||||
@@ -67,6 +73,15 @@ export function RehberMakale({ slug }: { slug: string }) {
|
|||||||
<p className="mt-3 text-base leading-7 text-slate-600">
|
<p className="mt-3 text-base leading-7 text-slate-600">
|
||||||
{yazi.aciklama}
|
{yazi.aciklama}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-3 text-xs text-slate-500">
|
||||||
|
Yayın: <time dateTime={yazi.tarih}>{trTarih(yazi.tarih)}</time>
|
||||||
|
{yazi.guncelleme ? (
|
||||||
|
<>
|
||||||
|
{" · "}Güncelleme:{" "}
|
||||||
|
<time dateTime={yazi.guncelleme}>{trTarih(yazi.guncelleme)}</time>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="mt-8 text-[15px] leading-7 text-slate-700
|
className="mt-8 text-[15px] leading-7 text-slate-700
|
||||||
[&_h2]:mt-10 [&_h2]:font-heading [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-slate-900
|
[&_h2]:mt-10 [&_h2]:font-heading [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-slate-900
|
||||||
@@ -76,7 +91,7 @@ export function RehberMakale({ slug }: { slug: string }) {
|
|||||||
[&_strong]:font-semibold [&_strong]:text-slate-900
|
[&_strong]:font-semibold [&_strong]:text-slate-900
|
||||||
[&_ul]:mt-3 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ul]:pl-6
|
[&_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
|
[&_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
|
[&_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
|
[&_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"
|
[&_blockquote]:mt-4 [&_blockquote]:border-l-4 [&_blockquote]:border-primary/30 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-slate-600"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { marked } from "marked";
|
|||||||
* ile tamamen prerender edilir; runtime'da fs erişimi olmaz).
|
* ile tamamen prerender edilir; runtime'da fs erişimi olmaz).
|
||||||
*
|
*
|
||||||
* Frontmatter elle parse edilir (yalnızca düz `anahtar: değer` satırları) —
|
* 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
|
* ```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
|
* sayfada ürünün ortak el çizimi bileşeniyle (ElCizimiSema) çizilir. Çitin
|
||||||
@@ -23,6 +23,8 @@ export type RehberOzet = {
|
|||||||
aciklama: string;
|
aciklama: string;
|
||||||
/** ISO tarih — Article JSON-LD datePublished */
|
/** ISO tarih — Article JSON-LD datePublished */
|
||||||
tarih: string;
|
tarih: string;
|
||||||
|
/** ISO tarih, isteğe bağlı — dateModified / sitemap lastmod / görünür satır */
|
||||||
|
guncelleme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +59,18 @@ const globalRehber = globalThis as unknown as {
|
|||||||
};
|
};
|
||||||
const CACHE_AKTIF = process.env.NODE_ENV === "production";
|
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): {
|
function parseFrontmatter(raw: string): {
|
||||||
meta: Record<string, string>;
|
meta: Record<string, string>;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -68,10 +82,7 @@ function parseFrontmatter(raw: string): {
|
|||||||
const ayrac = satir.indexOf(":");
|
const ayrac = satir.indexOf(":");
|
||||||
if (ayrac === -1) continue;
|
if (ayrac === -1) continue;
|
||||||
const anahtar = satir.slice(0, ayrac).trim();
|
const anahtar = satir.slice(0, ayrac).trim();
|
||||||
const deger = satir
|
const deger = tirnakSoy(satir.slice(ayrac + 1).trim());
|
||||||
.slice(ayrac + 1)
|
|
||||||
.trim()
|
|
||||||
.replace(/^["']|["']$/g, "");
|
|
||||||
if (anahtar) meta[anahtar] = deger;
|
if (anahtar) meta[anahtar] = deger;
|
||||||
}
|
}
|
||||||
return { meta, body: raw.slice(match[0].length) };
|
return { meta, body: raw.slice(match[0].length) };
|
||||||
@@ -98,39 +109,73 @@ function parseSema(slug: string, ham: string): RehberSema {
|
|||||||
return { tanim: satirlar.join("\n").trim(), ariaLabel, altyazi };
|
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 `<table>`'ı ö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(
|
||||||
|
"<table>",
|
||||||
|
'<div class="rehber-tablo" role="region" aria-label="Tablo" tabindex="0"><table>',
|
||||||
|
)
|
||||||
|
.replaceAll("</table>", "</table></div>");
|
||||||
|
}
|
||||||
|
|
||||||
function parcala(slug: string, body: string): RehberParca[] {
|
function parcala(slug: string, body: string): RehberParca[] {
|
||||||
const parcalar: RehberParca[] = [];
|
const parcalar: RehberParca[] = [];
|
||||||
let son = 0;
|
let son = 0;
|
||||||
for (const eslesme of body.matchAll(SEMA_CITI)) {
|
for (const eslesme of body.matchAll(SEMA_CITI)) {
|
||||||
const md = body.slice(son, eslesme.index).trim();
|
const md = body.slice(son, eslesme.index).trim();
|
||||||
if (md) {
|
if (md) {
|
||||||
parcalar.push({
|
parcalar.push({ tip: "html", html: markdownHtml(md) });
|
||||||
tip: "html",
|
|
||||||
html: marked.parse(md, { gfm: true, async: false }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
parcalar.push({ tip: "sema", sema: parseSema(slug, eslesme[1]) });
|
parcalar.push({ tip: "sema", sema: parseSema(slug, eslesme[1]) });
|
||||||
son = eslesme.index + eslesme[0].length;
|
son = eslesme.index + eslesme[0].length;
|
||||||
}
|
}
|
||||||
const kalan = body.slice(son).trim();
|
const kalan = body.slice(son).trim();
|
||||||
if (kalan) {
|
if (kalan) {
|
||||||
parcalar.push({
|
parcalar.push({ tip: "html", html: markdownHtml(kalan) });
|
||||||
tip: "html",
|
|
||||||
html: marked.parse(kalan, { gfm: true, async: false }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return parcalar;
|
return parcalar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TARIH_BICIMI = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
function metaDogrula(
|
function metaDogrula(
|
||||||
slug: string,
|
slug: string,
|
||||||
meta: Record<string, string>,
|
meta: Record<string, string>,
|
||||||
): asserts meta is { baslik: string; aciklama: string; tarih: string } {
|
): asserts meta is {
|
||||||
|
baslik: string;
|
||||||
|
aciklama: string;
|
||||||
|
tarih: string;
|
||||||
|
guncelleme?: string;
|
||||||
|
} {
|
||||||
if (!meta.baslik || !meta.aciklama || !meta.tarih) {
|
if (!meta.baslik || !meta.aciklama || !meta.tarih) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`,
|
`rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Tarihler sitemap lastmod'a, JSON-LD'ye ve görünür satıra olduğu gibi
|
||||||
|
// akıyor; bozuk biçim sessizce yayına çıkmasın, build kırılsın.
|
||||||
|
if (!TARIH_BICIMI.test(meta.tarih)) {
|
||||||
|
throw new Error(
|
||||||
|
`rehber: ${slug}.md frontmatter "tarih" YYYY-AA-GG biçiminde olmalı`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (meta.guncelleme !== undefined) {
|
||||||
|
if (!TARIH_BICIMI.test(meta.guncelleme)) {
|
||||||
|
throw new Error(
|
||||||
|
`rehber: ${slug}.md frontmatter "guncelleme" YYYY-AA-GG biçiminde olmalı`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (meta.guncelleme < meta.tarih) {
|
||||||
|
throw new Error(
|
||||||
|
`rehber: ${slug}.md frontmatter "guncelleme" "tarih"ten önce olamaz`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function oku(slug: string): RehberYazi | null {
|
function oku(slug: string): RehberYazi | null {
|
||||||
@@ -148,6 +193,7 @@ function oku(slug: string): RehberYazi | null {
|
|||||||
baslik: meta.baslik,
|
baslik: meta.baslik,
|
||||||
aciklama: meta.aciklama,
|
aciklama: meta.aciklama,
|
||||||
tarih: meta.tarih,
|
tarih: meta.tarih,
|
||||||
|
guncelleme: meta.guncelleme,
|
||||||
parcalar: parcala(slug, body),
|
parcalar: parcala(slug, body),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -166,8 +212,8 @@ function ozetOku(slug: string): RehberOzet | null {
|
|||||||
const tam = CACHE_AKTIF ? globalRehber.rehberYazilar?.get(slug) : undefined;
|
const tam = CACHE_AKTIF ? globalRehber.rehberYazilar?.get(slug) : undefined;
|
||||||
if (tam !== undefined) {
|
if (tam !== undefined) {
|
||||||
if (tam === null) return null;
|
if (tam === null) return null;
|
||||||
const { slug: s, baslik, aciklama, tarih } = tam;
|
const { slug: s, baslik, aciklama, tarih, guncelleme } = tam;
|
||||||
return { slug: s, baslik, aciklama, tarih };
|
return { slug: s, baslik, aciklama, tarih, guncelleme };
|
||||||
}
|
}
|
||||||
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
|
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
|
||||||
if (!fs.existsSync(dosya)) return null;
|
if (!fs.existsSync(dosya)) return null;
|
||||||
@@ -178,6 +224,7 @@ function ozetOku(slug: string): RehberOzet | null {
|
|||||||
baslik: meta.baslik,
|
baslik: meta.baslik,
|
||||||
aciklama: meta.aciklama,
|
aciklama: meta.aciklama,
|
||||||
tarih: meta.tarih,
|
tarih: meta.tarih,
|
||||||
|
guncelleme: meta.guncelleme,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export function articleJsonLd(opts: {
|
|||||||
description: string;
|
description: string;
|
||||||
path: string;
|
path: string;
|
||||||
datePublished: string;
|
datePublished: string;
|
||||||
|
/** Verilmezse yayın tarihi kullanılır (yazı hiç güncellenmemiş). */
|
||||||
|
dateModified?: string;
|
||||||
}): JsonLdData {
|
}): JsonLdData {
|
||||||
return {
|
return {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -159,6 +161,7 @@ export function articleJsonLd(opts: {
|
|||||||
description: opts.description,
|
description: opts.description,
|
||||||
url: `${SITE_URL}${opts.path}`,
|
url: `${SITE_URL}${opts.path}`,
|
||||||
datePublished: opts.datePublished,
|
datePublished: opts.datePublished,
|
||||||
|
dateModified: opts.dateModified ?? opts.datePublished,
|
||||||
inLanguage: "tr",
|
inLanguage: "tr",
|
||||||
publisher: {
|
publisher: {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
|
|||||||
30
src/lib/tarih.ts
Normal file
30
src/lib/tarih.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* `YYYY-MM-DD` tarihini Türkçe uzun biçime çevirir ("21 Eylül 2026").
|
||||||
|
* Bilinçli olarak `Date`/`toLocale*` kullanmaz: sunucu ile tarayıcının saat
|
||||||
|
* dilimi ya da ICU verisi farklıysa aynı girdi farklı metin üretir
|
||||||
|
* (hydration uyuşmazlığı, bir gün kayması). Saf metin dönüşümü her yerde
|
||||||
|
* aynı sonucu verir.
|
||||||
|
*/
|
||||||
|
const AYLAR = [
|
||||||
|
"Ocak",
|
||||||
|
"Şubat",
|
||||||
|
"Mart",
|
||||||
|
"Nisan",
|
||||||
|
"Mayıs",
|
||||||
|
"Haziran",
|
||||||
|
"Temmuz",
|
||||||
|
"Ağustos",
|
||||||
|
"Eylül",
|
||||||
|
"Ekim",
|
||||||
|
"Kasım",
|
||||||
|
"Aralık",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function trTarih(iso: string): string {
|
||||||
|
const eslesme = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
const ay = eslesme ? AYLAR[Number(eslesme[2]) - 1] : undefined;
|
||||||
|
if (!eslesme || !ay) {
|
||||||
|
throw new Error(`tarih: "${iso}" YYYY-AA-GG biçiminde değil`);
|
||||||
|
}
|
||||||
|
return `${Number(eslesme[3])} ${ay} ${eslesme[1]}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user