- derin /bolum ve /universite sayfalarında sıra aralığı hangi yıldan geldiğini söyler (2025 / 2024 / 2024–2025); 2024 sırası '2025 verisi' diye sunulmaz - <title> yuvarlanmış rakamlarla ve şablon eki olmadan ≤~86 karakter - giriş formunda tek geri sayım interval'i (çift sayaç butonu erken açıyordu) - RYBBIT_HOST sonundaki '/' atılır Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
397 lines
15 KiB
TypeScript
397 lines
15 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import { ChevronRight } from "lucide-react";
|
||
import {
|
||
getAllUniversiteler,
|
||
getAllUniversiteSlugs,
|
||
getUniversiteBySlug,
|
||
bolumSlugFromIsim,
|
||
} from "../katalog-queries";
|
||
import { trBaslikDuzeni } from "@/lib/slug";
|
||
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 {
|
||
KATALOG_CTA_ACIKLAMA,
|
||
Sayfalama,
|
||
SAYFA_BOYU,
|
||
sayfaDilimi,
|
||
siraYiliEtiketi,
|
||
} from "./sayfalama";
|
||
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";
|
||
import type { Program } from "@/types/yokatlas";
|
||
|
||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||
|
||
export function uniToplamSayfa(programSayisi: number): number {
|
||
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
||
}
|
||
|
||
// Sayfaların generateStaticParams'ı için — sayfalar *-queries import etmez.
|
||
export function uniStaticSlugs(): string[] {
|
||
return getAllUniversiteSlugs();
|
||
}
|
||
|
||
// 2..N sayfaları; 1. sayfa taban rota (/universite/[slug]) olduğundan üretilmez.
|
||
export function uniSayfaStaticParams(): { slug: string; no: string }[] {
|
||
return getAllUniversiteler().flatMap((u) => {
|
||
const toplam = uniToplamSayfa(u.programSayisi);
|
||
return Array.from({ length: Math.max(0, toplam - 1) }, (_, i) => ({
|
||
slug: u.slug,
|
||
no: String(i + 2),
|
||
}));
|
||
});
|
||
}
|
||
|
||
type UniDetay = NonNullable<ReturnType<typeof getUniversiteBySlug>>;
|
||
|
||
/**
|
||
* Sayfanın satır dilimi + o dilimde geçen fakülte/birim adları (tablo
|
||
* sırasıyla, tekrarsız). Liste fakülteye göre dizili olduğundan her derin
|
||
* sayfa belirli birimleri kapsar. Metadata ve gövde aynı hesabı buradan alır.
|
||
* Fakültesi boş satırlar tabloda "Diğer" grubuna düşer; ad listesine girmez.
|
||
*/
|
||
function uniSayfaDilimi(uni: UniDetay, sayfa: number) {
|
||
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
|
||
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
|
||
const dilim = sayfaDilimi(tumProgramlar, sayfa);
|
||
const fakulteAdlari: string[] = [];
|
||
for (const p of dilim) {
|
||
if (p.fakulte && !fakulteAdlari.includes(p.fakulte)) {
|
||
fakulteAdlari.push(p.fakulte);
|
||
}
|
||
}
|
||
return { tumProgramlar, dilim, fakulteAdlari };
|
||
}
|
||
|
||
/**
|
||
* İlk 2 ad + "ve N birim daha"; ad yoksa null (çağıran eski metne düşer).
|
||
* Dilim tek birimden ibaretse (50'den çok programlı MYO'lar art arda iki
|
||
* sayfayı doldurabiliyor) sayfalar ayırt edilsin diye birim içi taban sırası
|
||
* aralığı eklenir — birim içinde satırlar zaten taban sırasına göre dizili.
|
||
*/
|
||
function fakulteOzeti({
|
||
dilim,
|
||
fakulteAdlari: adlar,
|
||
}: ReturnType<typeof uniSayfaDilimi>): string | null {
|
||
if (adlar.length === 0) return null;
|
||
if (adlar.length === 1) {
|
||
const siralar = dilim
|
||
.map((p) => p.sira2025 ?? p.sira2024)
|
||
.filter((n): n is number => n != null);
|
||
if (siralar.length === 0) return adlar[0];
|
||
const min = Math.min(...siralar);
|
||
const max = Math.max(...siralar);
|
||
const yil = siraYiliEtiketi(
|
||
dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length,
|
||
siralar.length,
|
||
);
|
||
return min === max
|
||
? `${adlar[0]} (${yil} taban sırası ${sayi(min)})`
|
||
: `${adlar[0]} (${yil} taban sırası ${sayi(min)}–${sayi(max)} arası)`;
|
||
}
|
||
if (adlar.length === 2) return `${adlar[0]} ve ${adlar[1]}`;
|
||
return `${adlar[0]}, ${adlar[1]} ve ${adlar.length - 2} birim daha`;
|
||
}
|
||
|
||
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||
const uni = getUniversiteBySlug(slug);
|
||
if (!uni) return {};
|
||
if (sayfa > 1) {
|
||
// Üniversite adları uzun: <title> sayfa numarasıyla kalır (~60 karakter),
|
||
// farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider.
|
||
const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa));
|
||
return sayfaMetadata({
|
||
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
||
description: birimler
|
||
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
|
||
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
|
||
path: `/universite/${slug}/sayfa/${sayfa}`,
|
||
openGraph: {
|
||
images: ustSegmentOgGorseli(
|
||
`/universite/${slug}`,
|
||
"Üniversite taban puanları ve sıralamaları — KolayTercih",
|
||
),
|
||
},
|
||
});
|
||
}
|
||
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.`,
|
||
path: `/universite/${slug}`,
|
||
});
|
||
}
|
||
|
||
export function UniversiteIcerik({
|
||
slug,
|
||
sayfa,
|
||
}: {
|
||
slug: string;
|
||
sayfa: number;
|
||
}) {
|
||
const uni = getUniversiteBySlug(slug);
|
||
if (!uni) notFound();
|
||
|
||
const sayfaDilim = uniSayfaDilimi(uni, sayfa);
|
||
const { tumProgramlar, dilim } = sayfaDilim;
|
||
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
|
||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||
const birimler = fakulteOzeti(sayfaDilim);
|
||
const gruplar: {
|
||
fakulte: string;
|
||
programlar: (Program & { bolumSlug: string | null })[];
|
||
}[] = [];
|
||
for (const p of dilim) {
|
||
const anahtar = p.fakulte ?? "Diğer";
|
||
const satir = { ...p, bolumSlug: bolumSlugFromIsim(p.isim) };
|
||
const son = gruplar[gruplar.length - 1];
|
||
if (son && son.fakulte === anahtar) son.programlar.push(satir);
|
||
else gruplar.push({ fakulte: anahtar, programlar: [satir] });
|
||
}
|
||
|
||
const ilkSayfa = sayfa === 1;
|
||
const buYol = ilkSayfa
|
||
? `/universite/${slug}`
|
||
: `/universite/${slug}/sayfa/${sayfa}`;
|
||
const yurtdisi = uniUlkeKonum(uni.ad, uni.il);
|
||
|
||
return (
|
||
<>
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||
<JsonLd
|
||
data={breadcrumbJsonLd([
|
||
{ name: "Ana Sayfa", path: "/" },
|
||
{ name: "Üniversiteler", path: "/universiteler" },
|
||
{ name: uni.ad, path: `/universite/${slug}` },
|
||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||
])}
|
||
/>
|
||
{ilkSayfa ? (
|
||
<JsonLd
|
||
data={{
|
||
"@context": "https://schema.org",
|
||
"@type": "CollegeOrUniversity",
|
||
name: uni.ad,
|
||
url: `${SITE_URL}/universite/${slug}`,
|
||
...(uniLogoYolu(slug)
|
||
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
|
||
: {}),
|
||
...(uni.il || yurtdisi
|
||
? {
|
||
address: {
|
||
"@type": "PostalAddress",
|
||
...(yurtdisi?.sehir
|
||
? { addressLocality: yurtdisi.sehir }
|
||
: uni.il
|
||
? { addressLocality: trBaslikDuzeni(uni.il) }
|
||
: {}),
|
||
addressCountry: yurtdisi ? yurtdisi.iso : "TR",
|
||
},
|
||
}
|
||
: {}),
|
||
}}
|
||
/>
|
||
) : null}
|
||
|
||
<nav
|
||
aria-label="breadcrumb"
|
||
className="flex items-center gap-1 text-xs text-slate-500"
|
||
>
|
||
<Link href="/" className="hover:text-primary hover:underline">
|
||
Ana Sayfa
|
||
</Link>
|
||
<ChevronRight className="size-3" aria-hidden />
|
||
<Link
|
||
href="/universiteler"
|
||
className="hover:text-primary hover:underline"
|
||
>
|
||
Üniversiteler
|
||
</Link>
|
||
<ChevronRight className="size-3" aria-hidden />
|
||
{ilkSayfa ? (
|
||
<span className="text-slate-700">{uni.ad}</span>
|
||
) : (
|
||
<>
|
||
<Link
|
||
href={`/universite/${slug}`}
|
||
className="hover:text-primary hover:underline"
|
||
>
|
||
{uni.ad}
|
||
</Link>
|
||
<ChevronRight className="size-3" aria-hidden />
|
||
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||
</>
|
||
)}
|
||
</nav>
|
||
|
||
<div className="mt-4 flex items-center gap-4">
|
||
<UniLogo slug={slug} ad={uni.ad} boy="lg" />
|
||
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
|
||
{uni.ad} Taban Puanları 2026
|
||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||
</h1>
|
||
</div>
|
||
|
||
{ilkSayfa ? (
|
||
<>
|
||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||
{uni.ad}
|
||
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, 2025 YKS
|
||
yerleştirme sonuçlarına göre{" "}
|
||
<strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı
|
||
{uni.fakulteSayisi > 1
|
||
? `; programlar ${sayi(uni.fakulteSayisi)} fakülte/yüksekokula dağılıyor`
|
||
: ""}
|
||
.
|
||
{uni.stats.minSira != null ? (
|
||
<>
|
||
{" "}
|
||
En iyi taban başarı sıralaması{" "}
|
||
<strong>{sayi(uni.stats.minSira)}</strong>.
|
||
</>
|
||
) : null}
|
||
{uni.stats.toplamKontenjan > 0 ? (
|
||
<> Toplam kontenjan {sayi(uni.stats.toplamKontenjan)}.</>
|
||
) : null}{" "}
|
||
Aşağıdaki tablolar 2026 tercihleri için rehber niteliğindedir;{" "}
|
||
<em>
|
||
2026 taban puanları ve sıralamaları, yerleştirme sonuçları
|
||
açıklandığında netleşir.
|
||
</em>
|
||
</p>
|
||
|
||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||
<UniversiteKonumHaritasi universite={uni.ad} il={uni.il} />
|
||
<dl className="contents">
|
||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||
Program
|
||
</dt>
|
||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||
{sayi(uni.programSayisi)}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||
Fakülte / Birim
|
||
</dt>
|
||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||
{sayi(uni.fakulteSayisi)}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||
Tür
|
||
</dt>
|
||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||
{uni.unitur === "DEVLET"
|
||
? "Devlet"
|
||
: uni.unitur
|
||
? trBaslikDuzeni(uni.unitur)
|
||
: "—"}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||
En iyi sıralama
|
||
</dt>
|
||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||
{uni.stats.minSira != null ? sayi(uni.stats.minSira) : "—"}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||
{uni.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ı, fakülte sırasıyla).
|
||
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite
|
||
özeti ve istatistikler{" "}
|
||
<Link
|
||
href={`/universite/${slug}`}
|
||
className="font-medium text-primary hover:underline"
|
||
>
|
||
ilk sayfada
|
||
</Link>
|
||
.
|
||
</p>
|
||
)}
|
||
|
||
<PagePixelDivider seed={57} className="mt-8" />
|
||
|
||
<div id="programlar" className="mt-8">
|
||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
||
Üniversite programları
|
||
</h2>
|
||
<p className="mt-1 text-xs text-slate-500">
|
||
Fakülte ve birimlere göre
|
||
</p>
|
||
</div>
|
||
|
||
<UniversiteProgramTablosu gruplar={gruplar} />
|
||
|
||
<VeriKaynagi>
|
||
"—": o yıl için yerleştirme verisi bulunmuyor (yeni açılan
|
||
veya o yıl yerleşen çıkmayan program).{" "}
|
||
</VeriKaynagi>
|
||
|
||
<Sayfalama
|
||
tabanYol={`/universite/${slug}`}
|
||
sayfa={sayfa}
|
||
toplamSayfa={toplamSayfa}
|
||
/>
|
||
|
||
<div className="mt-10">
|
||
<CtaSiraForm
|
||
baslik={`Sıralaman ${uni.ad} tabanlarının neresinde?`}
|
||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||
/>
|
||
</div>
|
||
</main>
|
||
<SiteFooter />
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function UniversiteIcerikSkeleton() {
|
||
return (
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||
<Skeleton className="h-4 w-64" />
|
||
<div className="mt-4 flex items-center gap-4">
|
||
<Skeleton className="size-14 rounded-lg" />
|
||
<Skeleton className="h-10 w-2/3 max-w-xl" />
|
||
</div>
|
||
<Skeleton className="mt-4 h-5 w-full max-w-3xl" />
|
||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<Skeleton key={i} className="h-24 w-full rounded-xl" />
|
||
))}
|
||
</div>
|
||
<div className="mt-16 space-y-3">
|
||
<Skeleton className="h-7 w-64" />
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<Skeleton key={i} className="h-12 w-full" />
|
||
))}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|