fix(og): rehber kapak başlığı taşmıyor, İ/ş/ğ doğru yazı tipinden geliyor

İki kök neden: rakamlı başlıklarda karışık flex span satır kaydıramıyordu
(başlık artık kelime kelime kutulanıyor); Satori aynı aile adıyla kaydedilen
latin/latin-ext dosyalarından yalnızca birini kullanıyordu (ext alt-kümeleri
'<ad> Ext' adıyla kayıtlı, şablonlar OG_AILE virgüllü aile listesini veriyor).

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 9d90436027
commit 6094066c5e
4 changed files with 400 additions and 370 deletions

View File

@@ -1,5 +1,5 @@
import { ImageResponse } from "next/og";
import { loadSiteOgFonts } from "@/lib/og-fonts";
import { loadSiteOgFonts, OG_AILE } from "@/lib/og-fonts";
export const alt =
"KolayTercih — YKS tercihin, gerçek YÖK Atlas verisiyle. Sıralamanı gir, dengeli 24 tercihlik listeni al.";
@@ -10,83 +10,81 @@ export default async function OpenGraphImage() {
const fonts = await loadSiteOgFonts();
return new ImageResponse(
(
<div
style={{
display: "flex",
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
background: "white",
}}
>
<div
style={{
display: "flex",
width: "100%",
height: "100%",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "white",
maxWidth: 920,
padding: "0 48px",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
maxWidth: 920,
padding: "0 48px",
fontFamily: OG_AILE.bricolage,
letterSpacing: "-0.05em",
color: "#0f172a",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
fontFamily: "Bricolage Grotesque",
letterSpacing: "-0.05em",
color: "#0f172a",
}}
<svg
width="64"
height="64"
viewBox="0 0 180 180"
style={{ display: "flex", marginRight: 4 }}
>
<svg
width="64"
height="64"
viewBox="0 0 180 180"
style={{ display: "flex", marginRight: 4 }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span style={{ display: "flex", fontSize: 72, lineHeight: 1 }}>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span style={{ display: "flex", fontSize: 72, lineHeight: 1 }}>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
<div
style={{
display: "flex",
marginTop: 28,
fontFamily: "Clarity City",
fontSize: 34,
fontWeight: 600,
lineHeight: 1.25,
textAlign: "center",
color: "#1e293b",
}}
>
YKS tercihin, gerçek YÖK Atlas verisiyle
</div>
<div
style={{
display: "flex",
marginTop: 28,
fontFamily: OG_AILE.clarity,
fontSize: 34,
fontWeight: 600,
lineHeight: 1.25,
textAlign: "center",
color: "#1e293b",
}}
>
YKS tercihin, gerçek YÖK Atlas verisiyle
</div>
<div
style={{
display: "flex",
marginTop: 16,
fontFamily: "Clarity City",
fontSize: 26,
fontWeight: 300,
lineHeight: 1.3,
textAlign: "center",
color: "#64748b",
}}
>
Sıralamanı gir · Dengeli 24 tercihlik listeni al
</div>
<div
style={{
display: "flex",
marginTop: 16,
fontFamily: OG_AILE.clarity,
fontSize: 26,
fontWeight: 300,
lineHeight: 1.3,
textAlign: "center",
color: "#64748b",
}}
>
Sıralamanı gir · Dengeli 24 tercihlik listeni al
</div>
</div>
),
</div>,
{ ...size, fonts },
);
}

View File

@@ -1,6 +1,6 @@
import { ImageResponse } from "next/og";
import { getRehber } from "@/lib/rehber";
import { loadRehberOgFonts } from "@/lib/og-fonts";
import { loadRehberOgFonts, OG_AILE } from "@/lib/og-fonts";
import {
rehberBasliginiBol,
rehberMetniniParcala,
@@ -19,137 +19,180 @@ export default async function RehberOpenGraphImage({
const yazi = getRehber(slug);
const baslik = yazi?.baslik ?? "Tercih Rehberi";
const { vurgu, kalan } = rehberBasliginiBol(baslik);
const kelimeler = [
...vurgu
.split(/\s+/)
.filter(Boolean)
.map((metin) => ({ metin, vurgu: true })),
...kalan
.split(/\s+/)
.filter(Boolean)
.map((metin) => ({ metin, vurgu: false })),
];
// Kademeler sayfadaki RehberKapak ile aynı mantık; 90+ karakter için bir
// kademe daha var — 1000px genişlikte üç satırı aşmasın.
const baslikBoyutu =
baslik.length > 90
? 44
: baslik.length > 70
? 48
: baslik.length > 52
? 54
: 62;
const kelimeAraligi = Math.round(baslikBoyutu * 0.26);
const fonts = await loadRehberOgFonts();
return new ImageResponse(
(
<div
style={{
position: "relative",
display: "flex",
width: "100%",
height: "100%",
overflow: "hidden",
background: "white",
color: "#2563eb",
fontFamily: OG_AILE.bricolage,
}}
>
{[
[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],
].map(([left, top, opacity], i) => (
<span
key={i}
style={{
position: "absolute",
display: "flex",
left,
top,
width: 14,
height: 14,
borderRadius: 4,
background: "#2563eb",
opacity,
}}
/>
))}
<span
style={{
position: "absolute",
display: "flex",
left: 1040,
top: 50,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<span
style={{
position: "absolute",
display: "flex",
left: 80,
top: 530,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
border: "2px solid rgba(37,99,235,0.15)",
}}
/>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
overflow: "hidden",
background: "white",
color: "#2563eb",
fontFamily: "Bricolage Grotesque",
padding: "58px 68px 52px",
}}
>
{[
[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],
].map(([left, top, opacity], i) => (
<span
key={i}
style={{
position: "absolute",
display: "flex",
left,
top,
width: 14,
height: 14,
borderRadius: 4,
background: "#2563eb",
opacity,
}}
/>
))}
<span
style={{
position: "absolute",
display: "flex",
left: 1040,
top: 50,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<span
style={{
position: "absolute",
display: "flex",
left: 80,
top: 530,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
border: "2px solid rgba(37,99,235,0.15)",
}}
/>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
padding: "58px 68px 52px",
alignItems: "center",
fontFamily: OG_AILE.clarity,
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
color: "#2563eb",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
fontFamily: "Clarity City",
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
color: "#2563eb",
}}
>
<span>Tercih Rehberi</span>
</div>
<span>Tercih Rehberi</span>
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
maxWidth: 980,
fontFamily: "Cal Sans",
fontSize: baslik.length > 70 ? 48 : baslik.length > 52 ? 54 : 62,
fontWeight: 400,
lineHeight: 1.02,
letterSpacing: "-0.025em",
color: "#2563eb",
}}
>
{/*
Satori satır kaydırmayı yalnızca düz metin düğümlerinde yapar;
rakam+metin karışık bir flex span tek satırda kalır ve parçalar
üst üste biner (2026'lı başlıklarda görülen bozulma). Bu yüzden
başlık kelime kelime ayrı kutulara bölünür: kaydırma kelime
sınırında olur, her kelime kendi rengini/yazı tipini taşır.
*/}
<div
style={{
display: "flex",
flexWrap: "wrap",
maxWidth: 1000,
fontSize: baslikBoyutu,
lineHeight: 1.06,
color: "#2563eb",
}}
>
{kelimeler.map((kelime, i) => (
<span
key={i}
style={{
display: "flex",
fontFamily: "Bricolage Grotesque",
color: "#ff5a00",
fontWeight: 600,
letterSpacing: "-0.04em",
marginRight: kelimeAraligi,
fontFamily: kelime.vurgu ? OG_AILE.bricolage : OG_AILE.calSans,
fontWeight: kelime.vurgu ? 600 : 400,
letterSpacing: kelime.vurgu ? "-0.04em" : "-0.025em",
color: kelime.vurgu ? "#ff5a00" : "#2563eb",
}}
>
{rehberMetniniParcala(vurgu).map((parca, i) =>
{rehberMetniniParcala(kelime.metin).map((parca, j) =>
/^\d/.test(parca) ? (
<span
key={i}
key={j}
style={{
display: "flex",
fontFamily: "Bricolage Grotesque",
fontFamily: OG_AILE.bricolage,
fontStyle: "italic",
fontWeight: 800,
}}
@@ -161,68 +204,42 @@ export default async function RehberOpenGraphImage({
),
)}
</span>
{kalan ? (
<span
style={{
display: "flex",
marginLeft: /^\s/.test(kalan) ? 12 : 0,
}}
>
{rehberMetniniParcala(kalan.trimStart()).map((parca, i) =>
/^\d/.test(parca) ? (
<span
key={i}
style={{
display: "flex",
fontFamily: "Bricolage Grotesque",
fontStyle: "italic",
fontWeight: 800,
}}
>
{parca}
</span>
) : (
parca
),
)}
</span>
) : null}
</div>
))}
</div>
<div
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<svg
width="38"
height="38"
viewBox="0 0 180 180"
style={{ display: "flex" }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginLeft: 2,
fontFamily: OG_AILE.bricolage,
fontSize: 24,
letterSpacing: "-0.05em",
color: "#2563eb",
}}
>
<svg
width="38"
height="38"
viewBox="0 0 180 180"
style={{ display: "flex" }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span
style={{
display: "flex",
marginLeft: 2,
fontFamily: "Bricolage Grotesque",
fontSize: 24,
letterSpacing: "-0.05em",
color: "#2563eb",
}}
>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
</div>
),
</div>,
{ ...size, fonts },
);
}

View File

@@ -21,7 +21,24 @@ type OgFont = {
style: "normal" | "italic";
};
/** latin + latin-ext çiftini aynı aile/ağırlıkta kaydeder (Türkçe glifler için). */
/**
* Satori aynı aile adı + ağırlık için TEK dosya seçer (`get()` en iyi eşleşen
* tek kaydı döndürür); iki alt-kümeyi aynı adla kaydetmek latin-ext dosyasını
* fiilen devre dışı bırakıyordu — İ/ş/ğ gibi glifler başka bir aileden
* (kayıt sırasına göre Clarity City) geliyor, ölçüm ile çizim ayrışıyor ve
* "İ" ile başlayan kelimeden sonraki boşluk yutuluyordu. Bu yüzden ext
* alt-kümesi `<ad> Ext` adıyla ayrı kaydedilir; şablonlar `fontFamily`'ye
* aşağıdaki OG_AILE değerlerini verir (Satori CSS gibi virgüllü METİN ister,
* dizi verilirse `split` hatasıyla çöker); glif ilk dosyada yoksa ikinciye
* geçer.
*/
export const OG_AILE = {
calSans: "Cal Sans, Cal Sans Ext",
bricolage: "Bricolage Grotesque, Bricolage Grotesque Ext",
clarity: "Clarity City",
} as const;
/** latin + latin-ext çiftini kaydeder; ext alt-kümesi `<ad> Ext` adını alır. */
async function latinPair(
name: string,
weight: OgFont["weight"],
@@ -35,7 +52,7 @@ async function latinPair(
]);
return [
{ name, data: latin, weight, style },
{ name, data: latinExt, weight, style },
{ name: `${name} Ext`, data: latinExt, weight, style },
];
}

View File

@@ -1,5 +1,5 @@
import { ImageResponse } from "next/og";
import { loadRehberOgFonts } from "@/lib/og-fonts";
import { loadRehberOgFonts, OG_AILE } from "@/lib/og-fonts";
export const OG_SIZE = { width: 1200, height: 630 };
export const OG_CONTENT_TYPE = "image/png";
@@ -51,187 +51,185 @@ export async function createOgSayfaImage(opts: {
const { eyebrow, title, description, logo } = opts;
return new ImageResponse(
(
<div
style={{
position: "relative",
display: "flex",
width: "100%",
height: "100%",
overflow: "hidden",
background: "white",
color: "#2563eb",
fontFamily: OG_AILE.bricolage,
}}
>
{PIXELS.map(([left, top, opacity], i) => (
<span
key={i}
style={{
position: "absolute",
display: "flex",
left,
top,
width: 14,
height: 14,
borderRadius: 4,
background: "#2563eb",
opacity,
}}
/>
))}
<span
style={{
position: "absolute",
display: "flex",
left: 1040,
top: 50,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<span
style={{
position: "absolute",
display: "flex",
left: 80,
top: 530,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
border: "2px solid rgba(37,99,235,0.15)",
}}
/>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
height: "100%",
overflow: "hidden",
background: "white",
color: "#2563eb",
fontFamily: "Bricolage Grotesque",
padding: "58px 68px 52px",
}}
>
{PIXELS.map(([left, top, opacity], i) => (
<span
key={i}
style={{
position: "absolute",
display: "flex",
left,
top,
width: 14,
height: 14,
borderRadius: 4,
background: "#2563eb",
opacity,
}}
/>
))}
<span
style={{
position: "absolute",
display: "flex",
left: 1040,
top: 50,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<span
style={{
position: "absolute",
display: "flex",
left: 80,
top: 530,
width: 14,
height: 14,
borderRadius: 4,
background: "#ff5a00",
opacity: 0.8,
}}
/>
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
border: "2px solid rgba(37,99,235,0.15)",
display: "flex",
alignItems: "center",
fontFamily: OG_AILE.clarity,
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
color: "#2563eb",
}}
/>
>
<span>{eyebrow}</span>
{logo ? (
<span
style={{
display: "flex",
marginLeft: "auto",
alignItems: "center",
justifyContent: "center",
width: 108,
height: 108,
borderRadius: 24,
background: "white",
border: "2px solid rgba(37,99,235,0.15)",
boxShadow: "0 8px 24px rgba(15,23,42,0.08)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logo} alt="" width={84} height={84} />
</span>
) : null}
</div>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
padding: "58px 68px 52px",
maxWidth: 980,
gap: 18,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
fontFamily: "Clarity City",
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.18em",
textTransform: "uppercase",
flexWrap: "wrap",
fontFamily: OG_AILE.calSans,
fontSize: baslikBoyutu(title),
fontWeight: 400,
lineHeight: 1.05,
letterSpacing: "-0.025em",
color: "#2563eb",
}}
>
<span>{eyebrow}</span>
{logo ? (
<span
style={{
display: "flex",
marginLeft: "auto",
alignItems: "center",
justifyContent: "center",
width: 108,
height: 108,
borderRadius: 24,
background: "white",
border: "2px solid rgba(37,99,235,0.15)",
boxShadow: "0 8px 24px rgba(15,23,42,0.08)",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={logo} alt="" width={84} height={84} />
</span>
) : null}
{title}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
maxWidth: 980,
gap: 18,
}}
>
{description ? (
<div
style={{
display: "flex",
flexWrap: "wrap",
fontFamily: "Cal Sans",
fontSize: baslikBoyutu(title),
fontWeight: 400,
lineHeight: 1.05,
letterSpacing: "-0.025em",
color: "#2563eb",
fontFamily: OG_AILE.clarity,
fontSize: 26,
fontWeight: 700,
lineHeight: 1.35,
color: "#64748b",
maxWidth: 860,
}}
>
{title}
{description}
</div>
{description ? (
<div
style={{
display: "flex",
fontFamily: "Clarity City",
fontSize: 26,
fontWeight: 700,
lineHeight: 1.35,
color: "#64748b",
maxWidth: 860,
}}
>
{description}
</div>
) : null}
</div>
) : null}
</div>
<div
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<svg
width="38"
height="38"
viewBox="0 0 180 180"
style={{ display: "flex" }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginLeft: 2,
fontFamily: OG_AILE.bricolage,
fontSize: 24,
letterSpacing: "-0.05em",
color: "#2563eb",
}}
>
<svg
width="38"
height="38"
viewBox="0 0 180 180"
style={{ display: "flex" }}
>
<rect x="27" y="56" width="57" height="57" rx="13" fill="#3b82f6" />
<rect x="91" y="29" width="56" height="56" rx="13" fill="#fdc7a7" />
<rect x="91" y="95" width="56" height="56" rx="13" fill="#ff5a00" />
</svg>
<span
style={{
display: "flex",
marginLeft: 2,
fontFamily: "Bricolage Grotesque",
fontSize: 24,
letterSpacing: "-0.05em",
color: "#2563eb",
}}
>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
<span style={{ fontWeight: 300 }}>Kolay</span>
<span style={{ fontWeight: 600 }}>Tercih</span>
</span>
</div>
</div>
),
</div>,
{ ...OG_SIZE, fonts },
);
}