Files
kolaytercih/src/features/katalog/components/universite-icerik.tsx
bilalgursen f05f3e0c69 fix: p7/p4 denetim bulguları — derin sayfa sıra yılı ve başlık uzunluğu, giriş sayacı, rybbit host
- 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>
2026-09-21 16:12:11 +03:00

397 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
&quot;—&quot;: 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>
);
}