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:
bilalgursen
2026-09-21 09:22:59 +03:00
parent e1bb4829fd
commit 4dfb24bb13
3 changed files with 182 additions and 24 deletions

View File

@@ -18,7 +18,7 @@ import {
} from "@/lib/seo";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
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 { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
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 {
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: `${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}`,
openGraph: {
images: ustSegmentOgGorseli(
@@ -84,14 +136,11 @@ export function UniversiteIcerik({
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 sayfaDilim = uniSayfaDilimi(uni, sayfa);
const { tumProgramlar, dilim } = sayfaDilim;
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
const dilim = tumProgramlar.slice(
(sayfa - 1) * SAYFA_BOYU,
sayfa * SAYFA_BOYU,
);
const birimler = fakulteOzeti(sayfaDilim);
const gruplar: {
fakulte: string;
programlar: (Program & { bolumSlug: string | null })[];
@@ -263,7 +312,9 @@ export function UniversiteIcerik({
{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{" "}
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"