feat(seo): GEO paketi — yapılandırılmış metadata, AI bot erişimi, veri tazelik sinyali

- sayfaMetadata(): indekslenebilir sayfalar için tek kaynaklı canonical +
  eksiksiz openGraph üretimi; sayfalar buna geçirildi
- robots.ts: yalnızca /api/ engellenir; noindex'li sayfalar robots engeli
  yerine meta ile yönetilir, AI botları niyeti belgeleyen açık grupla izinli
- next.config: htmlLimitedBots'a AI botları eklendi (JS çalıştırmayan
  botlara metadata head içinde bloklanarak verilir)
- sitemap lastmod + katalogda görünür 'Kaynak/son güncelleme' satırı
  (VeriKaynagi) KATALOG_VERI_TARIHI tek kaynağından
- docs/geo/geo-raporu.md: çalışmanın raporu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 00:51:16 +03:00
parent d2651eadb9
commit 53eb07bb34
18 changed files with 718 additions and 73 deletions

View File

@@ -1,18 +1,18 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
import { SiteFooter } from "@/components/site-footer";
import {
BolumKatalogu,
BolumKataloguSkeleton,
} from "@/features/katalog/components/bolum-listesi";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/bolumler",
});
export default function BolumlerPage() {
return (

View File

@@ -3,13 +3,14 @@ import Link from "next/link";
import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/gizlilik",
});
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
export default function GizlilikPage() {

View File

@@ -3,13 +3,14 @@ import { Mail } from "lucide-react";
import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
title: "İletişim",
description:
"KolayTercih destek ekibine ulaş: sorular, geri bildirim ve iade talepleri için iletişim kanalları.",
alternates: { canonical: "/iletisim" },
};
path: "/iletisim",
});
export default function IletisimPage() {
return (

View File

@@ -3,13 +3,14 @@ import Link from "next/link";
import { Parallax } from "@/components/parallax";
import { PagePixelDivider } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/kosullar",
});
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
export default function KosullarPage() {

View File

@@ -7,13 +7,14 @@ import {
MeraklisinaIcerik,
MeraklisinaIcerikSkeleton,
} from "@/features/pazarlama/components/meraklisina-icerik";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
title: "Meraklısına: KolayTercih nasıl çalışır?",
alternates: { canonical: "/meraklisina" },
path: "/meraklisina",
description:
"Sıralamanı girdiğinde arka planda ne oluyor? Dilimler, risk renkleri ve sihirbazın adım adım daralan filtreleri — öğrenci diliyle, sıralamana göre hesaplanan canlı görsellerle.",
};
});
export default function MeraklisinaPage() {
return (

View File

@@ -8,13 +8,14 @@ import {
PaketSatinAl,
PaketSatinAlSkeleton,
} from "@/features/odeme/components/paket-satinal";
import { sayfaMetadata } from "@/lib/seo";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/paket",
});
export default function PaketPage({
searchParams,

View File

@@ -1,6 +1,6 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { PagePixelDivider } from "@/components/pixel-decor";
@@ -9,12 +9,12 @@ import {
RehberListesiSkeleton,
} from "@/features/rehber/components/rehber-listesi";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/rehber",
});
export default function RehberIndexPage() {
return (

View File

@@ -1,15 +1,44 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/seo";
// Yalnızca /api/ engellenir. /sonuc, /giris, /listem, /odeme/*, /rapor/*
// bilinçli olarak ENGELLENMEDİ: bu sayfalar noindex meta taşıyor ve robots.txt
// engeli o meta'nın görülmesini imkânsız kılardı (dış link alırlarsa
// "robots.txt tarafından engellendi ama dizine eklendi" durumuna düşerlerdi).
const DISALLOW = ["/api/"];
// Arama/cevap motorlarının tarayıcıları. `*` kuralı bunları zaten kapsıyor;
// grup, niyeti belgelemek için açıkça yazıldı: AI aramalarında kaynak
// gösterilmek istiyoruz. DİKKAT: bir bot kendi adına yazılmış grubu bulursa `*`
// grubunu yok sayar — buraya eklenen her bot için DISALLOW tekrar edilmeli.
const AI_BOTLARI = [
// Arama indeksi + kullanıcı tetiklemeli fetch
"OAI-SearchBot",
"ChatGPT-User",
"Claude-SearchBot",
"Claude-User",
"PerplexityBot",
"Perplexity-User",
"DuckAssistBot",
"MistralAI-User",
"meta-webindexer",
"Amzn-SearchBot",
// Model eğitimi: aramasız yanıtlarda markanın bilinmesi buradan geçer
"GPTBot",
"ClaudeBot",
"Google-Extended",
"Applebot-Extended",
"meta-externalagent",
"Amazonbot",
"CCBot",
];
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"],
},
rules: [
{ userAgent: "*", allow: "/", disallow: DISALLOW },
{ userAgent: AI_BOTLARI, allow: "/", disallow: DISALLOW },
],
sitemap: `${SITE_URL}/sitemap.xml`,
};
}

View File

@@ -1,5 +1,5 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/seo";
import { KATALOG_VERI_TARIHI, SITE_URL } from "@/lib/seo";
import { getAllBolumSlugs, getAllUniversiteSlugs } from "@/lib/katalog";
import { getAllRehberler } from "@/lib/rehber";
@@ -7,8 +7,18 @@ import { getAllRehberler } from "@/lib/rehber";
// /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}/bolumler`,
lastModified: KATALOG_VERI_TARIHI,
priority: 0.8,
changeFrequency: "weekly",
},
{
url: `${SITE_URL}/universiteler`,
lastModified: KATALOG_VERI_TARIHI,
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" },
@@ -20,12 +30,14 @@ const staticEntries: MetadataRoute.Sitemap = [
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const bolumler: MetadataRoute.Sitemap = getAllBolumSlugs().map((slug) => ({
url: `${SITE_URL}/bolum/${slug}`,
lastModified: KATALOG_VERI_TARIHI,
priority: 0.7,
changeFrequency: "monthly",
}));
const universiteler: MetadataRoute.Sitemap = getAllUniversiteSlugs().map(
(slug) => ({
url: `${SITE_URL}/universite/${slug}`,
lastModified: KATALOG_VERI_TARIHI,
priority: 0.6,
changeFrequency: "monthly",
}),

View File

@@ -1,18 +1,18 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd, breadcrumbJsonLd } from "@/lib/seo";
import { JsonLd, breadcrumbJsonLd, sayfaMetadata } from "@/lib/seo";
import { SiteFooter } from "@/components/site-footer";
import {
UniversiteKatalogu,
UniversiteKataloguSkeleton,
} from "@/features/katalog/components/universite-listesi";
export const metadata: Metadata = {
export const metadata: Metadata = sayfaMetadata({
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" },
};
path: "/universiteler",
});
export default function UniversitelerPage() {
return (