feat(rehber): guncelleme alanı → sitemap/JSON-LD/OG/görünür tarih

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 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 04:47:29 +03:00
parent a9ea0bf49b
commit d9e125c5de
5 changed files with 106 additions and 8 deletions

View File

@@ -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,
];
} }

View File

@@ -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

View File

@@ -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;
}; };
/** /**
@@ -139,15 +141,41 @@ function parcala(slug: string, body: string): RehberParca[] {
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 {
@@ -165,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),
}; };
} }
@@ -183,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;
@@ -195,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,
}; };
} }

View File

@@ -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
View 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]}`;
}