feat(geo): llms.txt, RSS, IndexNow, Article yazar/görsel, bölüm sayfasına veri özeti, vakıf bursu yazısı
All checks were successful
Deploy / deploy (push) Successful in 6m16s

GEO araştırması (docs/arastirma/2026-09-24-geo-seo) Dalga 1-2 uygulaması:
- /llms.txt ve /feed.xml route'ları (getAllRehberler'den otomatik), head'de RSS linki
- Article JSON-LD: author, image, mainEntityOfPage, publisher logo; görünür 'Yazan' satırı
- robots: ClaudeBot ayrı grupta Crawl-delay 1
- scripts/indexnow.ts (pnpm indexnow) + public anahtar dosyası
- bölüm sayfası açılışı veriden üretilen özgü cümleler (il/program/tür/kontenjan, taban aralığı, yıllık yön)
- ana sayfa fiyat iddiasına kaynak; iki yazıda 23.000 -> 21.000+; beş yazıya 'Kısa cevap' açılışı
- yeni yazı: vakif-universitesi-bursu-kesilir-mi (ÖSYM 2026 kılavuzu + YÖK ücret esasları)
- YAYIN-KUYRUGU: IndexNow adımı ve aylık AI görünürlük rutini

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 17:42:29 +03:00
parent 6094066c5e
commit 24d4bea32c
24 changed files with 1202 additions and 11 deletions

54
src/app/feed.xml/route.ts Normal file
View File

@@ -0,0 +1,54 @@
import { getAllRehberler } from "@/lib/rehber";
import { SITE_NAME, SITE_URL } from "@/lib/seo";
/**
* Rehber yazılarının RSS 2.0 beslemesi. Kök layout metadata'sındaki
* alternates.types bu adresi <link rel="alternate"> olarak <head>'e yazar.
* Cache Components altında istek verisi okunmadığı için prerender edilir.
*/
function xmlKacis(metin: string): string {
return metin
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
/** "YYYY-MM-DD" → RFC 822 (RSS pubDate). Gün 09:00 Türkiye saati kabul edilir. */
function rfc822(iso: string): string {
return new Date(`${iso}T09:00:00+03:00`).toUTCString();
}
export async function GET() {
const yazilar = getAllRehberler();
const enYeni = yazilar[0]?.guncelleme ?? yazilar[0]?.tarih;
const items = yazilar
.map(
(y) => ` <item>
<title>${xmlKacis(y.baslik)}</title>
<link>${SITE_URL}/rehber/${y.slug}</link>
<guid isPermaLink="true">${SITE_URL}/rehber/${y.slug}</guid>
<description>${xmlKacis(y.aciklama)}</description>
<pubDate>${rfc822(y.tarih)}</pubDate>
</item>`,
)
.join("\n");
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${SITE_NAME} Tercih Rehberi</title>
<link>${SITE_URL}/rehber</link>
<description>YKS tercih dönemi rehberi: veriye dayalı, öğrenci diliyle yazılar.</description>
<language>tr</language>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml" />
${enYeni ? ` <lastBuildDate>${rfc822(enYeni)}</lastBuildDate>\n` : ""}${items}
</channel>
</rss>
`;
return new Response(xml, {
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
});
}

View File

@@ -52,6 +52,9 @@ export const metadata: Metadata = {
description:
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmandan çok daha düşük fiyata, yapay zekâ destekli tercih danışmanlığı.",
applicationName: "KolayTercih",
alternates: {
types: { "application/rss+xml": "/feed.xml" },
},
openGraph: {
type: "website",
locale: "tr_TR",

62
src/app/llms.txt/route.ts Normal file
View File

@@ -0,0 +1,62 @@
import { getAllRehberler } from "@/lib/rehber";
import { KATALOG_VERI_TARIHI, SITE_URL } from "@/lib/seo";
import { SON_YIL, VERI_ARALIGI } from "@/lib/veri-yillari";
import { PROGRAM_SAYISI_METNI } from "@/lib/veri-kapsami";
import { trTarih } from "@/lib/tarih";
/**
* llmstxt.org biçiminde site özeti: H1, tek paragraf özet, H2 altında
* bağlantı listeleri. Yalnızca sitede zaten görünen bilgiyi taşır; içerik
* gövdesi (llms-full.txt) bilinçli olarak üretilmez — etkisi kanıtsız,
* toplu kopyalamayı kolaylaştırır (docs/arastirma/2026-09-24-geo-seo).
*
* Rehber listesi getAllRehberler()'den gelir; sitemap ile aynı kaynak,
* elle bakım yok. Cache Components altında istek verisi okunmadığı için
* build'de prerender edilir.
*/
export async function GET() {
const yazilar = getAllRehberler();
const rehberSatirlari = yazilar
.map(
(y) =>
`- [${y.baslik}](${SITE_URL}/rehber/${y.slug}): ${y.aciklama}`,
)
.join("\n");
const metin = `# KolayTercih
> Türkiye'de YKS (üniversite giriş sınavı) tercih danışmanlığı: YÖK Atlas'ın
> resmî yerleştirme verisine dayanan, yapay zekâ destekli 24 tercihlik dengeli
> liste. Ücretsiz katalog: bölüm, üniversite ve il sayfalarında ${VERI_ARALIGI}
> taban sıralamaları.
Veri kaynağı: YÖK Atlas ${SON_YIL} yerleştirme sonuçları, ${PROGRAM_SAYISI_METNI}
program. Katalog en son ${trTarih(KATALOG_VERI_TARIHI)} tarihinde güncellendi.
KolayTercih, ÖSYM ya da YÖK ile bağlantılı değildir; yerleşme garantisi vermez.
Yapay zekâ önerileri yalnızca kullanıcının sıralamasıyla açılan gerçek programlar
arasından seçilir.
## Ana bölümler
- [Bölümler](${SITE_URL}/bolumler): bölüm bazında taban puanı ve sıralama tabloları
- [Üniversiteler](${SITE_URL}/universiteler): il bazlı üniversite kataloğu
- [Üniversite sıralaması](${SITE_URL}/universite-siralamasi): URAP akademik başarı sıralaması
- [Tercih Rehberi](${SITE_URL}/rehber): tercih süreciyle ilgili ${yazilar.length} rehber yazısı
- [Nasıl çalışır](${SITE_URL}/meraklisina): sihirbaz ve liste mantığının anlatımı
- [Paket](${SITE_URL}/paket): ücretli yapay zekâ tercih listesi hizmeti
## Rehber yazıları
${rehberSatirlari}
## Optional
- [Gizlilik](${SITE_URL}/gizlilik)
- [Kullanım koşulları](${SITE_URL}/kosullar)
- [İletişim](${SITE_URL}/iletisim)
`;
return new Response(metin, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}

View File

@@ -93,7 +93,7 @@ const problems = [
icon: Wallet,
title: "Danışman lüks haline geldi",
description:
"İyi bir tercih danışmanı 1.000–3.000 TL istiyor. Ücretsiz robotların çoğu ise yalnızca liste filtreliyor; listeyi kurup gerekçesini yazmıyor.",
"Armut'ta tercih danışmanlığı ilanları ortalama 1.000–3.000 TL (Eylül 2026). Ücretsiz robotların çoğu ise yalnızca liste filtreliyor; listeyi kurup gerekçesini yazmıyor.",
},
];
@@ -185,7 +185,7 @@ const comparison = [
{
feature: "Fiyat",
robot: "Ücretsiz",
danisman: "1.000–3.000 TL",
danisman: "1.000–3.000 TL*",
tercihai: `${PAKET_FIYATI} TL · tek seferlik`,
},
];
@@ -524,6 +524,9 @@ export default function Home() {
</TableBody>
</Table>
</div>
<p className="mt-3 text-xs text-slate-500">
* Danışman fiyat aralığı: Armut ortalama fiyatı, Eylül 2026.
</p>
</Reveal>
</Parallax>
</div>

View File

@@ -25,7 +25,7 @@ const AI_BOTLARI = [
"Amzn-SearchBot",
// Model eğitimi: aramasız yanıtlarda markanın bilinmesi buradan geçer
"GPTBot",
"ClaudeBot",
// ClaudeBot aşağıda kendi grubunda (Crawl-delay için); buraya yazma.
"Google-Extended",
"Applebot-Extended",
"meta-externalagent",
@@ -38,6 +38,9 @@ export default function robots(): MetadataRoute.Robots {
rules: [
{ userAgent: "*", allow: "/", disallow: DISALLOW },
{ userAgent: AI_BOTLARI, allow: "/", disallow: DISALLOW },
// Anthropic Crawl-delay'i resmî olarak destekliyor (support.claude.com);
// eğitim taraması 1011 URL'lik katalogda saniyede 1 istekle yeter.
{ userAgent: "ClaudeBot", allow: "/", disallow: DISALLOW, crawlDelay: 1 },
],
sitemap: `${SITE_URL}/sitemap.xml`,
};

View File

@@ -16,6 +16,7 @@ import {
getBolumBySlug,
uniSlugFromAd,
} from "../katalog-queries";
import type { Program } from "@/types/yokatlas";
import {
JsonLd,
breadcrumbJsonLd,
@@ -156,6 +157,63 @@ function siraAraligiMetni({
return verisiz > 0 ? `${aralik} ile taban verisi olmayanlar` : aralik;
}
/**
* İlk sayfanın açılış cümleleri: kaç ilde kaç program, devlet/vakıf dağılımı,
* toplam kontenjan, taban sıralaması aralığı ve bir önceki yıla göre yön.
* Yön için yalnızca iki yılda da sırası olan programlar sayılır; sıra sayısı
* büyümüşse o programa daha geriden yerleşilmiş demektir (öğrenci dilinde
* "gevşedi"). Beş programdan azsa yön cümlesi kurulmaz.
*/
function bolumVeriOzeti(bolum: BolumDetay, programlar: Program[]): string {
const { stats } = bolum;
const iller = new Set<string>();
let kontenjan = 0;
let gevseyen = 0;
let sikilasan = 0;
let karsilastirilan = 0;
for (const p of programlar) {
if (p.il) iller.add(p.il);
kontenjan += p.kontenjanSon ?? 0;
const onceki = p.siraGecmisi[p.siraGecmisi.length - 2];
if (p.siraSon != null && onceki != null) {
karsilastirilan += 1;
if (p.siraSon > onceki) gevseyen += 1;
else if (p.siraSon < onceki) sikilasan += 1;
}
}
const digerTur = stats.toplam - stats.devlet - stats.vakif;
const dagilim = [
stats.devlet > 0 ? `${sayi(stats.devlet)} devlet` : null,
stats.vakif > 0 ? `${sayi(stats.vakif)} vakıf` : null,
digerTur > 0 ? `${sayi(digerTur)} KKTC/yurt dışı` : null,
]
.filter(Boolean)
.join(", ");
const ilSayisi = iller.size > 0 ? iller.size : stats.ilSayisi;
const cumleler = [
`${bolum.ad} bölümü ${SON_YIL} YKS yerleştirmesinde ${sayi(ilSayisi)} ilde ${sayi(stats.toplam)} programda öğrenci aldı (${dagilim}${kontenjan > 0 ? `; toplam ${sayi(kontenjan)} kontenjan` : ""}).`,
];
if (stats.minSira != null && stats.maxSira != null) {
cumleler.push(
stats.minSira === stats.maxSira
? `Taban başarı sıralaması ${sayi(stats.minSira)}.`
: `Taban başarı sıralaması programa göre ${sayi(stats.minSira)} ile ${sayi(stats.maxSira)} arasında değişiyor.`,
);
}
if (karsilastirilan >= 5) {
// Yüzdeye iyelik eki gerekmeyen kalıp: "%7'sinde / %9'unda" gibi ek
// seçimi sayının okunuşuna bağlı, şablonda güvenle kurulamaz.
const kucOran = Math.round((sikilasan / karsilastirilan) * 100);
const buyOran = Math.round((gevseyen / karsilastirilan) * 100);
cumleler.push(
kucOran >= buyOran
? `${ONCEKI_YIL}'e göre taban sıralaması sayı olarak küçülen (yerleşmesi zorlaşan) program oranı %${kucOran}, büyüyen (kolaylaşan) %${buyOran}.`
: `${ONCEKI_YIL}'e göre taban sıralaması sayı olarak büyüyen (yerleşmesi kolaylaşan) program oranı %${buyOran}, küçülen (zorlaşan) %${kucOran}.`,
);
}
return cumleler.join(" ");
}
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
const bolum = getBolumBySlug(slug);
if (!bolum) return {};
@@ -237,6 +295,11 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
}
}
const { stats } = bolum;
// Sayfaya özgü, veriden türeyen özet cümle (GEO: yapay zekâ cevap motorları
// sayfa başına ilk metni saklar; 1011 katalog sayfasının aynı şablon cümleyle
// açılması hem alıntıyı hem Google'ın "ölçekli içerik" politikasını zorlar —
// docs/arastirma/2026-09-24-geo-seo). Rakamlar tablodakiyle aynı kaynaktan.
const veriOzeti = ilkSayfa ? bolumVeriOzeti(bolum, tumProgramlar) : null;
const hapSinifi =
"inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800";
@@ -325,9 +388,8 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
{ilkSayfa ? (
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
{bolum.ad} bölümünü açan {sayi(tumProgramlar.length)} üniversite
programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve
kontenjanlarını incele.{" "}
{veriOzeti} Aşağıdaki tabloda her programın taban puanı, başarı
sıralaması ve kontenjanı var.{" "}
<em>
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
netleşir.

View File

@@ -74,7 +74,8 @@ export function RehberMakale({ slug }: { slug: string }) {
{yazi.aciklama}
</p>
<p className="mt-3 text-xs text-slate-500">
Yayın: <time dateTime={yazi.tarih}>{trTarih(yazi.tarih)}</time>
Yazan: KolayTercih Ekibi · Yayın:{" "}
<time dateTime={yazi.tarih}>{trTarih(yazi.tarih)}</time>
{yazi.guncelleme ? (
<>
{" · "}Güncelleme:{" "}

View File

@@ -153,20 +153,38 @@ export function articleJsonLd(opts: {
datePublished: string;
/** Verilmezse yayın tarihi kullanılır (yazı hiç güncellenmemiş). */
dateModified?: string;
/** Mutlak görsel adresi; verilmezse yolun opengraph-image'ı kullanılır. */
image?: string;
}): JsonLdData {
const url = `${SITE_URL}${opts.path}`;
return {
"@context": "https://schema.org",
"@type": "Article",
headline: opts.title,
description: opts.description,
url: `${SITE_URL}${opts.path}`,
url,
mainEntityOfPage: url,
image: opts.image ?? `${url}/opengraph-image`,
datePublished: opts.datePublished,
dateModified: opts.dateModified ?? opts.datePublished,
inLanguage: "tr",
// Yazar kurum olarak verilir: yazılar tek imzayla değil ekipçe yazılıp
// resmî kaynakla (ÖSYM/YÖK) teyit ediliyor; görünür yazar satırı
// rehber-makale.tsx'te aynı adı kullanır. Kişi adı yayımlanacaksa
// burası Person'a çevrilir (docs/arastirma/2026-09-24-geo-seo).
author: {
"@type": "Organization",
name: `${SITE_NAME} Ekibi`,
url: SITE_URL,
},
publisher: {
"@type": "Organization",
name: SITE_NAME,
url: SITE_URL,
logo: {
"@type": "ImageObject",
url: `${SITE_URL}/kolay-tercih-mark.png`,
},
},
};
}