feat(seo): derin katalog sayfalarında başlık/açıklama/H1'i sayfanın içeriğiyle ayrıştır
/bolum/*/sayfa/N: title + description + H1 o sayfanın taban sırası aralığını taşır (dilimdeki min–max efektif sıra; veri yoksa "taban verisi olmayan programlar"). Görünür breadcrumb üniversite sayfasındaki desenle eşitlendi (üst bölüm linki + "Sayfa N"). /universite/*/sayfa/N: description + giriş paragrafı o sayfadaki birim adlarını sayar (ilk 2 + "ve N birim daha"); dilim tek birimden ibaretse birim içi taban sırası aralığı eklenir. <title> uzunluk nedeniyle değişmedi. Dilim hesabı tek yardımcıda (sayfaDilimi); metadata ve gövde aynı hesabı kullanır. Canonical, robots, sitemap, rota dosyaları ve 1. sayfaların başlık/açıklaması değişmedi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
sayfaMetadata,
|
sayfaMetadata,
|
||||||
ustSegmentOgGorseli,
|
ustSegmentOgGorseli,
|
||||||
} from "@/lib/seo";
|
} from "@/lib/seo";
|
||||||
import { SAYFA_BOYU } from "./sayfalama";
|
import { SAYFA_BOYU, sayfaDilimi } from "./sayfalama";
|
||||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||||
import { VeriKaynagi } from "./veri-kaynagi";
|
import { VeriKaynagi } from "./veri-kaynagi";
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
@@ -42,14 +42,77 @@ export function bolumSayfaStaticParams(): { slug: string; no: string }[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
for (const p of dilim) {
|
||||||
|
const efektif = p.sira2025 ?? p.sira2024;
|
||||||
|
if (efektif == null) {
|
||||||
|
verisiz += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (minSira == null || efektif < minSira) minSira = efektif;
|
||||||
|
if (maxSira == null || efektif > maxSira) maxSira = efektif;
|
||||||
|
}
|
||||||
|
return { tumProgramlar, dilim, minSira, maxSira, verisiz };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
}: ReturnType<typeof bolumSayfaDilimi>): string | null {
|
||||||
|
if (dilim.length === 0) return null;
|
||||||
|
if (minSira == null || maxSira == null) {
|
||||||
|
return "taban verisi olmayan programlar";
|
||||||
|
}
|
||||||
|
const aralik =
|
||||||
|
minSira === maxSira
|
||||||
|
? `taban sırası ${sayi(minSira)} olan programlar`
|
||||||
|
: `taban sırası ${sayi(minSira)}–${sayi(maxSira)} arası programlar`;
|
||||||
|
return verisiz > 0 ? `${aralik} ile taban verisi olmayanlar` : aralik;
|
||||||
|
}
|
||||||
|
|
||||||
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||||
const bolum = getBolumBySlug(slug);
|
const bolum = getBolumBySlug(slug);
|
||||||
if (!bolum) return {};
|
if (!bolum) return {};
|
||||||
const { stats } = bolum;
|
const { stats } = bolum;
|
||||||
if (sayfa > 1) {
|
if (sayfa > 1) {
|
||||||
|
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
|
||||||
|
const aralik = siraAraligiMetni(sayfaDilim);
|
||||||
|
const { minSira, maxSira } = sayfaDilim;
|
||||||
|
// <title> kısa kalsın: aralığın yalnız rakamları; açıklama tam cümleyi taşır.
|
||||||
|
const baslikEki =
|
||||||
|
minSira != null && maxSira != null
|
||||||
|
? minSira === maxSira
|
||||||
|
? `Sıra ${sayi(minSira)}, Sayfa ${sayfa}`
|
||||||
|
: `Sıra ${sayi(minSira)}–${sayi(maxSira)}, Sayfa ${sayfa}`
|
||||||
|
: `Sayfa ${sayfa}`;
|
||||||
return sayfaMetadata({
|
return sayfaMetadata({
|
||||||
title: `${bolum.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
title: `${bolum.ad} Taban Puanları 2026 (${baslikEki})`,
|
||||||
description: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
|
description: aralik
|
||||||
|
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın 2025 yerleştirme verileri — 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 2025 yerleştirme verileri.`,
|
||||||
path: `/bolum/${slug}/sayfa/${sayfa}`,
|
path: `/bolum/${slug}/sayfa/${sayfa}`,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: ustSegmentOgGorseli(
|
images: ustSegmentOgGorseli(
|
||||||
@@ -74,13 +137,12 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
|||||||
const bolum = getBolumBySlug(slug);
|
const bolum = getBolumBySlug(slug);
|
||||||
if (!bolum) notFound();
|
if (!bolum) notFound();
|
||||||
|
|
||||||
const tumProgramlar = [
|
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
|
||||||
...bolum.lisansProgramlari,
|
const { tumProgramlar } = sayfaDilim;
|
||||||
...bolum.onlisansProgramlari,
|
|
||||||
];
|
|
||||||
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
|
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
|
||||||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||||||
const ilkSayfa = sayfa === 1;
|
const ilkSayfa = sayfa === 1;
|
||||||
|
const aralik = siraAraligiMetni(sayfaDilim);
|
||||||
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
|
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,20 +175,57 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
|||||||
Bölümler
|
Bölümler
|
||||||
</Link>
|
</Link>
|
||||||
<ChevronRight className="size-3.5" aria-hidden />
|
<ChevronRight className="size-3.5" aria-hidden />
|
||||||
<span className="text-foreground">
|
{ilkSayfa ? (
|
||||||
{ilkSayfa ? bolum.ad : `Sayfa ${sayfa}`}
|
<span className="text-foreground">{bolum.ad}</span>
|
||||||
</span>
|
) : (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href={`/bolum/${slug}`}
|
||||||
|
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
>
|
||||||
|
{bolum.ad}
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="size-3.5" aria-hidden />
|
||||||
|
<span className="text-foreground">Sayfa {sayfa}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header className="border-b pb-7 sm:pb-9">
|
<header className="border-b pb-7 sm:pb-9">
|
||||||
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||||
{bolum.ad}
|
{bolum.ad}
|
||||||
|
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||||
|
{!ilkSayfa && aralik ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<span className="mt-2 block text-lg font-semibold tracking-normal text-muted-foreground sm:text-2xl">
|
||||||
|
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
{ilkSayfa ? (
|
||||||
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
|
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele
|
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
|
||||||
{!ilkSayfa ? ` — sayfa ${sayfa}` : ""}.
|
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
|
||||||
</p>
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||||
|
{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 hover:underline"
|
||||||
|
>
|
||||||
|
ilk sayfada
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
<section aria-labelledby="universite-programlari" className="mt-8">
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
|
|||||||
/** Programatik tablo sayfaları için satır dilimi boyutu. */
|
/** Programatik tablo sayfaları için satır dilimi boyutu. */
|
||||||
export const SAYFA_BOYU = 50;
|
export const SAYFA_BOYU = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Düz listenin `sayfa` numaralı dilimi. Metadata (title/description) ile sayfa
|
||||||
|
* gövdesi aynı satırları anlatsın diye dilim hesabı tek yerde durur.
|
||||||
|
*/
|
||||||
|
export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] {
|
||||||
|
return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Statik path tabanlı sayfalama navigasyonu. 1. sayfa taban URL'in
|
* Statik path tabanlı sayfalama navigasyonu. 1. sayfa taban URL'in
|
||||||
* kendisidir (/bolum/x), sonrakiler /bolum/x/sayfa/2 ... — hepsi gerçek
|
* kendisidir (/bolum/x), sonrakiler /bolum/x/sayfa/2 ... — hepsi gerçek
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "@/lib/seo";
|
} from "@/lib/seo";
|
||||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
import { Sayfalama, SAYFA_BOYU, sayfaDilimi } from "./sayfalama";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||||
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||||
@@ -51,13 +51,65 @@ export function uniSayfaStaticParams(): { slug: string; no: string }[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return min === max
|
||||||
|
? `${adlar[0]} (taban sırası ${sayi(min)})`
|
||||||
|
: `${adlar[0]} (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 {
|
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||||
const uni = getUniversiteBySlug(slug);
|
const uni = getUniversiteBySlug(slug);
|
||||||
if (!uni) return {};
|
if (!uni) return {};
|
||||||
if (sayfa > 1) {
|
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({
|
return sayfaMetadata({
|
||||||
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
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.`,
|
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}`,
|
path: `/universite/${slug}/sayfa/${sayfa}`,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: ustSegmentOgGorseli(
|
images: ustSegmentOgGorseli(
|
||||||
@@ -84,14 +136,11 @@ export function UniversiteIcerik({
|
|||||||
const uni = getUniversiteBySlug(slug);
|
const uni = getUniversiteBySlug(slug);
|
||||||
if (!uni) notFound();
|
if (!uni) notFound();
|
||||||
|
|
||||||
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
|
const sayfaDilim = uniSayfaDilimi(uni, sayfa);
|
||||||
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
|
const { tumProgramlar, dilim } = sayfaDilim;
|
||||||
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
|
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
|
||||||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||||||
const dilim = tumProgramlar.slice(
|
const birimler = fakulteOzeti(sayfaDilim);
|
||||||
(sayfa - 1) * SAYFA_BOYU,
|
|
||||||
sayfa * SAYFA_BOYU,
|
|
||||||
);
|
|
||||||
const gruplar: {
|
const gruplar: {
|
||||||
fakulte: string;
|
fakulte: string;
|
||||||
programlar: (Program & { bolumSlug: string | null })[];
|
programlar: (Program & { bolumSlug: string | null })[];
|
||||||
@@ -263,7 +312,9 @@ export function UniversiteIcerik({
|
|||||||
{sayi(tumProgramlar.length)} programın{" "}
|
{sayi(tumProgramlar.length)} programın{" "}
|
||||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||||
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
|
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
|
||||||
satırları, fakülte sırasıyla). Üniversite özeti ve istatistikler{" "}
|
satırları, fakülte sırasıyla).
|
||||||
|
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite
|
||||||
|
özeti ve istatistikler{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${slug}`}
|
href={`/universite/${slug}`}
|
||||||
className="font-medium text-primary hover:underline"
|
className="font-medium text-primary hover:underline"
|
||||||
|
|||||||
Reference in New Issue
Block a user