refactor: rehber feature'ı — makale ve liste feature bileşenlerine çıktı (Faz 3)

- rehber-kapak + el-cizimi-sema features/rehber/components/ altına taşındı
  (el-cizimi-sema'nın göreli font yolu yeni konuma göre düzeltildi)
- /rehber'in kart gridi RehberListesi'ne, /rehber/[slug]'ın makale gövdesi
  (JsonLd + içerik + ilgili yazılar + notFound) RehberMakale'ye çıkarıldı;
  metadata/staticParams yardımcıları bileşen dosyasından export ediliyor
- İki sayfa senkron: params.then() + sayfada Suspense, skeleton'lar colocated
- rehber-queries.ts (server-only) lib/rehber'in feature kapısı
- Davranış değişikliği yok; içerik build-time statik

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:18:22 +03:00
parent 8d26e375ff
commit e3cf141f55
10 changed files with 266 additions and 191 deletions

View File

@@ -1,8 +1,8 @@
// Sihirbaz hunisinin statik anlatımı: interaktif demo yerine akışın tamamını // Sihirbaz hunisinin statik anlatımı: interaktif demo yerine akışın tamamını
// tek bakışta gösteren bir mermaid şeması. Çizim, ürün genelindeki ortak // tek bakışta gösteren bir mermaid şeması. Çizim, ürün genelindeki ortak
// "el çizimi" bileşeniyle yapılır (bkz. components/el-cizimi-sema.tsx). // "el çizimi" bileşeniyle yapılır (bkz. features/rehber/components/el-cizimi-sema.tsx).
import { ElCizimiSema } from "@/components/el-cizimi-sema"; import { ElCizimiSema } from "@/features/rehber/components/el-cizimi-sema";
// Renkler ürünün risk dilinden (kırmızı/sarı/yeşil) ve slate paletinden; // Renkler ürünün risk dilinden (kırmızı/sarı/yeşil) ve slate paletinden;
// renk tek başına anlam taşımaz, her düğüm metinle etiketlidir. // renk tek başına anlam taşımaz, her düğüm metinle etiketlidir.

View File

@@ -4,7 +4,7 @@ import { loadRehberOgFonts } from "@/lib/og-fonts";
import { import {
rehberBasliginiBol, rehberBasliginiBol,
rehberMetniniParcala, rehberMetniniParcala,
} from "@/components/rehber-kapak"; } from "@/features/rehber/components/rehber-kapak";
export const alt = "KolayTercih Tercih Rehberi yazı kapağı"; export const alt = "KolayTercih Tercih Rehberi yazı kapağı";
export const size = { width: 1200, height: 630 }; export const size = { width: 1200, height: 630 };

View File

@@ -1,153 +1,52 @@
import { Suspense } from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react"; 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 { ElCizimiSema } from "@/components/el-cizimi-sema";
import { RehberKapak } from "@/components/rehber-kapak";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import {
RehberMakale,
RehberMakaleSkeleton,
rehberStaticSlugs,
rehberYaziMetadata,
} from "@/features/rehber/components/rehber-makale";
export function generateStaticParams() { export function generateStaticParams() {
const slugs = getAllRehberSlugs(); return rehberStaticSlugs().map((slug) => ({ slug }));
if (slugs.length === 0) {
throw new Error("rehber: content/rehber altında hiç makale yok");
}
return slugs.map((slug) => ({ slug }));
} }
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: { }: PageProps<"/rehber/[slug]">): Promise<Metadata> {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
const yazi = getRehber(slug); return rehberYaziMetadata(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({ export default function RehberYaziPage({
params, params,
}: { }: PageProps<"/rehber/[slug]">) {
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 ( return (
<> <>
<main className="mx-auto w-full max-w-3xl px-4 py-12 sm:py-16"> <main className="mx-auto w-full max-w-3xl px-4 py-12 sm:py-16">
<JsonLd <nav
data={articleJsonLd({ aria-label="breadcrumb"
title: yazi.baslik, className="flex items-center gap-1 text-xs text-slate-500"
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"
> >
{yazi.parcalar.map((parca, i) => <Link href="/" className="hover:text-primary hover:underline">
parca.tip === "html" ? ( Ana Sayfa
<div </Link>
key={i} <ChevronRight className="size-3" aria-hidden />
dangerouslySetInnerHTML={{ __html: parca.html }} <Link href="/rehber" className="hover:text-primary hover:underline">
/> Rehber
) : ( </Link>
<ElCizimiSema </nav>
key={i}
tanim={parca.sema.tanim}
ariaLabel={parca.sema.ariaLabel}
figcaption={parca.sema.altyazi}
/>
),
)}
</div>
</article>
<div className="mt-12"> <Suspense fallback={<RehberMakaleSkeleton />}>
<CtaSiraForm /> {params.then(({ slug }) => (
</div> <RehberMakale slug={slug} />
))}
{digerYazilar.length > 0 ? ( </Suspense>
<section className="mt-12"> </main>
<h2 className="font-heading text-xl font-bold text-slate-900"> <SiteFooter />
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 />
</> </>
); );
} }

View File

@@ -1,12 +1,13 @@
import { Suspense } from "react";
import type { Metadata } from "next"; 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 { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form"; import { CtaSiraForm } from "@/components/cta-sira-form";
import { RehberKapak } from "@/components/rehber-kapak";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor"; import { PagePixelDivider } from "@/components/pixel-decor";
import {
RehberListesi,
RehberListesiSkeleton,
} from "@/features/rehber/components/rehber-listesi";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tercih Rehberi 2026 — YKS Tercih Dönemi Yazıları", title: "Tercih Rehberi 2026 — YKS Tercih Dönemi Yazıları",
@@ -16,62 +17,35 @@ export const metadata: Metadata = {
}; };
export default function RehberIndexPage() { export default function RehberIndexPage() {
const yazilar = getAllRehberler();
return ( return (
<> <>
<main className="mx-auto w-full max-w-6xl px-4 py-12 sm:py-16"> <main className="mx-auto w-full max-w-6xl px-4 py-12 sm:py-16">
<JsonLd <JsonLd
data={breadcrumbJsonLd([ data={breadcrumbJsonLd([
{ name: "Ana Sayfa", path: "/" }, { name: "Ana Sayfa", path: "/" },
{ name: "Rehber", path: "/rehber" }, { name: "Rehber", path: "/rehber" },
])} ])}
/> />
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl"> <h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
Tercih Rehberi Tercih Rehberi
</h1> </h1>
<p className="mt-4 max-w-2xl text-sm leading-7 text-slate-600"> <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 Tercih döneminin en çok sorulan soruları, veriye dayalı ve öğrenci
diliyle. Panik yok, pazarlama yok sadece işine yarayacak bilgi. diliyle. Panik yok, pazarlama yok sadece işine yarayacak bilgi.
</p> </p>
<PagePixelDivider seed={11} className="mt-8" /> <PagePixelDivider seed={11} className="mt-8" />
<ul className="mt-8 columns-1 gap-5 sm:columns-2 lg:columns-3"> <Suspense fallback={<RehberListesiSkeleton />}>
{yazilar.map((y) => ( <RehberListesi />
<li key={y.slug} className="mb-5 break-inside-avoid"> </Suspense>
<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"> <div className="mx-auto mt-16 max-w-3xl">
<CtaSiraForm /> <CtaSiraForm />
</div> </div>
</main> </main>
<SiteFooter /> <SiteFooter />
</> </>
); );
} }

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { AramaFunnelKarti } from "@/components/arama-funnel-karti"; import { AramaFunnelKarti } from "@/components/arama-funnel-karti";
import { RehberKapak } from "@/components/rehber-kapak"; import { RehberKapak } from "@/features/rehber/components/rehber-kapak";
import { UniLogo } from "@/components/uni-logo"; import { UniLogo } from "@/components/uni-logo";
import { aramaNormalize, eslesmePuani } from "@/lib/arama"; import { aramaNormalize, eslesmePuani } from "@/lib/arama";
import type { RehberOzet } from "@/lib/rehber"; import type { RehberOzet } from "@/lib/rehber";

View File

@@ -14,7 +14,7 @@ import localFont from "next/font/local";
// var). DİKKAT: lisans "kişisel kullanım için ücretsiz" — canlıya çıkmadan // var). DİKKAT: lisans "kişisel kullanım için ücretsiz" — canlıya çıkmadan
// mistifonts.com/sweet-cucumber-mocktail adresinden ticari lisans alınmalı. // mistifonts.com/sweet-cucumber-mocktail adresinden ticari lisans alınmalı.
const elYazisi = localFont({ const elYazisi = localFont({
src: "../fonts/SweetCucumberMocktail.woff2", src: "../../../fonts/SweetCucumberMocktail.woff2",
display: "swap", display: "swap",
}); });

View File

@@ -0,0 +1,53 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { getAllRehberler } from "../rehber-queries";
import { RehberKapak } from "./rehber-kapak";
import { Skeleton } from "@/components/ui/skeleton";
export function RehberListesi() {
const yazilar = getAllRehberler();
return (
<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>
);
}
export function RehberListesiSkeleton() {
return (
<ul className="mt-8 columns-1 gap-5 sm:columns-2 lg:columns-3">
{Array.from({ length: 3 }, (_, i) => (
<li key={i} className="mb-5 break-inside-avoid">
<div className="rounded-3xl border border-slate-200 bg-white p-2 shadow-sm">
<Skeleton className="aspect-[1200/630] w-full rounded-[1.15rem]" />
<div className="px-3 pb-3 pt-4">
<Skeleton className="h-5 w-full" />
<Skeleton className="mt-2 h-5 w-2/3" />
</div>
</div>
</li>
))}
</ul>
);
}

View File

@@ -0,0 +1,138 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getAllRehberler, getAllRehberSlugs, getRehber } from "../rehber-queries";
import { JsonLd, articleJsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { CtaSiraForm } from "@/components/cta-sira-form";
import { ElCizimiSema } from "./el-cizimi-sema";
import { RehberKapak } from "./rehber-kapak";
import { Skeleton } from "@/components/ui/skeleton";
// Sayfanın generateStaticParams'ı için — sayfalar *-queries import etmez.
export function rehberStaticSlugs(): string[] {
const slugs = getAllRehberSlugs();
if (slugs.length === 0) {
throw new Error("rehber: content/rehber altında hiç makale yok");
}
return slugs;
}
export function rehberYaziMetadata(slug: string): Metadata {
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 function RehberMakale({ slug }: { slug: string }) {
const yazi = getRehber(slug);
if (!yazi) notFound();
const digerYazilar = getAllRehberler()
.filter((y) => y.slug !== slug)
.slice(0, 4);
return (
<>
<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}` },
])}
/>
<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"
>
{yazi.parcalar.map((parca, i) =>
parca.tip === "html" ? (
<div key={i} dangerouslySetInnerHTML={{ __html: parca.html }} />
) : (
<ElCizimiSema
key={i}
tanim={parca.sema.tanim}
ariaLabel={parca.sema.ariaLabel}
figcaption={parca.sema.altyazi}
/>
),
)}
</div>
</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}
</>
);
}
export function RehberMakaleSkeleton() {
return (
<div>
<Skeleton className="mt-6 aspect-[1200/630] w-full rounded-3xl" />
<Skeleton className="mt-8 h-10 w-3/4" />
<div className="mt-3 space-y-3">
{Array.from({ length: 5 }, (_, i) => (
<Skeleton key={i} className="h-5 w-full" />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import "server-only";
// Rehber feature'ının sorgu kapısı. İçerik motoru @/lib/rehber'de yaşar
// (content/rehber'i build sırasında fs'ten okur); feature bileşenleri ve
// sayfa yardımcıları yalnızca buradan okur.
export {
getAllRehberler,
getAllRehberSlugs,
getRehber,
type RehberOzet,
} from "@/lib/rehber";