- 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>
179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import { ChevronRight } from "lucide-react";
|
||
import {
|
||
getAllBolumler,
|
||
getAllBolumSlugs,
|
||
getBolumBySlug,
|
||
uniSlugFromAd,
|
||
} from "../katalog-queries";
|
||
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";
|
||
|
||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||
|
||
export function bolumToplamSayfa(programSayisi: number): number {
|
||
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
||
}
|
||
|
||
// Sayfaların generateStaticParams'ı için — sayfalar *-queries import etmez.
|
||
export function bolumStaticSlugs(): string[] {
|
||
return getAllBolumSlugs();
|
||
}
|
||
|
||
// 2..N sayfaları; 1. sayfa taban rota (/bolum/[slug]) olduğundan üretilmez.
|
||
export function bolumSayfaStaticParams(): { slug: string; no: string }[] {
|
||
return getAllBolumler().flatMap((b) => {
|
||
const toplam = bolumToplamSayfa(b.programSayisi);
|
||
return Array.from({ length: Math.max(0, toplam - 1) }, (_, i) => ({
|
||
slug: b.slug,
|
||
no: String(i + 2),
|
||
}));
|
||
});
|
||
}
|
||
|
||
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||
const bolum = getBolumBySlug(slug);
|
||
if (!bolum) return {};
|
||
const { stats } = bolum;
|
||
if (sayfa > 1) {
|
||
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.`,
|
||
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 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.`,
|
||
path: `/bolum/${slug}`,
|
||
});
|
||
}
|
||
|
||
export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||
const bolum = getBolumBySlug(slug);
|
||
if (!bolum) notFound();
|
||
|
||
const tumProgramlar = [
|
||
...bolum.lisansProgramlari,
|
||
...bolum.onlisansProgramlari,
|
||
];
|
||
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
|
||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||
const ilkSayfa = sayfa === 1;
|
||
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
|
||
|
||
return (
|
||
<>
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||
<JsonLd
|
||
data={breadcrumbJsonLd([
|
||
{ name: "Ana Sayfa", path: "/" },
|
||
{ name: "Bölümler", path: "/bolumler" },
|
||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||
])}
|
||
/>
|
||
|
||
<nav
|
||
aria-label="İçerik yolu"
|
||
className="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
|
||
>
|
||
<Link
|
||
href="/"
|
||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
Ana sayfa
|
||
</Link>
|
||
<ChevronRight className="size-3.5" aria-hidden />
|
||
<Link
|
||
href="/bolumler"
|
||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
Bölümler
|
||
</Link>
|
||
<ChevronRight className="size-3.5" aria-hidden />
|
||
<span className="text-foreground">
|
||
{ilkSayfa ? bolum.ad : `Sayfa ${sayfa}`}
|
||
</span>
|
||
</nav>
|
||
|
||
<header className="border-b pb-7 sm:pb-9">
|
||
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||
{bolum.ad}
|
||
</h1>
|
||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
|
||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele
|
||
{!ilkSayfa ? ` — sayfa ${sayfa}` : ""}.
|
||
</p>
|
||
</header>
|
||
|
||
<section aria-labelledby="universite-programlari" className="mt-8">
|
||
<div className="mb-4 flex items-end justify-between gap-4">
|
||
<h2
|
||
id="universite-programlari"
|
||
className="font-heading text-xl font-bold text-foreground sm:text-2xl"
|
||
>
|
||
Üniversite programları
|
||
</h2>
|
||
<span className="shrink-0 text-sm text-muted-foreground">
|
||
{sayi(tumProgramlar.length)} program
|
||
</span>
|
||
</div>
|
||
|
||
<BolumProgramListesi
|
||
programlar={tumProgramlar.map((program) => ({
|
||
...program,
|
||
universiteSlug: uniSlugFromAd(program.universite),
|
||
}))}
|
||
bazAd={bolum.ad}
|
||
tabanYol={`/bolum/${slug}`}
|
||
sayfa={sayfa}
|
||
/>
|
||
<VeriKaynagi className="mt-6" />
|
||
</section>
|
||
</main>
|
||
<SiteFooter />
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function BolumIcerikSkeleton() {
|
||
return (
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||
<Skeleton className="h-5 w-56" />
|
||
<div className="border-b pb-7 sm:pb-9">
|
||
<Skeleton className="mt-5 h-10 w-2/3 max-w-xl" />
|
||
<Skeleton className="mt-3 h-5 w-full max-w-2xl" />
|
||
</div>
|
||
<div className="mt-8 space-y-3">
|
||
<Skeleton className="h-7 w-64" />
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<Skeleton key={i} className="h-12 w-full" />
|
||
))}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|