feat(seo): GEO paketi — yapılandırılmış metadata, AI bot erişimi, veri tazelik sinyali
- sayfaMetadata(): indekslenebilir sayfalar için tek kaynaklı canonical + eksiksiz openGraph üretimi; sayfalar buna geçirildi - robots.ts: yalnızca /api/ engellenir; noindex'li sayfalar robots engeli yerine meta ile yönetilir, AI botları niyeti belgeleyen açık grupla izinli - next.config: htmlLimitedBots'a AI botları eklendi (JS çalıştırmayan botlara metadata head içinde bloklanarak verilir) - sitemap lastmod + katalogda görünür 'Kaynak/son güncelleme' satırı (VeriKaynagi) KATALOG_VERI_TARIHI tek kaynağından - docs/geo/geo-raporu.md: çalışmanın raporu Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
|
||||
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
BolumKatalogu,
|
||||
BolumKataloguSkeleton,
|
||||
} from "@/features/katalog/components/bolum-listesi";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026",
|
||||
description:
|
||||
"Üniversite bölümlerinin 2025 yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, 2026 tercihleri için.",
|
||||
alternates: { canonical: "/bolumler" },
|
||||
};
|
||||
path: "/bolumler",
|
||||
});
|
||||
|
||||
export default function BolumlerPage() {
|
||||
return (
|
||||
|
||||
@@ -3,13 +3,14 @@ import Link from "next/link";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Gizlilik ve KVKK",
|
||||
description:
|
||||
"KolayTercih gizlilik politikası ve KVKK aydınlatma metni: hangi veriler toplanır, nasıl işlenir ve haklarınız nelerdir.",
|
||||
alternates: { canonical: "/gizlilik" },
|
||||
};
|
||||
path: "/gizlilik",
|
||||
});
|
||||
|
||||
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
||||
export default function GizlilikPage() {
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Mail } from "lucide-react";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "İletişim",
|
||||
description:
|
||||
"KolayTercih destek ekibine ulaş: sorular, geri bildirim ve iade talepleri için iletişim kanalları.",
|
||||
alternates: { canonical: "/iletisim" },
|
||||
};
|
||||
path: "/iletisim",
|
||||
});
|
||||
|
||||
export default function IletisimPage() {
|
||||
return (
|
||||
|
||||
@@ -3,13 +3,14 @@ import Link from "next/link";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Kullanım ve Satış Koşulları",
|
||||
description:
|
||||
"KolayTercih kullanım ve satış koşulları: hizmet kapsamı, ödeme, iade politikası ve sorumluluk sınırları.",
|
||||
alternates: { canonical: "/kosullar" },
|
||||
};
|
||||
path: "/kosullar",
|
||||
});
|
||||
|
||||
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
||||
export default function KosullarPage() {
|
||||
|
||||
@@ -7,13 +7,14 @@ import {
|
||||
MeraklisinaIcerik,
|
||||
MeraklisinaIcerikSkeleton,
|
||||
} from "@/features/pazarlama/components/meraklisina-icerik";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Meraklısına: KolayTercih nasıl çalışır?",
|
||||
alternates: { canonical: "/meraklisina" },
|
||||
path: "/meraklisina",
|
||||
description:
|
||||
"Sıralamanı girdiğinde arka planda ne oluyor? Dilimler, risk renkleri ve sihirbazın adım adım daralan filtreleri — öğrenci diliyle, sıralamana göre hesaplanan canlı görsellerle.",
|
||||
};
|
||||
});
|
||||
|
||||
export default function MeraklisinaPage() {
|
||||
return (
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
PaketSatinAl,
|
||||
PaketSatinAlSkeleton,
|
||||
} from "@/features/odeme/components/paket-satinal";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Tercih Dönemi Paketi",
|
||||
description:
|
||||
"Tek seferlik Tercih Dönemi Paketi: yapay zekâ destekli kişisel 24 tercihlik liste, risk analizi, liste revizyonları ve soru hakkı. Abonelik yok.",
|
||||
alternates: { canonical: "/paket" },
|
||||
};
|
||||
path: "/paket",
|
||||
});
|
||||
|
||||
export default function PaketPage({
|
||||
searchParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
|
||||
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
RehberListesiSkeleton,
|
||||
} from "@/features/rehber/components/rehber-listesi";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Tercih Rehberi 2026 — YKS Tercih Dönemi Yazıları",
|
||||
description:
|
||||
"YKS tercih dönemi rehberi: tercih listesi nasıl yapılır, ölü tercih nedir, ek yerleştirme nasıl işler, sıralama mı puan mı — hepsi öğrenci diliyle.",
|
||||
alternates: { canonical: "/rehber" },
|
||||
};
|
||||
path: "/rehber",
|
||||
});
|
||||
|
||||
export default function RehberIndexPage() {
|
||||
return (
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { SITE_URL } from "@/lib/seo";
|
||||
|
||||
// Yalnızca /api/ engellenir. /sonuc, /giris, /listem, /odeme/*, /rapor/*
|
||||
// bilinçli olarak ENGELLENMEDİ: bu sayfalar noindex meta taşıyor ve robots.txt
|
||||
// engeli o meta'nın görülmesini imkânsız kılardı (dış link alırlarsa
|
||||
// "robots.txt tarafından engellendi ama dizine eklendi" durumuna düşerlerdi).
|
||||
const DISALLOW = ["/api/"];
|
||||
|
||||
// Arama/cevap motorlarının tarayıcıları. `*` kuralı bunları zaten kapsıyor;
|
||||
// grup, niyeti belgelemek için açıkça yazıldı: AI aramalarında kaynak
|
||||
// gösterilmek istiyoruz. DİKKAT: bir bot kendi adına yazılmış grubu bulursa `*`
|
||||
// grubunu yok sayar — buraya eklenen her bot için DISALLOW tekrar edilmeli.
|
||||
const AI_BOTLARI = [
|
||||
// Arama indeksi + kullanıcı tetiklemeli fetch
|
||||
"OAI-SearchBot",
|
||||
"ChatGPT-User",
|
||||
"Claude-SearchBot",
|
||||
"Claude-User",
|
||||
"PerplexityBot",
|
||||
"Perplexity-User",
|
||||
"DuckAssistBot",
|
||||
"MistralAI-User",
|
||||
"meta-webindexer",
|
||||
"Amzn-SearchBot",
|
||||
// Model eğitimi: aramasız yanıtlarda markanın bilinmesi buradan geçer
|
||||
"GPTBot",
|
||||
"ClaudeBot",
|
||||
"Google-Extended",
|
||||
"Applebot-Extended",
|
||||
"meta-externalagent",
|
||||
"Amazonbot",
|
||||
"CCBot",
|
||||
];
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
// /sonuc bilinçli olarak ENGELLENMEDİ: sayfa noindex meta taşıyor ve
|
||||
// robots.txt engeli o meta'nın görülmesini imkânsız kılardı.
|
||||
disallow: ["/api/", "/listem", "/odeme/", "/rapor/", "/giris"],
|
||||
},
|
||||
rules: [
|
||||
{ userAgent: "*", allow: "/", disallow: DISALLOW },
|
||||
{ userAgent: AI_BOTLARI, allow: "/", disallow: DISALLOW },
|
||||
],
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { SITE_URL } from "@/lib/seo";
|
||||
import { KATALOG_VERI_TARIHI, SITE_URL } from "@/lib/seo";
|
||||
import { getAllBolumSlugs, getAllUniversiteSlugs } from "@/lib/katalog";
|
||||
import { getAllRehberler } from "@/lib/rehber";
|
||||
|
||||
@@ -7,8 +7,18 @@ import { getAllRehberler } from "@/lib/rehber";
|
||||
// /odeme/sonuc, /rapor/yazdir (auth/utility sayfaları).
|
||||
const staticEntries: MetadataRoute.Sitemap = [
|
||||
{ url: `${SITE_URL}/`, priority: 1, changeFrequency: "weekly" },
|
||||
{ url: `${SITE_URL}/bolumler`, priority: 0.8, changeFrequency: "weekly" },
|
||||
{ url: `${SITE_URL}/universiteler`, priority: 0.8, changeFrequency: "weekly" },
|
||||
{
|
||||
url: `${SITE_URL}/bolumler`,
|
||||
lastModified: KATALOG_VERI_TARIHI,
|
||||
priority: 0.8,
|
||||
changeFrequency: "weekly",
|
||||
},
|
||||
{
|
||||
url: `${SITE_URL}/universiteler`,
|
||||
lastModified: KATALOG_VERI_TARIHI,
|
||||
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" },
|
||||
@@ -20,12 +30,14 @@ const staticEntries: MetadataRoute.Sitemap = [
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const bolumler: MetadataRoute.Sitemap = getAllBolumSlugs().map((slug) => ({
|
||||
url: `${SITE_URL}/bolum/${slug}`,
|
||||
lastModified: KATALOG_VERI_TARIHI,
|
||||
priority: 0.7,
|
||||
changeFrequency: "monthly",
|
||||
}));
|
||||
const universiteler: MetadataRoute.Sitemap = getAllUniversiteSlugs().map(
|
||||
(slug) => ({
|
||||
url: `${SITE_URL}/universite/${slug}`,
|
||||
lastModified: KATALOG_VERI_TARIHI,
|
||||
priority: 0.6,
|
||||
changeFrequency: "monthly",
|
||||
}),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
|
||||
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
UniversiteKatalogu,
|
||||
UniversiteKataloguSkeleton,
|
||||
} from "@/features/katalog/components/universite-listesi";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
title: "Üniversitelerin Taban Puanları ve Sıralamaları 2026",
|
||||
description:
|
||||
"Devlet ve vakıf üniversitelerinin 2025 yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, 2026 tercihleri için.",
|
||||
alternates: { canonical: "/universiteler" },
|
||||
};
|
||||
path: "/universiteler",
|
||||
});
|
||||
|
||||
export default function UniversitelerPage() {
|
||||
return (
|
||||
|
||||
@@ -60,19 +60,31 @@ function AccordionContent({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
// forceMount: kapalı içerik de ilk HTML'de kalır — JS çalıştırmayan
|
||||
// tarayıcılar (arama/AI botları) cevapları okuyabilsin. Radix kapalıyken
|
||||
// içeriği hiç render etmez; forceMount ile de yükseklik ölçümü kapalıyken 0
|
||||
// çıktığı için accordion-up/down keyframe'leri çalışmaz. Aç/kapa bu yüzden
|
||||
// grid-rows geçişiyle yapılır; geçiş Content'in KENDİSİNDE değil çocuğunda
|
||||
// durur, çünkü Radix ölçüm sırasında Content'in transition-duration'ını
|
||||
// sıfırlar. visibility geçişi kapalı içeriği odak/ekran okuyucudan çıkarır.
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
forceMount
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||
className="group/accordion-content text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div className="grid grid-rows-[1fr] transition-[grid-template-rows,visibility] duration-200 ease-out group-data-[state=closed]/accordion-content:invisible group-data-[state=closed]/accordion-content:grid-rows-[0fr] motion-reduce:transition-none">
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
|
||||
@@ -8,9 +8,15 @@ import {
|
||||
getBolumBySlug,
|
||||
uniSlugFromAd,
|
||||
} from "../katalog-queries";
|
||||
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
|
||||
import {
|
||||
JsonLd,
|
||||
breadcrumbJsonLd,
|
||||
sayfaMetadata,
|
||||
ustSegmentOgGorseli,
|
||||
} from "@/lib/seo";
|
||||
import { SAYFA_BOYU } from "./sayfalama";
|
||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
@@ -41,21 +47,27 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||
if (!bolum) return {};
|
||||
const { stats } = bolum;
|
||||
if (sayfa > 1) {
|
||||
return {
|
||||
return sayfaMetadata({
|
||||
title: `${bolum.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
||||
description: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
|
||||
alternates: { canonical: `/bolum/${slug}/sayfa/${sayfa}` },
|
||||
};
|
||||
path: `/bolum/${slug}/sayfa/${sayfa}`,
|
||||
openGraph: {
|
||||
images: ustSegmentOgGorseli(
|
||||
`/bolum/${slug}`,
|
||||
"Bölüm taban puanları ve sıralamaları — KolayTercih",
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
const enIyi =
|
||||
stats.minSira != null
|
||||
? ` En iyi taban sıralaması ${sayi(stats.minSira)}.`
|
||||
: "";
|
||||
return {
|
||||
return sayfaMetadata({
|
||||
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
|
||||
description: `${bolum.ad} 2025 yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} 2026 tercih rehberi.`,
|
||||
alternates: { canonical: `/bolum/${slug}` },
|
||||
};
|
||||
path: `/bolum/${slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
@@ -139,6 +151,7 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
tabanYol={`/bolum/${slug}`}
|
||||
sayfa={sayfa}
|
||||
/>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
bolumSlugFromIsim,
|
||||
} from "../katalog-queries";
|
||||
import { trBaslikDuzeni } from "@/lib/slug";
|
||||
import { JsonLd, breadcrumbJsonLd, SITE_URL } from "@/lib/seo";
|
||||
import {
|
||||
JsonLd,
|
||||
breadcrumbJsonLd,
|
||||
sayfaMetadata,
|
||||
SITE_URL,
|
||||
ustSegmentOgGorseli,
|
||||
} from "@/lib/seo";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
||||
@@ -17,6 +23,7 @@ import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -48,17 +55,23 @@ export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||
const uni = getUniversiteBySlug(slug);
|
||||
if (!uni) return {};
|
||||
if (sayfa > 1) {
|
||||
return {
|
||||
return sayfaMetadata({
|
||||
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
||||
description: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
|
||||
alternates: { canonical: `/universite/${slug}/sayfa/${sayfa}` },
|
||||
};
|
||||
path: `/universite/${slug}/sayfa/${sayfa}`,
|
||||
openGraph: {
|
||||
images: ustSegmentOgGorseli(
|
||||
`/universite/${slug}`,
|
||||
"Üniversite taban puanları ve sıralamaları — KolayTercih",
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
return sayfaMetadata({
|
||||
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
|
||||
description: `${uni.ad} 2025 yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. 2026 tercih rehberi.`,
|
||||
alternates: { canonical: `/universite/${slug}` },
|
||||
};
|
||||
path: `/universite/${slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
export function UniversiteIcerik({
|
||||
@@ -274,11 +287,10 @@ export function UniversiteIcerik({
|
||||
|
||||
<UniversiteProgramTablosu gruplar={gruplar} />
|
||||
|
||||
<p className="mt-3 text-xs leading-5 text-slate-500">
|
||||
<VeriKaynagi>
|
||||
"—": o yıl için yerleştirme verisi bulunmuyor (yeni açılan
|
||||
veya o yıl yerleşen çıkmayan program). Kaynak: YÖK Atlas 2025
|
||||
yerleştirme sonuçları.
|
||||
</p>
|
||||
veya o yıl yerleşen çıkmayan program).{" "}
|
||||
</VeriKaynagi>
|
||||
|
||||
<Sayfalama
|
||||
tabanYol={`/universite/${slug}`}
|
||||
|
||||
29
src/features/katalog/components/veri-kaynagi.tsx
Normal file
29
src/features/katalog/components/veri-kaynagi.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { KATALOG_VERI_TARIHI } from "@/lib/seo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TARIH_METNI = new Date(
|
||||
`${KATALOG_VERI_TARIHI}T00:00:00Z`,
|
||||
).toLocaleDateString("tr-TR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
// Katalog tablolarının altındaki kaynak + tazelik satırı. Görünür metin olması
|
||||
// bilinçli: arama/AI botları JSON-LD'ye değil sayfadaki metne güvenir.
|
||||
export function VeriKaynagi({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<p className={cn("mt-3 text-xs leading-5 text-slate-500", className)}>
|
||||
{children}
|
||||
Kaynak: YÖK Atlas 2025 yerleştirme sonuçları. Son güncelleme:{" "}
|
||||
<time dateTime={KATALOG_VERI_TARIHI}>{TARIH_METNI}</time>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,12 @@ import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAllRehberler, getAllRehberSlugs, getRehber } from "../rehber-queries";
|
||||
import { JsonLd, articleJsonLd, breadcrumbJsonLd } from "@/lib/seo";
|
||||
import {
|
||||
JsonLd,
|
||||
articleJsonLd,
|
||||
breadcrumbJsonLd,
|
||||
sayfaMetadata,
|
||||
} from "@/lib/seo";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { ElCizimiSema } from "./el-cizimi-sema";
|
||||
import { RehberKapak } from "./rehber-kapak";
|
||||
@@ -20,17 +25,12 @@ export function rehberStaticSlugs(): string[] {
|
||||
export function rehberYaziMetadata(slug: string): Metadata {
|
||||
const yazi = getRehber(slug);
|
||||
if (!yazi) return {};
|
||||
return {
|
||||
return sayfaMetadata({
|
||||
title: yazi.baslik,
|
||||
description: yazi.aciklama,
|
||||
alternates: { canonical: `/rehber/${slug}` },
|
||||
openGraph: {
|
||||
type: "article",
|
||||
title: yazi.baslik,
|
||||
description: yazi.aciklama,
|
||||
publishedTime: yazi.tarih,
|
||||
},
|
||||
};
|
||||
path: `/rehber/${slug}`,
|
||||
openGraph: { type: "article", publishedTime: yazi.tarih },
|
||||
});
|
||||
}
|
||||
|
||||
export function RehberMakale({ slug }: { slug: string }) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { URUNLER } from "./credits";
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,62 @@ export const SITE_URL =
|
||||
|
||||
export const SITE_NAME = "KolayTercih";
|
||||
|
||||
/**
|
||||
* Katalog verisinin (data/yokatlas.db) en son yenilendiği gün. Sitemap lastmod,
|
||||
* katalog sayfalarındaki görünür "son güncelleme" satırı buradan okunur.
|
||||
* Yalnızca veri GERÇEKTEN değiştiğinde (ingest/refresh sonrası) elle güncelle;
|
||||
* build zamanına bağlama — her deploy'da değişen lastmod sinyali değersizleşir.
|
||||
*/
|
||||
export const KATALOG_VERI_TARIHI = "2026-08-09";
|
||||
|
||||
type SayfaMetadataGirdi = {
|
||||
title: string;
|
||||
description: string;
|
||||
/** Kanonik yol ("/bolumler"); metadataBase ile mutlak adrese çözülür. */
|
||||
path: string;
|
||||
/** Varsayılan "website"; rehber yazıları article alanlarını buradan verir. */
|
||||
openGraph?: NonNullable<Metadata["openGraph"]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* İndekslenebilir sayfaların metadata'sı için tek kaynak. Next, segmentler
|
||||
* arası metadata'yı SIĞ birleştirir: sayfa `openGraph` vermezse kök layout'unki
|
||||
* (ana sayfanın başlığı + og:url'i) olduğu gibi miras kalır; verirse nesnenin
|
||||
* tamamı ezilir (siteName/locale kaybolur). Bu yüzden openGraph burada eksiksiz
|
||||
* üretilir. twitter:title/description/image, openGraph'tan otomatik dolar.
|
||||
*/
|
||||
export function sayfaMetadata({
|
||||
title,
|
||||
description,
|
||||
path,
|
||||
openGraph,
|
||||
}: SayfaMetadataGirdi): Metadata {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: path },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "tr_TR",
|
||||
siteName: SITE_NAME,
|
||||
url: path,
|
||||
title,
|
||||
description,
|
||||
...openGraph,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Üst segmentin dosya-tabanlı OG görselini (opengraph-image.tsx) alt rotaya
|
||||
* elle bağlar. Gerekçe: alt rota `openGraph` tanımlayınca sığ birleştirme üst
|
||||
* segmentten gelen `images`'ı da düşürür (ör. /bolum/[slug]/sayfa/[no]).
|
||||
* Boyut, src/lib/og-sayfa.tsx içindeki OG_SIZE ile aynı olmalı.
|
||||
*/
|
||||
export function ustSegmentOgGorseli(path: string, alt: string) {
|
||||
return [{ url: `${path}/opengraph-image`, width: 1200, height: 630, alt }];
|
||||
}
|
||||
|
||||
type JsonLdData = Record<string, unknown>;
|
||||
|
||||
export function organizationJsonLd(): JsonLdData {
|
||||
|
||||
Reference in New Issue
Block a user