Frontmatter'a isteğe bağlı `guncelleme` alanı geldi ve uçtan uca akıyor: sitemap lastmod (yazı + /rehber dizini), Article JSON-LD dateModified, og:article modified_time ve makale başlığının altındaki görünür tarih satırı. `tarih`/`guncelleme` YYYY-AA-GG değilse ya da güncelleme yayından önceyse build kırılır. Tarih metni saat diliminden bağımsız `trTarih` ile üretilir. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
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-08-09";
|
||
|
||
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;
|
||
}): JsonLdData {
|
||
return {
|
||
"@context": "https://schema.org",
|
||
"@type": "Article",
|
||
headline: opts.title,
|
||
description: opts.description,
|
||
url: `${SITE_URL}${opts.path}`,
|
||
datePublished: opts.datePublished,
|
||
dateModified: opts.dateModified ?? opts.datePublished,
|
||
inLanguage: "tr",
|
||
publisher: {
|
||
"@type": "Organization",
|
||
name: SITE_NAME,
|
||
url: SITE_URL,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** 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"),
|
||
}}
|
||
/>
|
||
);
|
||
}
|