refactor: katalog feature'ı — sayfalar senkron kompozisyona geçti (Faz 2)

- 8 katalog bileşeni src/features/katalog/components/ altına taşındı;
  katalog-queries.ts (server-only) lib/katalog'un feature kapısı oldu
- /bolumler ve /universiteler'in sayfa içi fetch+inline bileşenleri
  BolumKatalogu/UniversiteKatalogu feature bileşenlerine çıkarıldı
- 6 sayfa async'ten senkron'a: params.then() + sayfada Suspense,
  skeleton'lar feature bileşeninin yanında (BolumIcerikSkeleton vb.)
- generateStaticParams artık *-queries yerine bileşen dosyasının
  helper'larını kullanıyor (bolumStaticSlugs, uniSayfaStaticParams)
- Davranış değişikliği yok; route durumları aynı (◐), 1917 statik sayfa

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:14:00 +03:00
parent 820eea7eca
commit 8d26e375ff
19 changed files with 425 additions and 258 deletions

View File

@@ -0,0 +1,316 @@
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, SITE_URL } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
import { PagePixelDivider } from "@/components/pixel-decor";
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
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),
}));
});
}
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
const uni = getUniversiteBySlug(slug);
if (!uni) return {};
if (sayfa > 1) {
return {
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
description: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
alternates: { canonical: `/universite/${slug}/sayfa/${sayfa}` },
};
}
return {
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.`,
alternates: { canonical: `/universite/${slug}` },
};
}
export function UniversiteIcerik({
slug,
sayfa,
}: {
slug: string;
sayfa: number;
}) {
const uni = getUniversiteBySlug(slug);
if (!uni) notFound();
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
const dilim = tumProgramlar.slice(
(sayfa - 1) * SAYFA_BOYU,
sayfa * SAYFA_BOYU,
);
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}`;
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
? {
address: {
"@type": "PostalAddress",
addressLocality: trBaslikDuzeni(uni.il),
addressCountry: "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). Ü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} />
<p className="mt-3 text-xs leading-5 text-slate-500">
&quot;—&quot;: o yıl için yerleştirme verisi bulunmuyor (yeni açılan
veya o yıl yerleşen çıkmayan program). Kaynak: YÖK Atlas 2025
yerleştirme sonuçları.
</p>
<Sayfalama
tabanYol={`/universite/${slug}`}
sayfa={sayfa}
toplamSayfa={toplamSayfa}
/>
<div className="mt-10">
<CtaSiraForm
baslik={`Sıralamanla ${uni.ad} programlarına yerleşir misin?`}
/>
</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>
);
}