All checks were successful
Deploy / deploy (push) Successful in 6m16s
GEO araştırması (docs/arastirma/2026-09-24-geo-seo) Dalga 1-2 uygulaması: - /llms.txt ve /feed.xml route'ları (getAllRehberler'den otomatik), head'de RSS linki - Article JSON-LD: author, image, mainEntityOfPage, publisher logo; görünür 'Yazan' satırı - robots: ClaudeBot ayrı grupta Crawl-delay 1 - scripts/indexnow.ts (pnpm indexnow) + public anahtar dosyası - bölüm sayfası açılışı veriden üretilen özgü cümleler (il/program/tür/kontenjan, taban aralığı, yıllık yön) - ana sayfa fiyat iddiasına kaynak; iki yazıda 23.000 -> 21.000+; beş yazıya 'Kısa cevap' açılışı - yeni yazı: vakif-universitesi-bursu-kesilir-mi (ÖSYM 2026 kılavuzu + YÖK ücret esasları) - YAYIN-KUYRUGU: IndexNow adımı ve aylık AI görünürlük rutini Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
528 lines
20 KiB
TypeScript
528 lines
20 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import {
|
||
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 { OzetKart } from "@/components/ozet-kart";
|
||
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) };
|
||
}
|
||
|
||
/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */
|
||
const PUAN_TURU_HAP: Record<string, string> = {
|
||
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<ReturnType<typeof getBolumBySlug>>;
|
||
|
||
/**
|
||
* 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 };
|
||
}
|
||
|
||
/** <title> 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<typeof bolumSayfaDilimi>): 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<string>();
|
||
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;
|
||
// <title> 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<string>();
|
||
const vakifUni = new Set<string>();
|
||
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;
|
||
// 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-white px-4 text-sm font-semibold text-slate-800";
|
||
|
||
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: "Bölümler", path: "/bolumler" },
|
||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||
])}
|
||
/>
|
||
|
||
<nav
|
||
aria-label="İçerik yolu"
|
||
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
|
||
>
|
||
<Link
|
||
href="/"
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
Ana Sayfa
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
<Link
|
||
href="/bolumler"
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
Bölümler
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
{ilkSayfa ? (
|
||
<span className="text-slate-700">{bolum.ad}</span>
|
||
) : (
|
||
<>
|
||
<Link
|
||
href={`/bolum/${slug}`}
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
{bolum.ad}
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||
</>
|
||
)}
|
||
</nav>
|
||
|
||
{/* 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). */}
|
||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||
<h1
|
||
id="bolum-baslik"
|
||
className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left"
|
||
>
|
||
{bolum.ad}
|
||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||
{!ilkSayfa && aralik ? (
|
||
<>
|
||
{" "}
|
||
<span className="mt-2 block text-lg font-semibold text-slate-500 sm:text-2xl">
|
||
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||
</span>
|
||
</>
|
||
) : null}
|
||
</h1>
|
||
|
||
{duzeyler.length > 0 || puanTurleri.length > 0 ? (
|
||
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||
{duzeyler.map((d) => (
|
||
<span key={d} className={hapSinifi}>
|
||
<GraduationCap className="size-4 text-primary" aria-hidden />
|
||
{d}
|
||
</span>
|
||
))}
|
||
{puanTurleri.map((t) => (
|
||
<span key={t} className={hapSinifi}>
|
||
<Calculator className="size-4 text-primary" aria-hidden />
|
||
{t}
|
||
</span>
|
||
))}
|
||
</p>
|
||
) : null}
|
||
|
||
{ilkSayfa ? (
|
||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||
{veriOzeti} Aşağıdaki tabloda her programın taban puanı, başarı
|
||
sıralaması ve kontenjanı var.{" "}
|
||
<em>
|
||
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
|
||
netleşir.
|
||
</em>
|
||
</p>
|
||
) : (
|
||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||
{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şı{" "}
|
||
<Link
|
||
href={`/bolum/${slug}`}
|
||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
ilk sayfada
|
||
</Link>
|
||
.
|
||
</p>
|
||
)}
|
||
|
||
{/* 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. */}
|
||
<div className="mt-8">
|
||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||
</div>
|
||
|
||
{/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin
|
||
sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */}
|
||
{ilkSayfa ? (
|
||
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||
<OzetKart
|
||
ikon={LayoutList}
|
||
etiket="Program"
|
||
deger={sayi(stats.toplam)}
|
||
alt={`${sayi(stats.ilSayisi)} ilde`}
|
||
/>
|
||
<OzetKart
|
||
ikon={Landmark}
|
||
etiket="Devlet"
|
||
deger={sayi(stats.devlet)}
|
||
alt={
|
||
devletUni.size > 0
|
||
? `${sayi(devletUni.size)} üniversitede`
|
||
: "bu bölümde yok"
|
||
}
|
||
/>
|
||
<OzetKart
|
||
ikon={Building2}
|
||
etiket="Vakıf"
|
||
deger={sayi(vakifProgram)}
|
||
alt={
|
||
vakifUni.size > 0
|
||
? `${sayi(vakifUni.size)} üniversitede`
|
||
: "bu bölümde yok"
|
||
}
|
||
/>
|
||
<OzetKart
|
||
ikon={Target}
|
||
etiket="En iyi sıralama"
|
||
{...(stats.minSira != null
|
||
? kartSayisi(stats.minSira)
|
||
: { deger: "—" })}
|
||
alt={`${SON_YIL} taban sırası`}
|
||
/>
|
||
</dl>
|
||
) : null}
|
||
|
||
<PagePixelDivider seed={83} className="mx-auto mt-12 lg:mx-0" />
|
||
|
||
<section aria-labelledby="universite-programlari" className="mt-10">
|
||
<div className="text-center lg:text-left">
|
||
<h2
|
||
id="universite-programlari"
|
||
className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl"
|
||
>
|
||
Üniversite programları
|
||
</h2>
|
||
<p className="mt-2 text-base text-slate-500">
|
||
{sayi(tumProgramlar.length)} program, taban sırasına göre
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<BolumProgramListesi
|
||
programlar={tumProgramlar.map((program) => ({
|
||
...program,
|
||
universiteSlug: uniSlugFromAd(program.universite),
|
||
}))}
|
||
bazAd={bolum.ad}
|
||
tabanYol={`/bolum/${slug}`}
|
||
sayfa={sayfa}
|
||
/>
|
||
</div>
|
||
<VeriKaynagi className="mt-6" />
|
||
</section>
|
||
|
||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||
<div className="mt-10">
|
||
<CtaSiraForm
|
||
konum="alt"
|
||
baslik={ctaBaslik}
|
||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||
/>
|
||
</div>
|
||
</main>
|
||
<SiteFooter />
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function BolumIcerikSkeleton() {
|
||
return (
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||
<Skeleton className="mx-auto h-5 w-56 lg:mx-0" />
|
||
<Skeleton className="mx-auto mt-6 h-10 w-2/3 max-w-xl lg:mx-0" />
|
||
<Skeleton className="mx-auto mt-5 h-10 w-48 rounded-full lg:mx-0" />
|
||
<Skeleton className="mx-auto mt-6 h-5 w-full max-w-3xl lg:mx-0" />
|
||
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||
{Array.from({ length: 4 }, (_, i) => (
|
||
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||
))}
|
||
</div>
|
||
<div className="mt-16 space-y-3">
|
||
<Skeleton className="h-7 w-64" />
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<Skeleton key={i} className="h-20 w-full rounded-2xl" />
|
||
))}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|