diff --git a/src/app/bolum/[slug]/opengraph-image.tsx b/src/app/bolum/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..1703927 --- /dev/null +++ b/src/app/bolum/[slug]/opengraph-image.tsx @@ -0,0 +1,31 @@ +import { getAllBolumler } from "@/lib/katalog"; +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Bölüm taban puanları ve sıralamaları — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const bolum = getAllBolumler().find((b) => b.slug === slug); + const ad = bolum?.ad ?? "Bölüm"; + const programSayisi = bolum?.programSayisi; + const sayi = + programSayisi != null + ? `${programSayisi.toLocaleString("tr-TR")} program · ` + : ""; + + return createOgSayfaImage({ + eyebrow: "Bölüm", + title: `${ad} taban puanları`, + description: `${sayi}2025 yerleştirme verileri · 2026 tercih rehberi`, + }); +} diff --git a/src/app/bolumler/opengraph-image.tsx b/src/app/bolumler/opengraph-image.tsx new file mode 100644 index 0000000..9df4d77 --- /dev/null +++ b/src/app/bolumler/opengraph-image.tsx @@ -0,0 +1,18 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = + "Tüm bölümlerin taban puanları ve sıralamaları 2026 — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Katalog", + title: "Tüm bölümlerin taban puanları ve sıralamaları", + description: "Lisans ve önlisans programları · 2025 yerleştirme verileri", + }); +} diff --git a/src/app/giris/opengraph-image.tsx b/src/app/giris/opengraph-image.tsx new file mode 100644 index 0000000..8856b9b --- /dev/null +++ b/src/app/giris/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Giriş — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Hesap", + title: "Giriş yap", + description: "Tercih listene ve soru hakkına kaldığın yerden devam et", + }); +} diff --git a/src/app/gizlilik/opengraph-image.tsx b/src/app/gizlilik/opengraph-image.tsx new file mode 100644 index 0000000..bc2553c --- /dev/null +++ b/src/app/gizlilik/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Gizlilik ve KVKK — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Yasal", + title: "Gizlilik ve KVKK", + description: "Hangi veriler toplanır, nasıl işlenir ve haklarınız nelerdir", + }); +} diff --git a/src/app/iletisim/opengraph-image.tsx b/src/app/iletisim/opengraph-image.tsx new file mode 100644 index 0000000..dd960dd --- /dev/null +++ b/src/app/iletisim/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "İletişim — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Destek", + title: "İletişim", + description: "Sorular, geri bildirim ve iade talepleri için bize yaz", + }); +} diff --git a/src/app/kosullar/opengraph-image.tsx b/src/app/kosullar/opengraph-image.tsx new file mode 100644 index 0000000..a0a024f --- /dev/null +++ b/src/app/kosullar/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Kullanım ve Satış Koşulları — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Yasal", + title: "Kullanım ve satış koşulları", + description: "Hizmet kapsamı, ödeme, iade politikası ve sorumluluk sınırları", + }); +} diff --git a/src/app/listem/opengraph-image.tsx b/src/app/listem/opengraph-image.tsx new file mode 100644 index 0000000..a544efc --- /dev/null +++ b/src/app/listem/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Listem — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Hesabım", + title: "Listem", + description: "Kişisel tercih listen, risk analizi ve soru hakkı", + }); +} diff --git a/src/app/meraklisina/opengraph-image.tsx b/src/app/meraklisina/opengraph-image.tsx new file mode 100644 index 0000000..9fecffc --- /dev/null +++ b/src/app/meraklisina/opengraph-image.tsx @@ -0,0 +1,18 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Meraklısına: KolayTercih nasıl çalışır?"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Meraklısına", + title: "KolayTercih nasıl çalışır?", + description: + "Dilimler, risk renkleri ve daralan filtreler — öğrenci diliyle", + }); +} diff --git a/src/app/odeme/sonuc/opengraph-image.tsx b/src/app/odeme/sonuc/opengraph-image.tsx new file mode 100644 index 0000000..38018cc --- /dev/null +++ b/src/app/odeme/sonuc/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Ödeme sonucu — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Ödeme", + title: "Ödeme sonucu", + description: "Siparişinin durumunu buradan takip edebilirsin", + }); +} diff --git a/src/app/paket/opengraph-image.tsx b/src/app/paket/opengraph-image.tsx new file mode 100644 index 0000000..0841b38 --- /dev/null +++ b/src/app/paket/opengraph-image.tsx @@ -0,0 +1,18 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Tercih Dönemi Paketi — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Paket", + title: "Tercih Dönemi Paketi", + description: + "Sıralamana özel 24 tercihlik liste, risk analizi ve soru hakkı", + }); +} diff --git a/src/app/rapor/yazdir/opengraph-image.tsx b/src/app/rapor/yazdir/opengraph-image.tsx new file mode 100644 index 0000000..2a9545d --- /dev/null +++ b/src/app/rapor/yazdir/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Tercih raporu — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Rapor", + title: "Tercih raporu", + description: "Yazdırmaya hazır kişisel tercih listesi özeti", + }); +} diff --git a/src/app/rehber/opengraph-image.tsx b/src/app/rehber/opengraph-image.tsx new file mode 100644 index 0000000..8879648 --- /dev/null +++ b/src/app/rehber/opengraph-image.tsx @@ -0,0 +1,18 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Tercih Rehberi 2026 — YKS tercih dönemi yazıları — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Rehber", + title: "YKS tercih dönemi rehberi", + description: + "Tercih listesi, ölü tercih, ek yerleştirme ve sıralama — öğrenci diliyle", + }); +} diff --git a/src/app/sonuc/opengraph-image.tsx b/src/app/sonuc/opengraph-image.tsx new file mode 100644 index 0000000..195547d --- /dev/null +++ b/src/app/sonuc/opengraph-image.tsx @@ -0,0 +1,17 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Tercih listeni kur — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Tercih listesi", + title: "Tercih listeni kur", + description: "Sıralamana göre dengeli 24 tercihlik listen hazırlanıyor", + }); +} diff --git a/src/app/universite/[slug]/opengraph-image.tsx b/src/app/universite/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..8a54680 --- /dev/null +++ b/src/app/universite/[slug]/opengraph-image.tsx @@ -0,0 +1,33 @@ +import { getAllUniversiteler } from "@/lib/katalog"; +import { trBaslikDuzeni } from "@/lib/slug"; +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = "Üniversite taban puanları ve sıralamaları — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const uni = getAllUniversiteler().find((u) => u.slug === slug); + const ad = uni?.ad ?? "Üniversite"; + const parcalar: string[] = []; + if (uni?.il) parcalar.push(trBaslikDuzeni(uni.il)); + if (uni?.programSayisi != null) { + parcalar.push(`${uni.programSayisi.toLocaleString("tr-TR")} program`); + } + parcalar.push("2025 yerleştirme verileri"); + + return createOgSayfaImage({ + eyebrow: "Üniversite", + title: `${ad} taban puanları`, + description: parcalar.join(" · "), + }); +} diff --git a/src/app/universiteler/opengraph-image.tsx b/src/app/universiteler/opengraph-image.tsx new file mode 100644 index 0000000..07fb32c --- /dev/null +++ b/src/app/universiteler/opengraph-image.tsx @@ -0,0 +1,18 @@ +import { + createOgSayfaImage, + OG_CONTENT_TYPE, + OG_SIZE, +} from "@/lib/og-sayfa"; + +export const alt = + "Üniversitelerin taban puanları ve sıralamaları 2026 — KolayTercih"; +export const size = OG_SIZE; +export const contentType = OG_CONTENT_TYPE; + +export default async function Image() { + return createOgSayfaImage({ + eyebrow: "Katalog", + title: "Üniversitelerin taban puanları ve sıralamaları", + description: "Devlet ve vakıf üniversiteleri · 2025 yerleştirme verileri", + }); +} diff --git a/src/lib/og-fonts.ts b/src/lib/og-fonts.ts index c974ef4..543a49d 100644 --- a/src/lib/og-fonts.ts +++ b/src/lib/og-fonts.ts @@ -39,91 +39,105 @@ async function latinPair( ]; } +let siteOgFontsPromise: Promise | null = null; +let rehberOgFontsPromise: Promise | null = null; + /** Ana sayfa OG: Bricolage (marka) + ClarityCity (gövde). */ -export async function loadSiteOgFonts(): Promise { - const [bricolage, clarityLight, claritySemiBold] = await Promise.all([ - Promise.all([ - latinPair( - "Bricolage Grotesque", - 300, - "bricolage-grotesque-latin-300-normal.woff", - "bricolage-grotesque-latin-ext-300-normal.woff", - ), - latinPair( - "Bricolage Grotesque", - 600, - "bricolage-grotesque-latin-600-normal.woff", - "bricolage-grotesque-latin-ext-600-normal.woff", - ), - ]).then((pairs) => pairs.flat()), - loadFont("ClarityCity-Light.ttf"), - loadFont("ClarityCity-SemiBold.ttf"), - ]); +export function loadSiteOgFonts(): Promise { + if (!siteOgFontsPromise) { + siteOgFontsPromise = (async () => { + const [bricolage, clarityLight, claritySemiBold] = await Promise.all([ + Promise.all([ + latinPair( + "Bricolage Grotesque", + 300, + "bricolage-grotesque-latin-300-normal.woff", + "bricolage-grotesque-latin-ext-300-normal.woff", + ), + latinPair( + "Bricolage Grotesque", + 600, + "bricolage-grotesque-latin-600-normal.woff", + "bricolage-grotesque-latin-ext-600-normal.woff", + ), + ]).then((pairs) => pairs.flat()), + loadFont("ClarityCity-Light.ttf"), + loadFont("ClarityCity-SemiBold.ttf"), + ]); - return [ - ...bricolage, - { - name: "Clarity City", - data: clarityLight, - weight: 300, - style: "normal", - }, - { - name: "Clarity City", - data: claritySemiBold, - weight: 600, - style: "normal", - }, - ]; + return [ + ...bricolage, + { + name: "Clarity City", + data: clarityLight, + weight: 300, + style: "normal" as const, + }, + { + name: "Clarity City", + data: claritySemiBold, + weight: 600, + style: "normal" as const, + }, + ]; + })(); + } + return siteOgFontsPromise; } -/** Rehber kapak OG: ClarityCity + Cal Sans + Bricolage. */ -export async function loadRehberOgFonts(): Promise { - const [clarityBold, calSans, light, semiBold, extraBold] = await Promise.all([ - loadFont("ClarityCity-Bold.ttf"), - latinPair( - "Cal Sans", - 400, - "cal-sans-latin-400-normal.woff", - "cal-sans-latin-ext-400-normal.woff", - ), - latinPair( - "Bricolage Grotesque", - 300, - "bricolage-grotesque-latin-300-normal.woff", - "bricolage-grotesque-latin-ext-300-normal.woff", - ), - latinPair( - "Bricolage Grotesque", - 600, - "bricolage-grotesque-latin-600-normal.woff", - "bricolage-grotesque-latin-ext-600-normal.woff", - ), - latinPair( - "Bricolage Grotesque", - 800, - "bricolage-grotesque-latin-800-normal.woff", - "bricolage-grotesque-latin-ext-800-normal.woff", - ), - ]); +/** Rehber / sayfa OG: ClarityCity + Cal Sans + Bricolage. */ +export function loadRehberOgFonts(): Promise { + if (!rehberOgFontsPromise) { + rehberOgFontsPromise = (async () => { + const [clarityBold, calSans, light, semiBold, extraBold] = + await Promise.all([ + loadFont("ClarityCity-Bold.ttf"), + latinPair( + "Cal Sans", + 400, + "cal-sans-latin-400-normal.woff", + "cal-sans-latin-ext-400-normal.woff", + ), + latinPair( + "Bricolage Grotesque", + 300, + "bricolage-grotesque-latin-300-normal.woff", + "bricolage-grotesque-latin-ext-300-normal.woff", + ), + latinPair( + "Bricolage Grotesque", + 600, + "bricolage-grotesque-latin-600-normal.woff", + "bricolage-grotesque-latin-ext-600-normal.woff", + ), + latinPair( + "Bricolage Grotesque", + 800, + "bricolage-grotesque-latin-800-normal.woff", + "bricolage-grotesque-latin-ext-800-normal.woff", + ), + ]); - // Sitede italic kullanılıyor; ayrı italic dosya yok — aynı kesimi italic olarak kaydet. - const extraBoldItalic = extraBold.map((font) => ({ - ...font, - style: "italic" as const, - })); + // Sitede italic kullanılıyor; ayrı italic dosya yok — aynı kesimi italic olarak kaydet. + const extraBoldItalic = extraBold.map((font) => ({ + ...font, + style: "italic" as const, + })); - return [ - { - name: "Clarity City", - data: clarityBold, - weight: 700, - style: "normal", - }, - ...calSans, - ...light, - ...semiBold, - ...extraBold, - ...extraBoldItalic, - ]; + return [ + { + name: "Clarity City", + data: clarityBold, + weight: 700, + style: "normal" as const, + }, + ...calSans, + ...light, + ...semiBold, + ...extraBold, + ...extraBoldItalic, + ]; + })(); + } + return rehberOgFontsPromise; } diff --git a/src/lib/og-sayfa.tsx b/src/lib/og-sayfa.tsx new file mode 100644 index 0000000..e248238 --- /dev/null +++ b/src/lib/og-sayfa.tsx @@ -0,0 +1,216 @@ +import { ImageResponse } from "next/og"; +import { loadRehberOgFonts } from "@/lib/og-fonts"; + +export const OG_SIZE = { width: 1200, height: 630 }; +export const OG_CONTENT_TYPE = "image/png"; + +const PIXELS: [number, number, number][] = [ + [960, 50, 0.16], + [980, 50, 0.28], + [1020, 50, 0.12], + [940, 70, 0.24], + [980, 70, 0.12], + [1000, 70, 0.3], + [1060, 70, 0.18], + [960, 90, 0.12], + [1000, 90, 0.2], + [1040, 90, 0.1], + [1080, 90, 0.26], + [1080, 110, 0.14], + [1120, 110, 0.22], + [1140, 110, 0.12], + [60, 490, 0.12], + [100, 490, 0.25], + [120, 490, 0.14], + [40, 510, 0.24], + [80, 510, 0.12], + [120, 510, 0.3], + [160, 510, 0.16], + [60, 530, 0.14], + [100, 530, 0.22], + [140, 530, 0.1], + [180, 530, 0.27], +]; + +function baslikBoyutu(title: string): number { + if (title.length > 70) return 44; + if (title.length > 52) return 50; + if (title.length > 36) return 56; + return 62; +} + +/** Sayfa OG kartı — rehber kapağıyla aynı dil: eyebrow, başlık, marka. */ +export async function createOgSayfaImage(opts: { + eyebrow: string; + title: string; + description?: string; +}): Promise { + const fonts = await loadRehberOgFonts(); + const { eyebrow, title, description } = opts; + + return new ImageResponse( + ( +
+ {PIXELS.map(([left, top, opacity], i) => ( + + ))} + + +
+ +
+
+ {eyebrow} +
+ +
+
+ {title} +
+ {description ? ( +
+ {description} +
+ ) : null} +
+ +
+ + + + + + + Kolay + Tercih + +
+
+
+ ), + { ...OG_SIZE, fonts }, + ); +}