import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
Briefcase,
Building2,
Calculator,
ChevronRight,
GraduationCap,
Landmark,
LayoutList,
Target,
} from "lucide-react";
import {
getAllBolumler,
getAllBolumSlugs,
getBolumBySlug,
uniSlugFromAd,
} from "../katalog-queries";
import type { Program } from "@/types/yokatlas";
import {
JsonLd,
breadcrumbJsonLd,
sayfaMetadata,
ustSegmentOgGorseli,
} from "@/lib/seo";
import {
KATALOG_CTA_ACIKLAMA,
SAYFA_BOYU,
sayfaDilimi,
siraYiliEtiketi,
} from "./sayfalama";
import { BolumProgramListesi } from "./bolum-program-listesi";
import { VeriKaynagi } from "./veri-kaynagi";
import { NavBaslik } from "./nav-baslik";
import { AkademikAyrinti } from "./akademik-ayrinti";
import { OzetKart } from "@/components/ozet-kart";
import {
BOLUM_ISTIHDAM_KAYNAK_URL,
BOLUM_ISTIHDAM_YIL,
ISTIHDAM_ORTALAMA,
bolumIstihdamBul,
type BolumIstihdam,
type IstihdamDuzeyi,
} from "@/lib/bolum-istihdam";
import { PagePixelDivider } from "@/components/pixel-decor";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { Skeleton } from "@/components/ui/skeleton";
import {
BASLIK_YILI,
ONCEKI_YIL,
SON_YIL,
TERCIH_YILI,
} from "@/lib/veri-yillari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
/**
* Özet kartında iri sayı telefonda ~94 px'e sığmalı (DESIGN.md §4.8):
* 36 px Cal Sans'ta "1.618.988" 123 px ölçüldü, ikonun altına giriyor. 6 ve
* 7 haneli sıralar birimle kısalır: 245.300 → "245" + "bin",
* 1.618.988 → "1,62" + "milyon".
*/
function kartSayisi(n: number): { deger: string; birim?: string } {
// 999.500+ "1.000 bin" olmasın diye milyona geçer
if (n >= 999_500) {
return {
deger: (n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 }),
birim: "milyon",
};
}
if (n >= 100_000) return { deger: sayi(Math.round(n / 1000)), birim: "bin" };
return { deger: sayi(n) };
}
const ondalik = (n: number) =>
n.toLocaleString("tr-TR", { maximumFractionDigits: 1 });
/** Ayrıntı satırının ikinci satırı: içeride ne olduğunu söyler (DESIGN.md §4.9). */
function istihdamOnizleme(v: BolumIstihdam): string {
const parcalar = [
v.istihdam != null
? `Mezunların %${ondalik(v.istihdam)}'i kayıtlı çalışıyor`
: null,
v.isBulmaAy != null ? `ilk iş ${ondalik(v.isBulmaAy)} ayda` : null,
v.istihdam == null && v.kazanc
? `kazanç grubu: ${v.kazanc.toLocaleLowerCase("tr-TR")}`
: null,
].filter(Boolean);
const metin = parcalar.join(" · ");
return metin.charAt(0).toLocaleUpperCase("tr-TR") + metin.slice(1);
}
/**
* Mezun istihdamı: TÜİK Yükseköğretim İstihdam Göstergeleri (ülke geneli,
* bölüm bazında). Sayfada değil, özet kartlarının altındaki "Mezun
* istihdamı ›" satırının açtığı alt sayfada durur (DESIGN.md §4.9–4.11):
* kart ve süs ayraç yok, gösterge kareleri masonry, altında tek not ve
* kaynak. Renk sempati katar, anlam taşımaz — risk dili değildir. data-*
* işaretleri açılış koreografisinin sözleşmesi (akademik-ayrinti.tsx).
*/
function MezunIstihdami({
veri,
duzey,
}: {
veri: BolumIstihdam;
duzey: IstihdamDuzeyi;
}) {
const ort = ISTIHDAM_ORTALAMA[duzey];
const kare =
"flex flex-col items-center justify-center rounded-3xl px-3 text-white";
const etiket = "text-sm font-bold text-white/90";
const devSayi =
"font-heading text-5xl font-bold leading-none tracking-tight";
const orta = "font-heading text-3xl font-bold leading-none";
const istihdamKaresi =
veri.istihdam != null ? (
Kayıtlı çalışan
%{ondalik(veri.istihdam)}
{ort.istihdam != null ? (
Türkiye: %{ondalik(ort.istihdam)}
) : null}
) : null;
const kazancKaresi = veri.kazanc ? (
{veri.kazanc}
Kazanç grubu
) : null;
const sureKaresi =
veri.isBulmaAy != null ? (
İlk işi bulma
{ondalik(veri.isBulmaAy)}
ay
{ort.isBulmaAy != null ? (
Türkiye: {ondalik(ort.isBulmaAy)} ay
) : null}
) : null;
const solVar = istihdamKaresi != null || kazancKaresi != null;
return (
{solVar ? (
{istihdamKaresi}
{kazancKaresi}
) : null}
{sureKaresi ? (
{sureKaresi}
) : null}
Türkiye geneli, tüm üniversitelerin{" "}
{duzey === "lisans" ? "lisans" : "ön lisans"} mezunları birlikte;
üniversiteye göre değişir. Kayıtlı çalışan, sigortalı bir işte
çalışanların oranıdır. Kazanç grubu bölümler arası kıyastır, maaş
değildir.
);
}
/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */
const PUAN_TURU_HAP: Record = {
SAYISAL: "Sayısal puan",
"EŞİT AĞIRLIK": "Eşit ağırlık puanı",
SÖZEL: "Sözel puan",
DİL: "Dil puanı",
};
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),
}));
});
}
type BolumDetay = NonNullable>;
/**
* Sayfanın satır dilimi + o dilimin taban sırası aralığı. Liste efektif sıraya
* göre dizili (lisans, sonra önlisans) olduğundan her derin sayfa belirli bir
* sıralama aralığını kapsar. Metadata ve gövde aynı hesabı buradan alır; ikisi
* ayrı ayrı dilimlerse başlık ile tablo birbirinden kayabilir.
*/
function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) {
const tumProgramlar = [
...bolum.lisansProgramlari,
...bolum.onlisansProgramlari,
];
const dilim = sayfaDilimi(tumProgramlar, sayfa);
let minSira: number | null = null;
let maxSira: number | null = null;
let verisiz = 0;
// Sırası son yılda olmayıp bir önceki yıldan gelen satırlar: aralık metni
// yılı dürüstçe söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
let yalnizOnceki = 0;
for (const p of dilim) {
const efektif = p.efektifSira;
if (efektif == null) {
verisiz += 1;
continue;
}
if (p.siraSon == null) yalnizOnceki += 1;
if (minSira == null || efektif < minSira) minSira = efektif;
if (maxSira == null || efektif > maxSira) maxSira = efektif;
}
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalnizOnceki };
}
/** için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */
function kisaSira(n: number): string {
if (n >= 1_000_000) {
return `${(n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 })} mn`;
}
return n >= 10_000 ? `${Math.round(n / 1000)} bin` : sayi(n);
}
/**
* Dilimin okunur özeti ("taban sırası 95.000–180.000 arası programlar").
* Dilim boşsa null (çağıran sayfa numaralı eski metne düşer); dilimde hiç
* taban verisi yoksa aralık yerine bunu söyler.
*/
function siraAraligiMetni({
dilim,
minSira,
maxSira,
verisiz,
yalnizOnceki,
}: ReturnType): string | null {
if (dilim.length === 0) return null;
if (minSira == null || maxSira == null) {
return "taban verisi olmayan programlar";
}
const yil = siraYiliEtiketi(yalnizOnceki, dilim.length - verisiz);
const aralik =
minSira === maxSira
? `${yil} taban sırası ${sayi(minSira)} olan programlar`
: `${yil} taban sırası ${sayi(minSira)}–${sayi(maxSira)} arası programlar`;
return verisiz > 0 ? `${aralik} ile taban verisi olmayanlar` : aralik;
}
/**
* İlk sayfanın açılış cümleleri: kaç ilde kaç program, devlet/vakıf dağılımı,
* toplam kontenjan, taban sıralaması aralığı ve bir önceki yıla göre yön.
* Yön için yalnızca iki yılda da sırası olan programlar sayılır; sıra sayısı
* büyümüşse o programa daha geriden yerleşilmiş demektir (öğrenci dilinde
* "gevşedi"). Beş programdan azsa yön cümlesi kurulmaz.
*/
function bolumVeriOzeti(bolum: BolumDetay, programlar: Program[]): string {
const { stats } = bolum;
const iller = new Set();
let kontenjan = 0;
let gevseyen = 0;
let sikilasan = 0;
let karsilastirilan = 0;
for (const p of programlar) {
if (p.il) iller.add(p.il);
kontenjan += p.kontenjanSon ?? 0;
const onceki = p.siraGecmisi[p.siraGecmisi.length - 2];
if (p.siraSon != null && onceki != null) {
karsilastirilan += 1;
if (p.siraSon > onceki) gevseyen += 1;
else if (p.siraSon < onceki) sikilasan += 1;
}
}
const digerTur = stats.toplam - stats.devlet - stats.vakif;
const dagilim = [
stats.devlet > 0 ? `${sayi(stats.devlet)} devlet` : null,
stats.vakif > 0 ? `${sayi(stats.vakif)} vakıf` : null,
digerTur > 0 ? `${sayi(digerTur)} KKTC/yurt dışı` : null,
]
.filter(Boolean)
.join(", ");
const ilSayisi = iller.size > 0 ? iller.size : stats.ilSayisi;
const cumleler = [
`${bolum.ad} bölümü ${SON_YIL} YKS yerleştirmesinde ${sayi(ilSayisi)} ilde ${sayi(stats.toplam)} programda öğrenci aldı (${dagilim}${kontenjan > 0 ? `; toplam ${sayi(kontenjan)} kontenjan` : ""}).`,
];
if (stats.minSira != null && stats.maxSira != null) {
cumleler.push(
stats.minSira === stats.maxSira
? `Taban başarı sıralaması ${sayi(stats.minSira)}.`
: `Taban başarı sıralaması programa göre ${sayi(stats.minSira)} ile ${sayi(stats.maxSira)} arasında değişiyor.`,
);
}
if (karsilastirilan >= 5) {
// Yüzdeye iyelik eki gerekmeyen kalıp: "%7'sinde / %9'unda" gibi ek
// seçimi sayının okunuşuna bağlı, şablonda güvenle kurulamaz.
const kucOran = Math.round((sikilasan / karsilastirilan) * 100);
const buyOran = Math.round((gevseyen / karsilastirilan) * 100);
cumleler.push(
kucOran >= buyOran
? `${ONCEKI_YIL}'e göre taban sıralaması sayı olarak küçülen (yerleşmesi zorlaşan) program oranı %${kucOran}, büyüyen (kolaylaşan) %${buyOran}.`
: `${ONCEKI_YIL}'e göre taban sıralaması sayı olarak büyüyen (yerleşmesi kolaylaşan) program oranı %${buyOran}, küçülen (zorlaşan) %${kucOran}.`,
);
}
return cumleler.join(" ");
}
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
const bolum = getBolumBySlug(slug);
if (!bolum) return {};
const { stats } = bolum;
if (sayfa > 1) {
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
const aralik = siraAraligiMetni(sayfaDilim);
const { minSira, maxSira } = sayfaDilim;
// kısa kalsın (arama sonucunda ayırt edici kısım kesilmesin):
// yuvarlanmış rakamlar, kök şablonun " — KolayTercih" eki yok; açıklama tam
// cümleyi taşır. "s. N" yuvarlamada çakışan iki sayfayı yine ayırır.
const baslikEki =
minSira != null && maxSira != null
? minSira === maxSira
? `Sıra ${kisaSira(minSira)}, s. ${sayfa}`
: `Sıra ${kisaSira(minSira)}–${kisaSira(maxSira)}, s. ${sayfa}`
: `Sayfa ${sayfa}`;
const baslik = `${bolum.ad} Taban Puanları (${baslikEki})`;
const meta = sayfaMetadata({
title: baslik,
description: aralik
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (${SON_YIL}; o yıl verisi olmayanlarda ${ONCEKI_YIL}) — taban puanı, başarı sıralaması ve kontenjan.`
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın ${SON_YIL} yerleştirme verileri.`,
path: `/bolum/${slug}/sayfa/${sayfa}`,
openGraph: {
images: ustSegmentOgGorseli(
`/bolum/${slug}`,
"Bölüm taban puanları ve sıralamaları — KolayTercih",
),
},
});
return { ...meta, title: { absolute: baslik } };
}
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ı ${BASLIK_YILI}`,
description: `${bolum.ad} ${SON_YIL} yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} ${TERCIH_YILI} tercih rehberi.`,
path: `/bolum/${slug}`,
});
}
export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
const bolum = getBolumBySlug(slug);
if (!bolum) notFound();
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
const { tumProgramlar } = sayfaDilim;
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
const ilkSayfa = sayfa === 1;
const aralik = siraAraligiMetni(sayfaDilim);
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
// Üst satır ve alt kart aynı soruyu sorar; sayfalama sayfalarında da bölüm
// adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor).
const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`;
// Kimlik hapları (DESIGN.md §4.12): düzey + lisans puan türleri. TYT
// puanı önlisansın kendisi olduğundan ayrı hap açmaz.
const puanTurleri = [
...new Set(bolum.lisansProgramlari.map((p) => p.tur)),
].flatMap((t) => (PUAN_TURU_HAP[t] ? [PUAN_TURU_HAP[t]] : []));
const duzeyler = [
...(bolum.lisansProgramlari.length > 0 ? ["Lisans"] : []),
...(bolum.onlisansProgramlari.length > 0 ? ["Önlisans"] : []),
];
// Özet kartları: devlet ve vakıf ayrı sayılır; KKTC ve yurt dışı
// programları toplamda var ama iki karta da girmez (vakıf sayılmaz).
const devletUni = new Set();
const vakifUni = new Set();
let vakifProgram = 0;
for (const p of tumProgramlar) {
if (p.unitur === "DEVLET") devletUni.add(p.universite);
else if (p.unitur === "VAKIF" || p.unitur === "VAKIF MYO") {
vakifProgram += 1;
vakifUni.add(p.universite);
}
}
const { stats } = bolum;
// TÜİK mezun istihdamı: lisansı olan bölümde lisans tablosu, yalnız ön
// lisansı olanda ön lisans tablosu. Tabloda yoksa satır hiç çizilmez.
const istihdamDuzeyi: IstihdamDuzeyi =
bolum.lisansProgramlari.length > 0 ? "lisans" : "onlisans";
const istihdam = bolumIstihdamBul(slug, istihdamDuzeyi);
// Sayfaya özgü, veriden türeyen özet cümle (GEO: yapay zekâ cevap motorları
// sayfa başına ilk metni saklar; 1011 katalog sayfasının aynı şablon cümleyle
// açılması hem alıntıyı hem Google'ın "ölçekli içerik" politikasını zorlar —
// docs/arastirma/2026-09-24-geo-seo). Rakamlar tablodakiyle aynı kaynaktan.
const veriOzeti = ilkSayfa ? bolumVeriOzeti(bolum, tumProgramlar) : null;
const hapSinifi =
"inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800";
return (
<>
Ana Sayfa
Bölümler
{ilkSayfa ? (
{bolum.ad}
) : (
<>
{bolum.ad}
Sayfa {sayfa}
>
)}
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir
(logosu yok — DESIGN.md §4.14). Geniş ekran (lg) altında başlık
bloğu ortalı (§5.2). */}
{bolum.ad}
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
{!ilkSayfa && aralik ? (
<>
{" "}
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
>
) : null}
{duzeyler.length > 0 || puanTurleri.length > 0 ? (
{duzeyler.map((d) => (
{d}
))}
{puanTurleri.map((t) => (
{t}
))}
) : null}
{ilkSayfa ? (
{veriOzeti} Aşağıdaki tabloda her programın taban puanı, başarı
sıralaması ve kontenjanı var.{" "}
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
netleşir.
) : (
{bolum.ad} taban puanları tablosunun {sayfa}. sayfası (
{sayi(tumProgramlar.length)} programın{" "}
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
satırları, taban sırasına göre)
{aralik ? `: ${aralik}` : ""}. Listenin başı{" "}
ilk sayfada
.
)}
{/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra
girişi özet paragrafın hemen altında, listenin üstünde. Alttaki
kart kalır. */}
{/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin
sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */}
{ilkSayfa ? (
0
? `${sayi(devletUni.size)} üniversitede`
: "bu bölümde yok"
}
/>
0
? `${sayi(vakifUni.size)} üniversitede`
: "bu bölümde yok"
}
/>
) : null}
{ilkSayfa && istihdam ? (
}
>
) : null}
Üniversite programları
{sayi(tumProgramlar.length)} program, taban sırasına göre
({
...program,
universiteSlug: uniSlugFromAd(program.universite),
}))}
bazAd={bolum.ad}
tabanYol={`/bolum/${slug}`}
sayfa={sayfa}
/>
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
>
);
}
export function BolumIcerikSkeleton() {
return (
{Array.from({ length: 4 }, (_, i) => (
))}
{Array.from({ length: 5 }, (_, i) => (
))}
);
}