Update routing in next.config.ts to redirect "/tercih-robotu" to the hero form, add simple-icons dependency in package.json, and enhance database indexing for improved query performance. Remove unused SVG files and update metadata across various pages for better SEO and clarity.
All checks were successful
Deploy / deploy (push) Successful in 14m14s

This commit is contained in:
bilalgursen
2026-08-01 17:28:38 +03:00
parent 569b9e5da2
commit 8cf0c3267d
76 changed files with 5050 additions and 255 deletions

View File

@@ -1,9 +1,17 @@
import { NextResponse, type NextRequest } from "next/server";
import { PUAN_TURLERI, rankWindowFacets, type PuanTuruKey } from "@/lib/db";
import {
PUAN_TURLERI,
dilimAra,
rankWindowFacets,
type DilimKey,
type PuanTuruKey,
} from "@/lib/db";
import { KATEGORILER } from "@/lib/kategoriler";
// Ana sayfadaki sihirbaz modalı için: sıralamaya uygun kategori/il/üniversite
// tipi facet'leri. /sonuc bu veriyi server component'te doğrudan alır; burada
// client'tan sıra girildikten sonra çekilir.
// Sihirbaz modalı için: sıralamaya uygun kategori/il/üniversite tipi
// facet'leri. Kademeli filtre: `kategori` (tekrarlı) il adımını,
// `kategori` + `il` (tekrarlı) üniversite tipi adımını süzer — bir önceki
// adımın seçimi bir sonrakinin seçeneklerini daraltır (bkz. rankWindowFacets).
export function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const sira = Number.parseInt(searchParams.get("sira") ?? "", 10);
@@ -18,5 +26,31 @@ export function GET(request: NextRequest) {
);
}
return NextResponse.json({ facetler: rankWindowFacets(sira, tur) });
const kategoriler = [
...new Set(
searchParams
.getAll("kategori")
.filter((k) => (KATEGORILER as readonly string[]).includes(k)),
),
];
const iller = [
...new Set(searchParams.getAll("il").filter((i) => i.trim().length > 0)),
].slice(0, 5);
// ?dilimler=1: /meraklisina'daki canlı dilim şeridi için dilim başına
// toplam sayılar da eklenir (limit 0 → yalnızca COUNT çalışır)
const dilimToplam =
searchParams.get("dilimler") === "1"
? Object.fromEntries(
(["hayal", "dengeli", "garanti"] as DilimKey[]).map((d) => [
d,
dilimAra(sira, tur, d, { limit: 0 }).toplam,
]),
)
: undefined;
return NextResponse.json({
facetler: rankWindowFacets(sira, tur, { kategoriler, iller }),
...(dilimToplam ? { dilimToplam } : {}),
});
}

View File

@@ -0,0 +1,86 @@
import { NextResponse, type NextRequest } from "next/server";
import { getAllBolumler, getAllUniversiteler } from "@/lib/katalog";
import { trBaslikDuzeni } from "@/lib/slug";
const SONUC_SINIRI = 6;
function normalize(metin: string): string {
return metin
.toLocaleLowerCase("tr-TR")
.replace(/ı/g, "i")
.replace(/ş/g, "s")
.replace(/ğ/g, "g")
.replace(/ü/g, "u")
.replace(/ö/g, "o")
.replace(/ç/g, "c")
.replace(/â/g, "a")
.replace(/î/g, "i")
.replace(/û/g, "u")
.normalize("NFKD")
.replace(/[̀-ͯ]/g, "");
}
function eslesmePuani(ad: string, sorgu: string): number {
const normalAd = normalize(ad);
if (normalAd === sorgu) return 0;
if (normalAd.startsWith(sorgu)) return 1;
if (normalAd.split(/\s+/).some((kelime) => kelime.startsWith(sorgu))) return 2;
return 3;
}
export function GET(request: NextRequest) {
const hamSorgu = request.nextUrl.searchParams.get("q")?.trim() ?? "";
const sorgu = normalize(hamSorgu).slice(0, 80);
if (sorgu.length < 2) {
return NextResponse.json({ bolumler: [], universiteler: [] });
}
const bolumler = getAllBolumler()
.filter((bolum) => normalize(bolum.ad).includes(sorgu))
.sort(
(a, b) =>
eslesmePuani(a.ad, sorgu) - eslesmePuani(b.ad, sorgu) ||
b.programSayisi - a.programSayisi,
)
.slice(0, SONUC_SINIRI)
.map((bolum) => ({
href: `/bolum/${bolum.slug}`,
ad: bolum.ad,
programSayisi: bolum.programSayisi,
enIyiSira: bolum.enIyiSira,
seviye:
bolum.lisans && bolum.onlisans
? "Lisans ve önlisans"
: bolum.lisans
? "Lisans"
: "Önlisans",
}));
const universiteler = getAllUniversiteler()
.filter((universite) => normalize(universite.ad).includes(sorgu))
.sort(
(a, b) =>
eslesmePuani(a.ad, sorgu) - eslesmePuani(b.ad, sorgu) ||
b.programSayisi - a.programSayisi,
)
.slice(0, SONUC_SINIRI)
.map((universite) => ({
href: `/universite/${universite.slug}`,
ad: universite.ad,
il: universite.il ? trBaslikDuzeni(universite.il) : null,
tur:
universite.unitur === "DEVLET"
? "Devlet"
: universite.unitur &&
["VAKIF", "VAKIF MYO"].includes(universite.unitur)
? "Vakıf"
: universite.unitur
? "KKTC / Yurt dışı"
: null,
programSayisi: universite.programSayisi,
fakulteSayisi: universite.fakulteSayisi,
}));
return NextResponse.json({ bolumler, universiteler });
}

View File

@@ -0,0 +1,43 @@
import { NextResponse, type NextRequest } from "next/server";
import {
PUAN_TURLERI,
dilimAra,
type DilimKey,
type PuanTuruKey,
} from "@/lib/db";
const SAYFA_BOYU = 20;
const DILIMLER: DilimKey[] = ["hayal", "dengeli", "garanti"];
// Sonuç sayfasındaki ham tablonun "daha fazla göster" sayfalaması: dilim
// başına SAYFA_BOYU program, yakından uzağa sıralı. İlk sayfa server
// component'te render edilir (bkz. sonuc/page.tsx); devamı buradan akar.
export function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const sira = Number.parseInt(searchParams.get("sira") ?? "", 10);
const turParam = searchParams.get("tur") ?? "say";
const tur: PuanTuruKey =
turParam in PUAN_TURLERI ? (turParam as PuanTuruKey) : "say";
const dilimParam = searchParams.get("dilim") ?? "";
const offset = Number.parseInt(searchParams.get("offset") ?? "0", 10);
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
return NextResponse.json(
{ error: "Geçerli bir sıralama gerekli." },
{ status: 400 },
);
}
if (!DILIMLER.includes(dilimParam as DilimKey)) {
return NextResponse.json({ error: "Geçersiz dilim." }, { status: 400 });
}
if (!Number.isFinite(offset) || offset < 0 || offset > 100_000) {
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
}
const { programlar, toplam } = dilimAra(sira, tur, dilimParam as DilimKey, {
limit: SAYFA_BOYU,
offset,
});
return NextResponse.json({ programlar, toplam });
}

View File

@@ -0,0 +1,129 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react";
import { getBolumBySlug, uniSlugFromAd } from "@/lib/katalog";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { Sayfalama, SAYFA_BOYU } from "@/components/sayfalama";
import { BolumProgramListesi } from "@/components/bolum-program-listesi";
const sayi = (n: number) => n.toLocaleString("tr-TR");
export function bolumToplamSayfa(programSayisi: number): number {
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
}
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
const bolum = getBolumBySlug(slug);
if (!bolum) return {};
const { stats } = bolum;
if (sayfa > 1) {
return {
title: `${bolum.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
description: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
alternates: { canonical: `/bolum/${slug}/sayfa/${sayfa}` },
};
}
const enIyi =
stats.minSira != null
? ` En iyi taban sıralaması ${sayi(stats.minSira)}.`
: "";
return {
title: `${bolum.ad} Taban Puanları ve Başarı Sıralamaları 2026`,
description: `${bolum.ad} 2025 yerleştirme verileri: ${stats.toplam} programın taban puanı ve sıralaması, ${stats.devlet} devlet / ${stats.vakif} vakıf.${enIyi} 2026 tercih rehberi.`,
alternates: { canonical: `/bolum/${slug}` },
};
}
export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
const bolum = getBolumBySlug(slug);
if (!bolum) notFound();
const tumProgramlar = [
...bolum.lisansProgramlari,
...bolum.onlisansProgramlari,
];
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
const dilim = tumProgramlar.slice(
(sayfa - 1) * SAYFA_BOYU,
sayfa * SAYFA_BOYU,
);
const ilkSayfa = sayfa === 1;
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
return (
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
<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="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
>
<Link
href="/"
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Ana sayfa
</Link>
<ChevronRight className="size-3.5" aria-hidden />
<Link
href="/bolumler"
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Bölümler
</Link>
<ChevronRight className="size-3.5" aria-hidden />
<span className="text-foreground">
{ilkSayfa ? bolum.ad : `Sayfa ${sayfa}`}
</span>
</nav>
<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">
{bolum.ad}
</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
puanlarını, başarı sıralamalarını ve kontenjanlarını incele
{!ilkSayfa ? ` — sayfa ${sayfa}` : ""}.
</p>
</header>
<section aria-labelledby="universite-programlari" className="mt-8">
<div className="mb-4 flex items-end justify-between gap-4">
<h2
id="universite-programlari"
className="font-heading text-xl font-bold text-foreground sm:text-2xl"
>
Üniversite programları
</h2>
<span className="shrink-0 text-sm text-muted-foreground">
{sayi(tumProgramlar.length)} program
</span>
</div>
<BolumProgramListesi
programlar={dilim.map((program) => ({
...program,
universiteSlug: uniSlugFromAd(program.universite),
}))}
bazAd={bolum.ad}
/>
</section>
<Sayfalama
tabanYol={`/bolum/${slug}`}
sayfa={sayfa}
toplamSayfa={toplamSayfa}
/>
</main>
);
}

View File

@@ -0,0 +1,28 @@
import type { Metadata } from "next";
import { getAllBolumSlugs } from "@/lib/katalog";
import { BolumIcerik, bolumSayfaMetadata } from "./bolum-icerik";
// Tüm bölümler build sırasında bilinir (veri ancak ingest + deploy ile
// değişir); bilinmeyen slug getBolumBySlug'dan null dönüp notFound()'a düşer.
// (dynamicParams cacheComponents ile uyumsuz olduğundan kullanılamıyor.)
export function generateStaticParams() {
return getAllBolumSlugs().map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
return bolumSayfaMetadata(slug, 1);
}
export default async function BolumPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <BolumIcerik slug={slug} sayfa={1} />;
}

View File

@@ -0,0 +1,47 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllBolumler } from "@/lib/katalog";
import {
BolumIcerik,
bolumSayfaMetadata,
bolumToplamSayfa,
} from "../../bolum-icerik";
// Yalnızca 1'den fazla sayfası olan bölümler için 2..N sayfaları üretilir;
// 1. sayfa taban rota (/bolum/[slug]) olduğundan burada üretilmez.
export function generateStaticParams() {
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),
}));
});
}
function sayfaNo(no: string): number | null {
const n = Number(no);
return Number.isInteger(n) && n >= 2 ? n : null;
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; no: string }>;
}): Promise<Metadata> {
const { slug, no } = await params;
const n = sayfaNo(no);
if (n == null) return {};
return bolumSayfaMetadata(slug, n);
}
export default async function BolumSayfaPage({
params,
}: {
params: Promise<{ slug: string; no: string }>;
}) {
const { slug, no } = await params;
const n = sayfaNo(no);
if (n == null) notFound();
return <BolumIcerik slug={slug} sayfa={n} />;
}

123
src/app/bolumler/page.tsx Normal file
View File

@@ -0,0 +1,123 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getAllBolumler, type BolumOzet } from "@/lib/katalog";
import { KATEGORILER } from "@/lib/kategoriler";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026",
description:
"Üniversite bölümlerinin 2025 yerleştirme taban puanları ve başarı sıralamaları: lisans ve önlisans programları kategori kategori, 2026 tercihleri için.",
alternates: { canonical: "/bolumler" },
};
const sayi = (n: number) => n.toLocaleString("tr-TR");
function BolumLinkleri({ bolumler }: { bolumler: BolumOzet[] }) {
return (
<ul className="mt-3 grid gap-x-6 gap-y-1.5 sm:grid-cols-2 lg:grid-cols-3">
{bolumler.map((b) => (
<li key={b.slug}>
<Link
href={`/bolum/${b.slug}`}
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 hover:text-primary"
>
<span className="group-hover:underline">{b.ad}</span>
<span className="text-xs text-slate-400">
{sayi(b.programSayisi)}
</span>
</Link>
</li>
))}
</ul>
);
}
function KategoriGruplari({
bolumler,
seviye,
}: {
bolumler: BolumOzet[];
seviye: "lisans" | "onlisans";
}) {
const kalanlar = new Set(bolumler.map((b) => b.slug));
const gruplar = KATEGORILER.map((kategori) => {
const uyeler = bolumler.filter((b) =>
(b.kategoriler as readonly string[]).includes(kategori),
);
for (const b of uyeler) kalanlar.delete(b.slug);
return { kategori: kategori as string, uyeler };
}).filter((g) => g.uyeler.length > 0);
const diger = bolumler.filter((b) => kalanlar.has(b.slug));
if (diger.length > 0) gruplar.push({ kategori: "Diğer", uyeler: diger });
return (
<>
{gruplar.map(({ kategori, uyeler }) => (
<section key={`${seviye}-${kategori}`} className="mt-8">
<h3 className="font-heading text-lg font-bold text-slate-900">
{kategori}
<span className="ml-2 text-xs font-normal text-slate-400">
{sayi(uyeler.length)} bölüm
</span>
</h3>
<BolumLinkleri bolumler={uyeler} />
</section>
))}
</>
);
}
export default function BolumlerPage() {
const hepsi = getAllBolumler();
const lisans = hepsi.filter((b) => b.lisans);
const onlisans = hepsi.filter((b) => b.onlisans && !b.lisans);
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" },
])}
/>
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026
</h1>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{sayi(hepsi.length)} bölümün 2025 YKS yerleştirme verileri: taban
puanları, başarı sıralamaları, kontenjanlar ve devlet/vakıf dağılımı.
Bölüme tıklayınca o bölümü sunan tüm üniversitelerin tablosunu
görürsün. Veriler YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026
tercihleri için rehber niteliğindedir.
</p>
<PagePixelDivider seed={29} className="mt-8" />
<section className="mt-8">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Lisans Bölümleri (4 Yıllık)
</h2>
<KategoriGruplari bolumler={lisans} seviye="lisans" />
</section>
<div className="mt-12">
<CtaSiraForm />
</div>
<section className="mt-12">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Önlisans Bölümleri (2 Yıllık TYT)
</h2>
<KategoriGruplari bolumler={onlisans} seviye="onlisans" />
</section>
</main>
<SiteFooter />
</>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,21 +0,0 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -7,7 +7,8 @@ import { GirisForm } from "./giris-form";
import { GirisBaslik } from "./giris-baslik";
export const metadata: Metadata = {
title: "Giriş — KolayTercih",
title: "Giriş",
robots: { index: false, follow: true },
};
export default async function GirisPage({

View File

@@ -4,7 +4,10 @@ import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Gizlilik ve KVKK — KolayTercih",
title: "Gizlilik ve KVKK",
description:
"KolayTercih gizlilik politikası ve KVKK aydınlatma metni: hangi veriler toplanır, nasıl işlenir ve haklarınız nelerdir.",
alternates: { canonical: "/gizlilik" },
};
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.

View File

@@ -4,7 +4,10 @@ import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "İletişim — KolayTercih",
title: "İletişim",
description:
"KolayTercih destek ekibine ulaş: sorular, geri bildirim ve iade talepleri için iletişim kanalları.",
alternates: { canonical: "/iletisim" },
};
export default function IletisimPage() {

View File

@@ -4,7 +4,10 @@ import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Kullanım ve Satış Koşulları — KolayTercih",
title: "Kullanım ve Satış Koşulları",
description:
"KolayTercih kullanım ve satış koşulları: hizmet kapsamı, ödeme, iade politikası ve sorumluluk sınırları.",
alternates: { canonical: "/kosullar" },
};
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.

View File

@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Cal_Sans, Geist_Mono, Bricolage_Grotesque } from "next/font/google";
import localFont from "next/font/local";
import { Toaster } from "@/components/ui/sonner";
@@ -9,6 +9,7 @@ import { HeaderGate } from "@/components/header-gate";
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
import { DevPanel } from "@/components/dev/dev-panel";
import { ScrollFlow } from "@/components/scroll-flow";
import { JsonLd, organizationJsonLd, webSiteJsonLd, SITE_URL } from "@/lib/seo";
import "./globals.css";
const calSans = Cal_Sans({
@@ -42,9 +43,29 @@ const bricolage = Bricolage_Grotesque({
});
export const metadata: Metadata = {
title: "KolayTercih — Tercih Danışmanı",
metadataBase: new URL(SITE_URL),
title: {
default: "KolayTercih — Yapay Zekâ Destekli YKS Tercih Danışmanı",
template: "%s — KolayTercih",
},
description:
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmanın onda bir fiyatına, yapay zekâ destekli tercih danışmanlığı.",
applicationName: "KolayTercih",
openGraph: {
type: "website",
locale: "tr_TR",
siteName: "KolayTercih",
url: "/",
title: "KolayTercih — Yapay Zekâ Destekli YKS Tercih Danışmanı",
description:
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. Yapay zekâ destekli tercih danışmanlığı.",
},
twitter: { card: "summary_large_image" },
robots: { index: true, follow: true },
};
export const viewport: Viewport = {
themeColor: "#f8fafc",
};
export default function RootLayout({
@@ -55,9 +76,13 @@ export default function RootLayout({
return (
<html
lang="tr"
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} h-full antialiased`}
// `h-full` VERME — Lenis içerik yüksekliğini <html>'e bağlı ResizeObserver
// ile izliyor; html sabit %100 kalınca lazy yüklenen içerik sonrası limit
// bayatlıyor ve sayfa sonunda scroll kilitleniyor. html auto yükseklikte
// içerikle büyümeli, viewport tabanı body'deki min-h-dvh'den gelir.
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`}
>
<body className="min-h-full flex flex-col bg-slate-50">
<body className="min-h-dvh flex flex-col bg-slate-50">
{/* Site geneli progressive blur — üst ve alt */}
<div className="pointer-events-none fixed inset-x-0 top-0 z-40 h-28 print:hidden">
<ProgressiveBlur
@@ -87,6 +112,8 @@ export default function RootLayout({
<Toaster position="top-center" />
{/* Dev süperadmin paneli — prod build'de hiç render edilmez */}
{process.env.NODE_ENV !== "production" ? <DevPanel /> : null}
<JsonLd data={organizationJsonLd()} />
<JsonLd data={webSiteJsonLd()} />
</body>
</html>
);

View File

@@ -31,7 +31,8 @@ import { SohbetClient, type Mesaj } from "./sohbet-client";
import { RevizyonKutusu } from "./revizyon-kutusu";
export const metadata: Metadata = {
title: "Listem — KolayTercih",
title: "Listem",
robots: { index: false, follow: false },
};
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {

View File

@@ -0,0 +1,640 @@
"use client";
// /meraklisina'nın canlı anlatım katmanı: kullanıcı kendi sıralamasını girer,
// dilim şeridi ve kademeli filtre hunisi o sıralamanın GERÇEK sayılarıyla
// güncellenir. Provider sayfadaki metin bölümlerini (RSC children) sarar;
// görseller context'ten okur, böylece tek girişle iki diyagram birden yaşar.
import {
createContext,
useContext,
useEffect,
useRef,
useState,
type ReactNode,
} from "react";
import type { DilimKey, SihirbazFacetleri } from "@/lib/db";
const TUR_SECENEKLERI = [
{ deger: "say", etiket: "Sayısal" },
{ deger: "ea", etiket: "Eşit Ağırlık" },
{ deger: "soz", etiket: "Sözel" },
{ deger: "dil", etiket: "Dil" },
{ deger: "tyt", etiket: "TYT" },
] as const;
export type DemoVeri = {
facetler: SihirbazFacetleri;
dilimToplam: Record<DilimKey, number>;
};
type DemoDurum = {
sira: number;
tur: string;
veri: DemoVeri;
yukleniyor: boolean;
// Huni seçimleri ve kademeli facet cevapları
kategori: string | null;
il: string | null;
tip: "devlet" | "vakif" | null;
kategoriFacet: SihirbazFacetleri | null; // ?kategori= cevabı (iller buradan)
ilFacet: SihirbazFacetleri | null; // ?kategori&il cevabı (tipler buradan)
kategoriSec: (k: string | null) => void;
ilSec: (i: string | null) => void;
tipSec: (t: "devlet" | "vakif" | null) => void;
siraDegistir: (ham: string) => void;
turDegistir: (t: string) => void;
siraMetni: string;
};
const DemoContext = createContext<DemoDurum | null>(null);
function useDemo(): DemoDurum {
const ctx = useContext(DemoContext);
if (!ctx) throw new Error("Demo bileşenleri SiralamaDemo içinde kullanılmalı");
return ctx;
}
async function facetGetir(
sira: number,
tur: string,
secim: { kategori?: string; il?: string; dilimler?: boolean },
signal: AbortSignal,
): Promise<{ facetler: SihirbazFacetleri; dilimToplam?: Record<DilimKey, number> } | null> {
const params = new URLSearchParams({ sira: String(sira), tur });
if (secim.kategori) params.append("kategori", secim.kategori);
if (secim.il) params.append("il", secim.il);
if (secim.dilimler) params.set("dilimler", "1");
try {
const res = await fetch(`/api/facetler?${params}`, { signal });
if (!res.ok) return null;
return (await res.json()) as {
facetler: SihirbazFacetleri;
dilimToplam?: Record<DilimKey, number>;
};
} catch {
return null;
}
}
export function SiralamaDemo({
baslangicSira,
baslangicVeri,
children,
}: {
baslangicSira: number;
baslangicVeri: DemoVeri;
children: ReactNode;
}) {
const [siraMetni, setSiraMetni] = useState(
baslangicSira.toLocaleString("tr-TR"),
);
const [sira, setSira] = useState(baslangicSira);
const [tur, setTur] = useState("say");
const [veri, setVeri] = useState(baslangicVeri);
const [yukleniyor, setYukleniyor] = useState(false);
const [kategori, setKategori] = useState<string | null>(null);
const [il, setIl] = useState<string | null>(null);
const [tip, setTip] = useState<"devlet" | "vakif" | null>(null);
const [kategoriFacet, setKategoriFacet] = useState<SihirbazFacetleri | null>(
null,
);
const [ilFacet, setIlFacet] = useState<SihirbazFacetleri | null>(null);
const zamanlayici = useRef<ReturnType<typeof setTimeout> | null>(null);
// Sıralama/tür değişti → ana veriyi tazele, huni seçimlerini sıfırla
useEffect(() => {
if (sira === baslangicSira && tur === "say" && veri === baslangicVeri) {
return; // ilk render: sunucudan gelen veri zaten güncel
}
const ctrl = new AbortController();
// eslint-disable-next-line react-hooks/set-state-in-effect -- fetch başlangıç işareti
setYukleniyor(true);
setKategori(null);
setIl(null);
setTip(null);
setKategoriFacet(null);
setIlFacet(null);
facetGetir(sira, tur, { dilimler: true }, ctrl.signal)
.then((cevap) => {
if (!cevap?.dilimToplam) return;
setVeri({ facetler: cevap.facetler, dilimToplam: cevap.dilimToplam });
})
.finally(() => setYukleniyor(false));
return () => ctrl.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sira, tur]);
// Kategori seçildi → o kategorinin illerini getir
useEffect(() => {
if (!kategori) return;
const ctrl = new AbortController();
facetGetir(sira, tur, { kategori }, ctrl.signal).then((cevap) => {
if (cevap) setKategoriFacet(cevap.facetler);
});
return () => ctrl.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [kategori]);
// İl seçildi → kategori+il ile tipleri getir
useEffect(() => {
if (!kategori || !il) return;
const ctrl = new AbortController();
facetGetir(sira, tur, { kategori, il }, ctrl.signal).then((cevap) => {
if (cevap) setIlFacet(cevap.facetler);
});
return () => ctrl.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [kategori, il]);
function siraDegistir(ham: string) {
const rakamlar = ham.replace(/\D/g, "").slice(0, 7);
setSiraMetni(rakamlar ? Number(rakamlar).toLocaleString("tr-TR") : "");
if (zamanlayici.current) clearTimeout(zamanlayici.current);
const deger = Number(rakamlar);
if (!deger || deger < 1 || deger > 4_000_000) return;
zamanlayici.current = setTimeout(() => setSira(deger), 450);
}
const durum: DemoDurum = {
sira,
tur,
veri,
yukleniyor,
kategori,
il,
tip,
kategoriFacet,
ilFacet,
kategoriSec: (k) => {
setKategori(k);
setIl(null);
setTip(null);
setKategoriFacet(null);
setIlFacet(null);
},
ilSec: (i) => {
setIl(i);
setTip(null);
setIlFacet(null);
},
tipSec: setTip,
siraDegistir,
turDegistir: setTur,
siraMetni,
};
return <DemoContext.Provider value={durum}>{children}</DemoContext.Provider>;
}
/** Sıralama girişi + puan türü — iki diyagramı birden süren tek kumanda. */
export function DemoKumanda() {
const { siraMetni, siraDegistir, tur, turDegistir, sira, yukleniyor } =
useDemo();
return (
<div className="rounded-2xl border border-primary/20 bg-primary/5 p-5">
<label
htmlFor="demo-sira"
className="font-heading text-sm font-bold text-slate-900"
>
Kendi sıralamanı gir, aşağıdaki görseller sana göre değişsin
</label>
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center">
<input
id="demo-sira"
inputMode="numeric"
autoComplete="off"
value={siraMetni}
onChange={(e) => siraDegistir(e.target.value)}
placeholder="ör. 85.000"
className="h-11 w-full rounded-xl border border-slate-200 bg-white px-4 text-base font-medium tabular-nums outline-none transition-colors focus:border-primary sm:w-44"
/>
<div
role="radiogroup"
aria-label="Puan türü"
className="flex flex-wrap gap-1.5"
>
{TUR_SECENEKLERI.map((t) => (
<button
key={t.deger}
type="button"
role="radio"
aria-checked={tur === t.deger}
onClick={() => turDegistir(t.deger)}
className={
tur === t.deger
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white"
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
}
>
{t.etiket}
</button>
))}
</div>
</div>
<p
className={`mt-2 text-xs text-slate-500 ${yukleniyor ? "animate-pulse" : ""}`}
aria-live="polite"
>
{yukleniyor
? "Hesaplanıyor…"
: `Aşağıdaki tüm sayılar ${sira.toLocaleString("tr-TR")}. sıradaki bir aday için gerçek YÖK Atlas verisidir.`}
</p>
</div>
);
}
/** Sıralama ekseni: girilen sıraya göre dilim sınırları ve gerçek adetler. */
export function DilimSeridiCanli() {
const { sira, veri, yukleniyor } = useDemo();
const t = veri.dilimToplam;
const dengeliUst = Math.round(sira * 1.4);
const fmt = (n: number) => n.toLocaleString("tr-TR");
return (
<figure className={`mt-6 ${yukleniyor ? "animate-pulse opacity-60" : ""}`}>
<svg
viewBox="0 0 640 168"
role="img"
aria-label={`Sıralama ekseni: ${fmt(sira)}. sıradaki aday için Hayal diliminde ${fmt(t.hayal)}, Dengeli diliminde ${fmt(t.dengeli)}, Garanti diliminde ${fmt(t.garanti)} program var.`}
className="w-full"
>
{/* eksen */}
<line
x1="20"
y1="86"
x2="620"
y2="86"
stroke="currentColor"
className="text-slate-300"
strokeWidth="2"
/>
<text x="20" y="150" className="fill-slate-400 text-[11px]">
1. sıra (en iyi)
</text>
<text
x="620"
y="150"
textAnchor="end"
className="fill-slate-400 text-[11px]"
>
taban sıralaması büyüdükçe
</text>
{/* hayal */}
<rect x="20" y="70" width="150" height="32" rx="4" className="fill-red-100" />
<text x="95" y="40" textAnchor="middle" className="fill-red-600 text-[12px] font-semibold">
Hayal
</text>
<text x="95" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
{fmt(t.hayal)} program
</text>
<text x="95" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
tabanı senden iyi
</text>
{/* aday işareti */}
<line x1="172" y1="24" x2="172" y2="112" stroke="currentColor" className="text-slate-900" strokeWidth="2" strokeDasharray="4 3" />
<text x="172" y="16" textAnchor="middle" className="fill-slate-900 text-[12px] font-bold">
SEN · {fmt(sira)}.
</text>
{/* dengeli */}
<rect x="174" y="70" width="120" height="32" rx="4" className="fill-amber-100" />
<text x="234" y="40" textAnchor="middle" className="fill-amber-600 text-[12px] font-semibold">
Dengeli
</text>
<text x="234" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
{fmt(t.dengeli)} program
</text>
<text x="234" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
{fmt(sira)}. {fmt(dengeliUst)}. arası taban
</text>
{/* garanti */}
<rect x="296" y="70" width="324" height="32" rx="4" className="fill-emerald-100" />
<text x="458" y="40" textAnchor="middle" className="fill-emerald-700 text-[12px] font-semibold">
Garanti
</text>
<text x="458" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
{fmt(t.garanti)} program
</text>
<text x="458" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
{fmt(dengeliUst + 1)}. sonrası sonu yok, sayfaladıkça uzar
</text>
</svg>
<figcaption className="mt-2 text-xs text-slate-500">
Liste her dilimde{" "}
<span className="font-medium text-slate-700">
sana en yakın tabandan uzağa doğru
</span>{" "}
sıralanır; &ldquo;Daha fazla göster&rdquo; dedikçe uzaktakiler gelir.
</figcaption>
</figure>
);
}
function HuniCubugu({
etiket,
adet,
oran,
}: {
etiket: string;
adet: number;
oran: number;
}) {
return (
<div className="min-w-0 flex-1">
<div
className="flex h-9 min-w-36 items-center justify-between gap-2 rounded-lg bg-primary/85 px-3 transition-[width] duration-300"
style={{ width: `${Math.min(Math.max(oran * 100, 36), 100)}%` }}
>
<span className="truncate text-xs font-medium text-white">{etiket}</span>
<span className="shrink-0 text-xs font-bold tabular-nums text-white">
{adet.toLocaleString("tr-TR")}
</span>
</div>
</div>
);
}
function SecimCipleri<T extends { anahtar: string; etiket: string; adet: number }>({
secenekler,
secili,
onSec,
}: {
secenekler: T[];
secili: string | null;
onSec: (anahtar: string | null) => void;
}) {
return (
<div className="flex flex-wrap gap-1.5">
{secenekler.map((s) => (
<button
key={s.anahtar}
type="button"
aria-pressed={secili === s.anahtar}
onClick={() => onSec(secili === s.anahtar ? null : s.anahtar)}
className={
secili === s.anahtar
? "cursor-pointer rounded-full border border-orange-500 bg-orange-500 px-3 py-1.5 text-xs font-medium text-white"
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
}
>
{s.etiket}
<span className="ml-1 text-[10px] opacity-70 tabular-nums">
{s.adet.toLocaleString("tr-TR")}
</span>
</button>
))}
</div>
);
}
// AI listesinin dilim iskeleti: üstte hayal, ortada dengeli, sonda garanti.
// Ürünün risk renkleriyle boyanır; sayılar prompt'taki kuralın orta değeri.
const LISTE_ISKELETI: { dilim: string; adet: number; renk: string }[] = [
{ dilim: "hayal", adet: 5, renk: "bg-red-400" },
{ dilim: "dengeli", adet: 13, renk: "bg-amber-400" },
{ dilim: "garanti", adet: 6, renk: "bg-emerald-500" },
];
/** Huninin finali: kalan havuzun yapay zekâya gidişi ve 24'lük listenin
iskeleti. Havuz 24'ün altına düşerse gevşetme kuralını canlı gösterir. */
function YapayZekaAsamasi({ havuz }: { havuz: number }) {
const dar = havuz < 24;
return (
<div className="border-t border-slate-100 pt-4">
<p className="text-xs font-semibold text-slate-500">
Sonra sıra yapay zekâda
</p>
<p className="mt-1.5 text-xs leading-5 text-slate-600">
Seçimlerinden geriye kalan{" "}
<span className="font-bold tabular-nums text-slate-900">
{havuz.toLocaleString("tr-TR")} programlık havuz
</span>{" "}
yapay zekâya gider; o da bu havuzdan tam 24 tercih seçer ve şu iskelete
oturtur:
</p>
{/* 24 hücrelik liste şeridi — renk + metin birlikte (renk tek başına
anlam taşımaz) */}
<div
role="img"
aria-label="24 tercihlik listenin yapısı: en üstte yaklaşık 5 hayal, ortada 13 dengeli, sonda 6 garanti tercih."
className="mt-3"
>
<div className="flex gap-1">
{LISTE_ISKELETI.flatMap((b) =>
Array.from({ length: b.adet }, (_, i) => (
<span
key={`${b.dilim}-${i}`}
className={`h-6 min-w-0 flex-1 rounded ${b.renk}`}
/>
)),
)}
</div>
<div className="mt-1.5 flex justify-between text-[11px] text-slate-500">
<span>
üstte 46 <span className="font-semibold text-red-600">hayal</span>
</span>
<span>
ortada 1214{" "}
<span className="font-semibold text-amber-600">dengeli</span>
</span>
<span>
sonda 57{" "}
<span className="font-semibold text-emerald-600">garanti</span>
</span>
</div>
</div>
<ul className="mt-3 space-y-1 text-xs leading-5 text-slate-600">
<li>
· Havuzda olmayan bir bölümü yazamaz listedeki her satırın kimliği
havuzla karşılaştırılır, uymayan liste yeniden ürettirilir.
</li>
<li>
· Her satıra tek cümlelik gerekçe ve bir risk notu ekler; sıraya neyi
neden koyduğu görünür.
</li>
<li>
· Önceliklerin ( garantisi, maaş, prestij) hangi programın öne
geçeceğini belirler.
</li>
<li>
· Havuz 24&apos;ün altına düşerse filtreler adım adım gevşetilir
önce il, sonra alan ve bu, listende ıkça belirtilir.
</li>
</ul>
{dar ? (
<p className="mt-3 rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
Bu seçimle havuz 24&apos;ün altına düştü. Gerçek sihirbaz bu durumda
filtreleri adım adım gevşetir önce il, sonra alan tercihi esnetilir
ve bunu listende ıkça belirtir; liste her zaman 24&apos;e
tamamlanır.
</p>
) : null}
</div>
);
}
/** Kademeli filtre hunisi: gerçek sihirbaz mantığının oynanabilir kopyası. */
export function HuniCanli() {
const {
veri,
yukleniyor,
kategori,
il,
tip,
kategoriFacet,
ilFacet,
kategoriSec,
ilSec,
tipSec,
} = useDemo();
const toplam = veri.facetler.toplam;
const kategoriAdet =
kategori != null
? (veri.facetler.kategoriler.find((k) => k.ad === kategori)?.adet ?? 0)
: null;
const ilAdet =
il != null && kategoriFacet
? (kategoriFacet.iller.find((i) => i.il === il)?.adet ?? 0)
: null;
const tipAdet =
tip != null && ilFacet
? (ilFacet.uniturler.find((u) => u.grup === tip)?.adet ?? 0)
: null;
const kategoriSecenekleri = veri.facetler.kategoriler.slice(0, 6).map((k) => ({
anahtar: k.ad,
etiket: k.ad,
adet: k.adet,
}));
const ilSecenekleri = (kategoriFacet?.iller ?? []).slice(0, 6).map((i) => ({
anahtar: i.il,
etiket: i.il.toLocaleLowerCase("tr-TR"),
adet: i.adet,
}));
const tipSecenekleri = (ilFacet?.uniturler ?? []).map((u) => ({
anahtar: u.grup,
etiket: u.grup === "devlet" ? "Devlet" : "Vakıf",
adet: u.adet,
}));
// Yapay zekâya giden havuz: huninin en derin (en son seçilmiş) katmanı
const havuz = tipAdet ?? ilAdet ?? kategoriAdet ?? toplam;
return (
<div
className={`mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-5 ${
yukleniyor ? "animate-pulse opacity-60" : ""
}`}
>
{/* Huni çubukları — her seçim bir çubuk daha ekler */}
<div className="space-y-2">
<div className="flex items-center gap-3">
<span className="w-6 shrink-0" />
<HuniCubugu
etiket="Sıralamanla açılan tüm programlar"
adet={toplam}
oran={1}
/>
</div>
{kategori != null && kategoriAdet != null ? (
<div className="flex items-center gap-3">
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
1.
</span>
<HuniCubugu
etiket={`${kategori}” seçince`}
adet={kategoriAdet}
oran={toplam > 0 ? kategoriAdet / toplam : 0}
/>
</div>
) : null}
{il != null && ilAdet != null ? (
<div className="flex items-center gap-3">
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
2.
</span>
<HuniCubugu
etiket={`+ “${il.toLocaleLowerCase("tr-TR")}” seçince`}
adet={ilAdet}
oran={toplam > 0 ? ilAdet / toplam : 0}
/>
</div>
) : null}
{tip != null && tipAdet != null ? (
<div className="flex items-center gap-3">
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
3.
</span>
<HuniCubugu
etiket={`+ “${tip === "devlet" ? "Devlet" : "Vakıf"}” seçince`}
adet={tipAdet}
oran={toplam > 0 ? tipAdet / toplam : 0}
/>
</div>
) : null}
</div>
{/* Seçim adımları — sihirbazın küçük, oynanabilir hali */}
<div className="space-y-3 border-t border-slate-100 pt-4">
<div>
<p className="mb-1.5 text-xs font-semibold text-slate-500">
1. Bir alan seç
</p>
<SecimCipleri
secenekler={kategoriSecenekleri}
secili={kategori}
onSec={kategoriSec}
/>
</div>
{kategori != null ? (
<div>
<p className="mb-1.5 text-xs font-semibold text-slate-500">
2. Bir il seç{" "}
<span className="font-normal text-slate-400">
(iller artık yalnızca {kategori} programlarını sayıyor)
</span>
</p>
{ilSecenekleri.length > 0 ? (
<SecimCipleri
secenekler={ilSecenekleri}
secili={il}
onSec={ilSec}
/>
) : (
<p className="text-xs text-slate-400">Yükleniyor</p>
)}
</div>
) : null}
{kategori != null && il != null ? (
<div>
<p className="mb-1.5 text-xs font-semibold text-slate-500">
3. Üniversite tipi seç{" "}
<span className="font-normal text-slate-400">
(sayılar hem alanı hem ili hesaba katıyor)
</span>
</p>
{tipSecenekleri.length > 0 ? (
<SecimCipleri
secenekler={tipSecenekleri}
secili={tip}
onSec={(t) => tipSec(t as "devlet" | "vakif" | null)}
/>
) : (
<p className="text-xs text-slate-400">Yükleniyor</p>
)}
</div>
) : null}
</div>
<YapayZekaAsamasi havuz={havuz} />
</div>
);
}

View File

@@ -0,0 +1,189 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
import { dilimAra, rankWindowFacets, type DilimKey } from "@/lib/db";
import {
DemoKumanda,
DilimSeridiCanli,
HuniCanli,
SiralamaDemo,
type DemoVeri,
} from "./demo";
export const metadata: Metadata = {
title: "Meraklısına: KolayTercih nasıl çalışır?",
alternates: { canonical: "/meraklisina" },
description:
"Sıralamanı girdiğinde arka planda ne oluyor? Dilimler, risk renkleri ve sihirbazın adım adım daralan filtreleri — öğrenci diliyle, kendi sıralamanla deneyebileceğin canlı görsellerle.",
};
/**
* Algoritmanın öğrenci diline çevrilmiş anlatımı. Teknik detay (SQL, çarpan
* mutfağı) bilerek verilmez; amaç güven: veri gerçek, kurallar şeffaf.
* Görseller canlıdır: kullanıcı sıralamasını girer, dilim şeridi ve filtre
* hunisi o sıralamanın gerçek sayılarıyla güncellenir (bkz. demo.tsx).
* Diyagramlar ürünün risk renk dilini kullanır (yeşil/sarı/kırmızı) ve her
* bölge metinle de etiketlenir — renk tek başına anlam taşımaz.
*/
// Demo ilk açılışta bu sıralamayla dolu gelir; SSR'da gerçek veriyle render
// edilir ki kullanıcı girmeden de sayfa anlamlı olsun.
const VARSAYILAN_SIRA = 100_000;
export default function MeraklisinaPage() {
const dilimToplam = Object.fromEntries(
(["hayal", "dengeli", "garanti"] as DilimKey[]).map((d) => [
d,
dilimAra(VARSAYILAN_SIRA, "say", d, { limit: 0 }).toplam,
]),
) as Record<DilimKey, number>;
const baslangicVeri: DemoVeri = {
facetler: rankWindowFacets(VARSAYILAN_SIRA, "say"),
dilimToplam,
};
return (
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<Parallax fromStart strength={10}>
<p className="text-sm font-semibold tracking-wide text-primary">
Meraklısına
</p>
<h1 className="mt-1 font-heading text-3xl font-bold">
Sıralamanı girince arka planda ne oluyor?
</h1>
<p className="mt-3 text-slate-600">
Sihir yok, kara kutu yok: KolayTercih&apos;in önerdiği her satır resmî
YÖK Atlas verisinden gelir ve hangi kurala göre önüne geldiği bellidir.
Bu sayfa o kuralları öğrenci diliyle anlatır üstelik görseller
canlı: kendi sıralamanı gir, sayılar sana göre değişsin.
</p>
</Parallax>
<PagePixelDivider seed={73} className="mt-6" />
<SiralamaDemo
baslangicSira={VARSAYILAN_SIRA}
baslangicVeri={baslangicVeri}
>
<Parallax
fromStart
strength={4}
className="mt-8 space-y-10 text-sm leading-7 text-slate-700"
>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
1. Her şey taban sıralamasıyla başlar
</h2>
<p className="mt-2">
Bir bölümün <strong>taban sıralaması</strong>, geçen yıl o bölüme
yerleşen <em>son</em>{" "}
öğrencinin Türkiye sıralamasıdır. Senin sıralaman bu sayıdan
küçükse (yani daha iyiysen), geçen yılın koşullarında o bölüme
yerleşebilirdin demektir. Sistemde her programın en güncel taban
sıralaması kayıtlıdır; bir bölümün 2025 verisi henüz oluşmamışsa
bir önceki yılınki kullanılır.
</p>
</section>
<DemoKumanda />
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
2. Programlar üç dilime ayrılır
</h2>
<p className="mt-2">
Sıralamanı girdiğinde tüm programlar senin konumuna göre üç kümeye
ayrılır: tabanı senden <strong>daha iyi</strong> olanlar{" "}
<span className="font-semibold text-red-600">Hayal</span> (şansını
denediğin satırlar), tabanı sıralamana{" "}
<strong>yakın</strong> olanlar{" "}
<span className="font-semibold text-amber-600">Dengeli</span>{" "}
(listenin bel kemiği sıralamanın yaklaşık yüzde kırk fazlasına
kadar pay bırakılır), tabanı{" "}
<strong>belirgin şekilde altında</strong> kalan her şey ise{" "}
<span className="font-semibold text-emerald-600">Garanti</span>{" "}
dilimidir.
</p>
<DilimSeridiCanli />
<p className="mt-4">
Garanti diliminin bir sonu yok: 87. sıradaki bir adayın da 500
bininci sıradaki bir adayın da ulaşabildiği <em>bütün</em>{" "}
programlar listededir. Liste sana en yakın tabandan başlar; aşağı
indikçe ve &ldquo;Daha fazla göster&rdquo;e bastıkça senden
uzaklaşan seçenekler gelir. Böylece ilk gördüklerin her zaman en
isabetli adaylardır ama hiçbir seçenek senden saklanmaz.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
3. Risk renkleri her yerde aynı dili konuşur
</h2>
<p className="mt-2">
Tabloda, haritada ve yapay zekâ listesinde gördüğün nokta ve
renkler tek bir kurala bağlıdır:{" "}
<span className="font-semibold text-emerald-600">yeşil</span>{" "}
tabanı sıralamanın belirgin altında (güvenli),{" "}
<span className="font-semibold text-amber-600">sarı</span>{" "}
sıralamana yakın (az riskli),{" "}
<span className="font-semibold text-red-600">kırmızı</span>{" "}
tabanı senden iyi (riskli). Renk hangi sayfada olursa olsun aynı
hesaptan çıkar; bir satır sekme değiştirdi diye renk değiştirmez.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
4. Sihirbaz süzer, yapay zekâ kurar
</h2>
<p className="mt-2">
&ldquo;Listemi oluştur&rdquo; sihirbazındaki üç adım birbirinden
bağımsız değildir. İlk adımda yalnızca{" "}
<strong>sıralamanla ulaşabildiğin</strong>{" "}
alanlar listelenir; ikinci adımdaki iller, seçtiğin alanlardaki
program sayısına göre yeniden hesaplanır; üçüncü adımdaki
devlet/vakıf sayıları da hem alan hem il seçimini hesaba katar.
Adımlar bitince elde kalan gerçek programlar bir{" "}
<strong>aday havuzu</strong>{" "}
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
verilir. Aşağıda kendin dene her seçimin havuzu nasıl
daralttığını ve havuzun 24&apos;lük listeye nasıl dönüştüğünü huni
canlı gösterir.
</p>
<HuniCanli />
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
5. Dürüst olalım: bu bir tahmin aracıdır
</h2>
<p className="mt-2">
Tüm bu hesaplar <strong>geçen yılların</strong>{" "}
yerleştirme sonuçlarına dayanır. Kontenjanlar, tercih davranışları
ve puanlar her yıl değişir; geçen yıl &ldquo;garanti&rdquo;
görünen bir bölüm bu yıl dolabilir. Bu yüzden satırlarda son beş
yılın taban seyrini de gösteririz çizgi yukarı tırmanıyorsa
bölüm zorlaşıyor demektir. KolayTercih karar <em>destek</em>{" "}
aracıdır; listenin son hâli ve sorumluluğu her zaman sana aittir.
</p>
</section>
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-center">
<p className="font-heading text-base font-bold text-slate-900">
Kuralları öğrendin şimdi kendi sıralamanla dene.
</p>
<Link
href="/"
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
>
Sıralamanı gir
<ArrowRight className="size-4" aria-hidden />
</Link>
</div>
</Parallax>
</SiralamaDemo>
</main>
);
}

View File

@@ -9,7 +9,8 @@ import { Button } from "@/components/ui/button";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Ödeme Sonucu — KolayTercih",
title: "Ödeme Sonucu",
robots: { index: false, follow: false },
};
export default async function OdemeSonucPage({

View File

@@ -0,0 +1 @@
KolayTercih — YKS tercihin, gerçek YÖK Atlas verisiyle. Sıralamanı gir, dengeli 24 tercihlik listeni al.

BIN
src/app/opengraph-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1,4 +1,5 @@
import {
ArrowUpRight,
BarChart3,
CheckCircle2,
Compass,
@@ -44,6 +45,17 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { Metadata } from "next";
import {
JsonLd,
faqPageJsonLd,
softwareApplicationJsonLd,
} from "@/lib/seo";
import { SiteFooter } from "@/components/site-footer";
export const metadata: Metadata = {
alternates: { canonical: "/" },
};
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0,
@@ -170,6 +182,8 @@ export default function Home() {
className="flex min-h-screen flex-col bg-slate-50 text-slate-900"
>
<main className="flex-1">
<JsonLd data={faqPageJsonLd(faqs)} />
<JsonLd data={softwareApplicationJsonLd()} />
{/* Hero */}
<section className="relative isolate overflow-hidden">
<div
@@ -293,7 +307,20 @@ export default function Home() {
<div className="mx-auto max-w-6xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
<SectionEyebrow>Nasıl çalışır?</SectionEyebrow>
<div className="flex flex-wrap items-center justify-center gap-2">
<SectionEyebrow>Nasıl çalışır?</SectionEyebrow>
{/* Kuralların şeffaf anlatımına köprü: /meraklisina */}
<Link
href="/meraklisina"
className="group inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] text-white transition-colors duration-200 hover:bg-orange-600"
>
Meraklısına
<ArrowUpRight
className="size-3.5 transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
aria-hidden
/>
</Link>
</div>
<h2 className="font-heading text-3xl font-bold sm:text-4xl">
Üç adımda listen hazır
</h2>
@@ -515,41 +542,7 @@ export default function Home() {
</section>
</main>
<footer className="pb-32 pt-4">
<div className="mx-auto max-w-6xl px-4 text-center text-sm leading-relaxed text-slate-500">
<p>
© 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir;
KolayTercih, ÖSYM veya YÖK ile bağlantılı değildir.
</p>
<p className="mt-2">
KolayTercih bir karar destek aracıdır, yerleşme garantisi vermez.
Tercih listenizin son hali ve başvuru sorumluluğu size aittir.
</p>
<nav
aria-label="Yasal sayfalar"
className="mt-4 flex items-center justify-center gap-4"
>
<Link
href="/gizlilik"
className="hover:text-slate-700 hover:underline"
>
Gizlilik & KVKK
</Link>
<Link
href="/kosullar"
className="hover:text-slate-700 hover:underline"
>
Kullanım & iade koşulları
</Link>
<Link
href="/iletisim"
className="hover:text-slate-700 hover:underline"
>
İletişim
</Link>
</nav>
</div>
</footer>
<SiteFooter />
</div>
);
}

View File

@@ -18,7 +18,10 @@ import { PagePixelDivider } from "@/components/pixel-decor";
import { SatinAlForm } from "./satin-al-form";
export const metadata: Metadata = {
title: "Tercih Dönemi Paketi — KolayTercih",
title: "Tercih Dönemi Paketi",
description:
"Tek seferlik Tercih Dönemi Paketi: yapay zekâ destekli kişisel 24 tercihlik liste, risk analizi, liste revizyonları ve soru hakkı. Abonelik yok.",
alternates: { canonical: "/paket" },
};
function tl(kurus: number) {

View File

@@ -10,7 +10,8 @@ import type { RaporParams } from "@/lib/rapor-havuzu";
import { YazdirButonu } from "./yazdir-butonu";
export const metadata: Metadata = {
title: "Tercih Raporu (Yazdır) — KolayTercih",
title: "Tercih Raporu (Yazdır)",
robots: { index: false, follow: false },
};
const DILIM_LABEL: Record<string, string> = {

View File

@@ -0,0 +1,220 @@
import { ImageResponse } from "next/og";
import { getRehber } from "@/lib/rehber";
import {
rehberBasliginiBol,
rehberMetniniParcala,
} from "@/components/rehber-kapak";
export const alt = "KolayTercih Tercih Rehberi yazı kapağı";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function RehberOpenGraphImage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const yazi = getRehber(slug);
const baslik = yazi?.baslik ?? "Tercih Rehberi";
const { vurgu, kalan } = rehberBasliginiBol(baslik);
return new ImageResponse(
(
<div
style={{
position: "relative",
display: "flex",
width: "100%",
height: "100%",
overflow: "hidden",
background: "white",
color: "#2563eb",
fontFamily: "sans-serif",
}}
>
{[
[960, 50, 0.16], [980, 50, 0.28], [1020, 50, 0.12],
[940, 70, 0.24], [980, 70, 0.12], [1000, 70, 0.3],
[1060, 70, 0.18], [960, 90, 0.12], [1000, 90, 0.2],
[1040, 90, 0.1], [1080, 90, 0.26], [1080, 110, 0.14],
[1120, 110, 0.22], [1140, 110, 0.12], [60, 490, 0.12],
[100, 490, 0.25], [120, 490, 0.14], [40, 510, 0.24],
[80, 510, 0.12], [120, 510, 0.3], [160, 510, 0.16],
[60, 530, 0.14], [100, 530, 0.22], [140, 530, 0.1],
[180, 530, 0.27],
].map(([left, top, opacity], i) => (
<span
key={i}
style={{
position: "absolute",
display: "flex",
left,
top,
width: 14,
height: 14,
borderRadius: 4,
background: "#2563eb",
opacity,
}}
/>
))}
<span
style={{
position: "absolute",
display: "flex",
left: 1040,
top: 50,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<span
style={{
position: "absolute",
display: "flex",
left: 80,
top: 530,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
border: "2px solid rgba(37,99,235,0.15)",
}}
/>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
padding: "58px 68px 52px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
color: "#2563eb",
}}
>
<span>Tercih Rehberi</span>
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
maxWidth: 980,
fontSize: baslik.length > 70 ? 48 : baslik.length > 52 ? 54 : 62,
fontWeight: 300,
lineHeight: 1.02,
letterSpacing: "-0.025em",
color: "#2563eb",
}}
>
<span
style={{
display: "flex",
color: "#ff5a00",
fontWeight: 700,
letterSpacing: "-0.04em",
}}
>
{rehberMetniniParcala(vurgu).map((parca, i) =>
/^\d/.test(parca) ? (
<span
key={i}
style={{
display: "flex",
fontStyle: "italic",
fontWeight: 800,
}}
>
{parca}
</span>
) : (
parca
),
)}
</span>
{kalan ? (
<span
style={{
display: "flex",
marginLeft: /^\s/.test(kalan) ? 12 : 0,
}}
>
{rehberMetniniParcala(kalan.trimStart()).map((parca, i) =>
/^\d/.test(parca) ? (
<span
key={i}
style={{
display: "flex",
fontStyle: "italic",
fontWeight: 800,
}}
>
{parca}
</span>
) : (
parca
),
)}
</span>
) : null}
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<svg
width="38"
height="38"
viewBox="0 0 180 180"
style={{ display: "flex" }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span
style={{
display: "flex",
marginLeft: 2,
fontSize: 24,
letterSpacing: "-0.05em",
color: "#2563eb",
}}
>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 700 }}>Tercih</span>
</span>
</div>
</div>
</div>
),
size,
);
}

View File

@@ -0,0 +1,137 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react";
import { getAllRehberSlugs, getAllRehberler, getRehber } from "@/lib/rehber";
import { JsonLd, articleJsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { RehberKapak } from "@/components/rehber-kapak";
import { SiteFooter } from "@/components/site-footer";
export function generateStaticParams() {
const slugs = getAllRehberSlugs();
if (slugs.length === 0) {
throw new Error("rehber: content/rehber altında hiç makale yok");
}
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const yazi = getRehber(slug);
if (!yazi) return {};
return {
title: yazi.baslik,
description: yazi.aciklama,
alternates: { canonical: `/rehber/${slug}` },
openGraph: {
type: "article",
title: yazi.baslik,
description: yazi.aciklama,
publishedTime: yazi.tarih,
},
};
}
export default async function RehberYaziPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const yazi = getRehber(slug);
if (!yazi) notFound();
const digerYazilar = getAllRehberler()
.filter((y) => y.slug !== slug)
.slice(0, 4);
return (
<>
<main className="mx-auto w-full max-w-3xl px-4 py-12 sm:py-16">
<JsonLd
data={articleJsonLd({
title: yazi.baslik,
description: yazi.aciklama,
path: `/rehber/${slug}`,
datePublished: yazi.tarih,
})}
/>
<JsonLd
data={breadcrumbJsonLd([
{ name: "Ana Sayfa", path: "/" },
{ name: "Rehber", path: "/rehber" },
{ name: yazi.baslik, path: `/rehber/${slug}` },
])}
/>
<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="/rehber" className="hover:text-primary hover:underline">
Rehber
</Link>
</nav>
<article>
<RehberKapak baslik={yazi.baslik} className="mt-6 shadow-sm" />
<h1 className="mt-8 font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
{yazi.baslik}
</h1>
<p className="mt-3 text-base leading-7 text-slate-600">
{yazi.aciklama}
</p>
<div
className="mt-8 text-[15px] leading-7 text-slate-700
[&_h2]:mt-10 [&_h2]:font-heading [&_h2]:text-xl [&_h2]:font-bold [&_h2]:text-slate-900
[&_h3]:mt-6 [&_h3]:font-heading [&_h3]:text-base [&_h3]:font-bold [&_h3]:text-slate-900
[&_p]:mt-3
[&_a]:font-medium [&_a]:text-primary hover:[&_a]:underline
[&_strong]:font-semibold [&_strong]:text-slate-900
[&_ul]:mt-3 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ul]:pl-6
[&_ol]:mt-3 [&_ol]:list-decimal [&_ol]:space-y-1.5 [&_ol]:pl-6
[&_table]:mt-4 [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm
[&_th]:border [&_th]:border-slate-200 [&_th]:bg-slate-50 [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold
[&_td]:border [&_td]:border-slate-200 [&_td]:px-3 [&_td]:py-2
[&_blockquote]:mt-4 [&_blockquote]:border-l-4 [&_blockquote]:border-primary/30 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-slate-600"
dangerouslySetInnerHTML={{ __html: yazi.html }}
/>
</article>
<div className="mt-12">
<CtaSiraForm />
</div>
{digerYazilar.length > 0 ? (
<section className="mt-12">
<h2 className="font-heading text-xl font-bold text-slate-900">
Diğer rehber yazıları
</h2>
<ul className="mt-3 space-y-2">
{digerYazilar.map((y) => (
<li key={y.slug}>
<Link
href={`/rehber/${y.slug}`}
className="text-sm font-medium text-primary hover:underline"
>
{y.baslik}
</Link>
</li>
))}
</ul>
</section>
) : null}
</main>
<SiteFooter />
</>
);
}

77
src/app/rehber/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { getAllRehberler } from "@/lib/rehber";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { RehberKapak } from "@/components/rehber-kapak";
import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Tercih Rehberi 2026 — YKS Tercih Dönemi Yazıları",
description:
"YKS tercih dönemi rehberi: tercih listesi nasıl yapılır, ölü tercih nedir, ek yerleştirme nasıl işler, sıralama mı puan mı — hepsi öğrenci diliyle.",
alternates: { canonical: "/rehber" },
};
export default function RehberIndexPage() {
const yazilar = getAllRehberler();
return (
<>
<main className="mx-auto w-full max-w-6xl px-4 py-12 sm:py-16">
<JsonLd
data={breadcrumbJsonLd([
{ name: "Ana Sayfa", path: "/" },
{ name: "Rehber", path: "/rehber" },
])}
/>
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Tercih Rehberi
</h1>
<p className="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
Tercih döneminin en çok sorulan soruları, veriye dayalı ve öğrenci
diliyle. Panik yok, pazarlama yok sadece işine yarayacak bilgi.
</p>
<PagePixelDivider seed={11} className="mt-8" />
<ul className="mt-8 columns-1 gap-5 sm:columns-2 lg:columns-3">
{yazilar.map((y) => (
<li key={y.slug} className="mb-5 break-inside-avoid">
<Link
href={`/rehber/${y.slug}`}
className="block overflow-hidden rounded-3xl border border-slate-200 bg-white p-2 shadow-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"
>
<RehberKapak
baslik={y.baslik}
compact
className="rounded-[1.15rem]"
/>
<div className="px-3 pb-3 pt-4">
<p className="text-sm leading-6 text-slate-600">
{y.aciklama}
</p>
<span className="mt-4 inline-flex items-center gap-1.5 font-bricolage text-sm font-semibold text-primary">
Yazıyı oku
<ArrowRight
className="size-4"
aria-hidden
/>
</span>
</div>
</Link>
</li>
))}
</ul>
<div className="mx-auto mt-16 max-w-3xl">
<CtaSiraForm />
</div>
</main>
<SiteFooter />
</>
);
}

15
src/app/robots.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/seo";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
// /sonuc bilinçli olarak ENGELLENMEDİ: sayfa noindex meta taşıyor ve
// robots.txt engeli o meta'nın görülmesini imkânsız kılardı.
disallow: ["/api/", "/listem", "/odeme/", "/rapor/", "/giris"],
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

40
src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/seo";
import { getAllBolumSlugs, getAllUniversiteSlugs } from "@/lib/katalog";
import { getAllRehberler } from "@/lib/rehber";
// Bilinçli olarak sitemap DIŞI: /sonuc (noindex), /giris, /listem,
// /odeme/sonuc, /rapor/yazdir (auth/utility sayfaları).
const staticEntries: MetadataRoute.Sitemap = [
{ url: `${SITE_URL}/`, priority: 1, changeFrequency: "weekly" },
{ url: `${SITE_URL}/bolumler`, priority: 0.8, changeFrequency: "weekly" },
{ url: `${SITE_URL}/universiteler`, priority: 0.8, changeFrequency: "weekly" },
{ url: `${SITE_URL}/rehber`, priority: 0.8, changeFrequency: "weekly" },
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
{ url: `${SITE_URL}/gizlilik`, priority: 0.2, changeFrequency: "yearly" },
{ url: `${SITE_URL}/kosullar`, priority: 0.2, changeFrequency: "yearly" },
];
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const bolumler: MetadataRoute.Sitemap = getAllBolumSlugs().map((slug) => ({
url: `${SITE_URL}/bolum/${slug}`,
priority: 0.7,
changeFrequency: "monthly",
}));
const universiteler: MetadataRoute.Sitemap = getAllUniversiteSlugs().map(
(slug) => ({
url: `${SITE_URL}/universite/${slug}`,
priority: 0.6,
changeFrequency: "monthly",
}),
);
const rehberler: MetadataRoute.Sitemap = getAllRehberler().map((y) => ({
url: `${SITE_URL}/rehber/${y.slug}`,
lastModified: y.tarih,
priority: 0.7,
changeFrequency: "monthly",
}));
return [...staticEntries, ...bolumler, ...universiteler, ...rehberler];
}

View File

@@ -19,10 +19,15 @@ import type { MevcutRapor } from "./liste-paneli";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ProgramTablosu } from "@/components/program-tablosu";
import { ManuelHarita } from "@/components/manuel-liste/manuel-harita";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Tercih Listeni Kur — KolayTercih",
title: "Tercih Listeni Kur",
// Param uzayı sonsuz + içerik sıralamaya göre kişisel: indexlenmez ama
// link değeri follow ile geri akar. robots.txt'te ENGELLEME — noindex'in
// görülebilmesi için sayfa taranabilir kalmalı.
robots: { index: false, follow: true },
};
const TUR_LABELS: Record<PuanTuruKey, string> = {
@@ -132,15 +137,22 @@ export default async function SonucPage({
</p>
<PagePixelDivider seed={61} className="mt-7" />
{/* Sayfa hiyerarşisi: önce Türkiye haritası, sonra liste. Manuel
liste store'una abone olduğundan tablodan "+" ile eklenenler
buraya canlı yansır. */}
<div className="mt-8">
<ManuelHarita />
</div>
{/* AI'dan bağımsız ham tablo — haritanın hemen altında, sayfanın ana
gövdesi; puan türü seçici de listenin başında yaşar */}
<ProgramTablosu sonuclar={hamSonuclar} adaySira={sira} tur={turKey} />
{/* Satın alım funnel'ı: değişen cümlelerle adayı sihirbaza (rapor
varsa /listem paywall'ına) iten banner — paketli kullanıcıya
satacak bir şey kalmadığından gösterilmez */}
{!hasPaket ? <FunnelBanner hedefListem={Boolean(mevcutRapor)} /> : null}
{/* AI'dan bağımsız ham tablo + manuel liste haritası — sayfanın ana
gövdesi; puan türü seçici de listenin başında yaşar */}
<ProgramTablosu sonuclar={hamSonuclar} adaySira={sira} tur={turKey} />
{/* Sihirbaz akışı (CTA + modal + AI listesi) — blurlu AI paneli
sayfanın en altında yaşar */}
<SihirbazBolumu

View File

@@ -504,6 +504,8 @@ export function SihirbazBolumu({
<SihirbazModal
acik={acik}
onAcikDegisti={acikDegisti}
sira={sira}
tur={tur}
facetler={facetler}
baslangicSecimler={
sonSecimler ?? (bekleyen?.eslesir ? bekleyen.secimler : null)

View File

@@ -18,6 +18,8 @@ import type { SihirbazSecimleri } from "@/lib/sihirbaz";
export function SihirbazModal({
acik,
onAcikDegisti,
sira,
tur,
facetler,
baslangicSecimler,
sonButonEtiketi,
@@ -25,6 +27,8 @@ export function SihirbazModal({
}: {
acik: boolean;
onAcikDegisti: (acik: boolean) => void;
sira: number;
tur: string;
facetler: SihirbazFacetleri;
baslangicSecimler?: SihirbazSecimleri | null;
sonButonEtiketi: string;
@@ -41,6 +45,8 @@ export function SihirbazModal({
Sıralamana uygun 24 tercihlik listeni oluştur.
</DialogDescription>
<SihirbazAdimlarLazy
sira={sira}
tur={tur}
facetler={facetler}
baslangicSecimler={baslangicSecimler}
sonButonEtiketi={sonButonEtiketi}

View File

@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import { getAllUniversiteSlugs } from "@/lib/katalog";
import { UniversiteIcerik, uniSayfaMetadata } from "./universite-icerik";
export function generateStaticParams() {
return getAllUniversiteSlugs().map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
return uniSayfaMetadata(slug, 1);
}
export default async function UniversitePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <UniversiteIcerik slug={slug} sayfa={1} />;
}

View File

@@ -0,0 +1,47 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllUniversiteler } from "@/lib/katalog";
import {
UniversiteIcerik,
uniSayfaMetadata,
uniToplamSayfa,
} from "../../universite-icerik";
// Yalnızca 1'den fazla sayfası olan üniversiteler için 2..N üretilir;
// 1. sayfa taban rota (/universite/[slug]) olduğundan burada üretilmez.
export function generateStaticParams() {
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),
}));
});
}
function sayfaNo(no: string): number | null {
const n = Number(no);
return Number.isInteger(n) && n >= 2 ? n : null;
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; no: string }>;
}): Promise<Metadata> {
const { slug, no } = await params;
const n = sayfaNo(no);
if (n == null) return {};
return uniSayfaMetadata(slug, n);
}
export default async function UniversiteSayfaPage({
params,
}: {
params: Promise<{ slug: string; no: string }>;
}) {
const { slug, no } = await params;
const n = sayfaNo(no);
if (n == null) notFound();
return <UniversiteIcerik slug={slug} sayfa={n} />;
}

View File

@@ -0,0 +1,336 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react";
import { getUniversiteBySlug, bolumSlugFromIsim } from "@/lib/katalog";
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 "@/components/sayfalama";
import { PagePixelDivider } from "@/components/pixel-decor";
import { UniversiteKonumHaritasi } from "@/components/universite-konum-haritasi";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { Program } from "@/lib/db";
const sayi = (n: number) => n.toLocaleString("tr-TR");
const TUR_ETIKET: Record<string, string> = {
SAYISAL: "SAY",
"EŞİT AĞIRLIK": "EA",
SÖZEL: "SÖZ",
DİL: "DİL",
TYT: "TYT",
};
export function uniToplamSayfa(programSayisi: number): number {
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
}
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[] }[] = [];
for (const p of dilim) {
const anahtar = p.fakulte ?? "Diğer";
const son = gruplar[gruplar.length - 1];
if (son && son.fakulte === anahtar) son.programlar.push(p);
else gruplar.push({ fakulte: anahtar, programlar: [p] });
}
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}`,
...(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>
<h1 className="mt-3 font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
{uni.ad} Taban Puanları 2026
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
{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ı
ı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">
<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>
<UniversiteKonumHaritasi universite={uni.ad} il={uni.il} />
</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>
{gruplar.map(({ fakulte, programlar }, i) => (
<section key={`${fakulte}-${i}`} className="mt-6">
<h2 className="mb-3 font-heading text-xl font-bold text-slate-900">
{fakulte}
</h2>
<div className="overflow-x-auto rounded-xl border border-slate-200 bg-white">
<Table>
<TableHeader>
<TableRow>
<TableHead>Program</TableHead>
<TableHead>Puan Türü</TableHead>
<TableHead className="text-right">2025 Puan</TableHead>
<TableHead className="text-right">2025 Sıralama</TableHead>
<TableHead className="text-right">2024 Sıralama</TableHead>
<TableHead className="text-right">Kontenjan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{programlar.map((p) => {
const bolumSlug = bolumSlugFromIsim(p.isim);
return (
<TableRow key={p.id}>
<TableCell className="max-w-72">
{bolumSlug ? (
<Link
href={`/bolum/${bolumSlug}`}
className="font-medium text-slate-900 hover:text-primary hover:underline"
>
{p.isim.trim()}
</Link>
) : (
<span className="font-medium">
{p.isim.trim()}
</span>
)}
</TableCell>
<TableCell>{TUR_ETIKET[p.tur] ?? p.tur}</TableCell>
<TableCell className="text-right tabular-nums">
{p.puan2025 != null
? p.puan2025.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{p.sira2025 != null ? sayi(p.sira2025) : "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{p.sira2024 != null ? sayi(p.sira2024) : "—"}
</TableCell>
<TableCell className="text-right tabular-nums">
{p.kontenjan2025 != null
? sayi(p.kontenjan2025)
: "—"}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</section>
))}
<p className="mt-3 text-xs leading-5 text-slate-500">
&quot;&quot;: o yıl için yerleştirme verisi bulunmuyor (yeni ı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 />
</>
);
}

View File

@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import Link from "next/link";
import { getAllUniversiteler, type UniOzet } from "@/lib/katalog";
import { trBaslikDuzeni } from "@/lib/slug";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor";
export const metadata: Metadata = {
title: "Üniversitelerin Taban Puanları ve Sıralamaları 2026",
description:
"Devlet ve vakıf üniversitelerinin 2025 yerleştirme taban puanları, başarı sıralamaları ve kontenjanları — üniversite üniversite, 2026 tercihleri için.",
alternates: { canonical: "/universiteler" },
};
const sayi = (n: number) => n.toLocaleString("tr-TR");
function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
return (
<ul className="mt-3 grid gap-x-6 gap-y-1.5 sm:grid-cols-2">
{uniler.map((u) => (
<li key={u.slug}>
<Link
href={`/universite/${u.slug}`}
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 hover:text-primary"
>
<span className="group-hover:underline">{u.ad}</span>
<span className="text-xs text-slate-400">
{u.il ? `${trBaslikDuzeni(u.il)} · ` : ""}
{sayi(u.programSayisi)} program
</span>
</Link>
</li>
))}
</ul>
);
}
export default function UniversitelerPage() {
const hepsi = getAllUniversiteler();
const devlet = hepsi.filter((u) => u.unitur === "DEVLET");
const vakif = hepsi.filter(
(u) => u.unitur != null && ["VAKIF", "VAKIF MYO"].includes(u.unitur),
);
const digerUniler = hepsi.filter(
(u) => !devlet.includes(u) && !vakif.includes(u),
);
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" },
])}
/>
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Üniversitelerin Taban Puanları ve Sıralamaları 2026
</h1>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{sayi(hepsi.length)} üniversitenin 2025 YKS yerleştirme verileri.
Üniversiteye tıklayınca tüm programlarının taban puanlarını, başarı
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler
YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri için
rehber niteliğindedir.
</p>
<PagePixelDivider seed={63} className="mt-8" />
<section className="mt-8">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Devlet Üniversiteleri
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(devlet.length)}
</span>
</h2>
<UniLinkleri uniler={devlet} />
</section>
<section className="mt-10">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Vakıf Üniversiteleri
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(vakif.length)}
</span>
</h2>
<UniLinkleri uniler={vakif} />
</section>
{digerUniler.length > 0 ? (
<section className="mt-10">
<h2 className="font-heading text-2xl font-bold text-slate-900">
KKTC ve Yurt Dışı
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(digerUniler.length)}
</span>
</h2>
<UniLinkleri uniler={digerUniler} />
</section>
) : null}
<div className="mt-12">
<CtaSiraForm />
</div>
</main>
<SiteFooter />
</>
);
}