Files
kolaytercih/src/lib/seo.tsx
bilalgursen 24d4bea32c
All checks were successful
Deploy / deploy (push) Successful in 6m16s
feat(geo): llms.txt, RSS, IndexNow, Article yazar/görsel, bölüm sayfasına veri özeti, vakıf bursu yazısı
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>
2026-09-24 17:42:29 +03:00

203 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from "next";
import { URUNLER } from "./credits";
/**
* Kanonik site adresi. Ödeme akışının kök adresiyle (`appUrl()`,
* src/lib/app-url.ts) karıştırılmamalı: o runtime'da okunur, bu ise metadata
* için build anında sabitlenir. .dockerignore .env* dosyalarını dışladığı için
* prod build burada deterministik olarak kolaytercih.com'a düşer.
*/
export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL ??
(process.env.NODE_ENV === "production"
? "https://kolaytercih.com"
: "http://localhost:3000");
export const SITE_NAME = "KolayTercih";
/**
* Katalog verisinin (data/yokatlas.db) en son yenilendiği gün. Sitemap lastmod,
* katalog sayfalarındaki görünür "son güncelleme" satırı buradan okunur.
* Yalnızca veri GERÇEKTEN değiştiğinde (ingest/refresh sonrası) elle güncelle;
* build zamanına bağlama — her deploy'da değişen lastmod sinyali değersizleşir.
*/
export const KATALOG_VERI_TARIHI = "2026-09-21";
type SayfaMetadataGirdi = {
title: string;
description: string;
/** Kanonik yol ("/bolumler"); metadataBase ile mutlak adrese çözülür. */
path: string;
/** Varsayılan "website"; rehber yazıları article alanlarını buradan verir. */
openGraph?: NonNullable<Metadata["openGraph"]>;
};
/**
* İndekslenebilir sayfaların metadata'sı için tek kaynak. Next, segmentler
* arası metadata'yı SIĞ birleştirir: sayfa `openGraph` vermezse kök layout'unki
* (ana sayfanın başlığı + og:url'i) olduğu gibi miras kalır; verirse nesnenin
* tamamı ezilir (siteName/locale kaybolur). Bu yüzden openGraph burada eksiksiz
* üretilir. twitter:title/description/image, openGraph'tan otomatik dolar.
*/
export function sayfaMetadata({
title,
description,
path,
openGraph,
}: SayfaMetadataGirdi): Metadata {
return {
title,
description,
alternates: { canonical: path },
openGraph: {
type: "website",
locale: "tr_TR",
siteName: SITE_NAME,
url: path,
title,
description,
...openGraph,
},
};
}
/**
* Üst segmentin dosya-tabanlı OG görselini (opengraph-image.tsx) alt rotaya
* elle bağlar. Gerekçe: alt rota `openGraph` tanımlayınca sığ birleştirme üst
* segmentten gelen `images`'ı da düşürür (ör. /bolum/[slug]/sayfa/[no]).
* Boyut, src/lib/og-sayfa.tsx içindeki OG_SIZE ile aynı olmalı.
*/
export function ustSegmentOgGorseli(path: string, alt: string) {
return [{ url: `${path}/opengraph-image`, width: 1200, height: 630, alt }];
}
type JsonLdData = Record<string, unknown>;
export function organizationJsonLd(): JsonLdData {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: SITE_NAME,
url: SITE_URL,
logo: `${SITE_URL}/kolay-tercih-mark.svg`,
contactPoint: {
"@type": "ContactPoint",
email: "destek@kolaytercih.com",
contactType: "customer support",
availableLanguage: "Turkish",
},
};
}
export function webSiteJsonLd(): JsonLdData {
return {
"@context": "https://schema.org",
"@type": "WebSite",
name: SITE_NAME,
url: SITE_URL,
inLanguage: "tr",
};
}
export function softwareApplicationJsonLd(): JsonLdData {
return {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: SITE_NAME,
applicationCategory: "EducationalApplication",
operatingSystem: "Web",
url: SITE_URL,
inLanguage: "tr",
offers: {
"@type": "Offer",
price: (URUNLER.paket.amountKurus / 100).toFixed(0),
priceCurrency: "TRY",
description: URUNLER.paket.label,
},
};
}
export function faqPageJsonLd(
faqs: readonly { question: string; answer: string }[],
): JsonLdData {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((f) => ({
"@type": "Question",
name: f.question,
acceptedAnswer: { "@type": "Answer", text: f.answer },
})),
};
}
export function breadcrumbJsonLd(
items: readonly { name: string; path: string }[],
): JsonLdData {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, i) => ({
"@type": "ListItem",
position: i + 1,
name: item.name,
item: `${SITE_URL}${item.path}`,
})),
};
}
export function articleJsonLd(opts: {
title: string;
description: string;
path: string;
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,
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`,
},
},
};
}
/** JSON-LD'yi güvenli gömer: `<` kaçışı script-injection'ı engeller. */
export function JsonLd({ data }: { data: JsonLdData }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
}}
/>
);
}