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 { ImageResponse } from "next/og";
import { loadSiteOgFonts } from "@/lib/og-fonts"; import { loadSiteOgFonts, OG_AILE } from "@/lib/og-fonts";
export const alt = export const alt =
"KolayTercih — YKS tercihin, gerçek YÖK Atlas verisiyle. Sıralamanı gir, dengeli 24 tercihlik listeni al."; "KolayTercih — YKS tercihin, gerçek YÖK Atlas verisiyle. Sıralamanı gir, dengeli 24 tercihlik listeni al.";
@@ -10,7 +10,6 @@ export default async function OpenGraphImage() {
const fonts = await loadSiteOgFonts(); const fonts = await loadSiteOgFonts();
return new ImageResponse( return new ImageResponse(
(
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -34,7 +33,7 @@ export default async function OpenGraphImage() {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
fontFamily: "Bricolage Grotesque", fontFamily: OG_AILE.bricolage,
letterSpacing: "-0.05em", letterSpacing: "-0.05em",
color: "#0f172a", color: "#0f172a",
}} }}
@@ -59,7 +58,7 @@ export default async function OpenGraphImage() {
style={{ style={{
display: "flex", display: "flex",
marginTop: 28, marginTop: 28,
fontFamily: "Clarity City", fontFamily: OG_AILE.clarity,
fontSize: 34, fontSize: 34,
fontWeight: 600, fontWeight: 600,
lineHeight: 1.25, lineHeight: 1.25,
@@ -74,7 +73,7 @@ export default async function OpenGraphImage() {
style={{ style={{
display: "flex", display: "flex",
marginTop: 16, marginTop: 16,
fontFamily: "Clarity City", fontFamily: OG_AILE.clarity,
fontSize: 26, fontSize: 26,
fontWeight: 300, fontWeight: 300,
lineHeight: 1.3, lineHeight: 1.3,
@@ -85,8 +84,7 @@ export default async function OpenGraphImage() {
Sıralamanı gir · Dengeli 24 tercihlik listeni al Sıralamanı gir · Dengeli 24 tercihlik listeni al
</div> </div>
</div> </div>
</div> </div>,
),
{ ...size, fonts }, { ...size, fonts },
); );
} }

View File

@@ -1,6 +1,6 @@
import { ImageResponse } from "next/og"; import { ImageResponse } from "next/og";
import { getRehber } from "@/lib/rehber"; import { getRehber } from "@/lib/rehber";
import { loadRehberOgFonts } from "@/lib/og-fonts"; import { loadRehberOgFonts, OG_AILE } from "@/lib/og-fonts";
import { import {
rehberBasliginiBol, rehberBasliginiBol,
rehberMetniniParcala, rehberMetniniParcala,
@@ -19,10 +19,30 @@ export default async function RehberOpenGraphImage({
const yazi = getRehber(slug); const yazi = getRehber(slug);
const baslik = yazi?.baslik ?? "Tercih Rehberi"; const baslik = yazi?.baslik ?? "Tercih Rehberi";
const { vurgu, kalan } = rehberBasliginiBol(baslik); 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(); const fonts = await loadRehberOgFonts();
return new ImageResponse( return new ImageResponse(
(
<div <div
style={{ style={{
position: "relative", position: "relative",
@@ -32,18 +52,34 @@ export default async function RehberOpenGraphImage({
overflow: "hidden", overflow: "hidden",
background: "white", background: "white",
color: "#2563eb", color: "#2563eb",
fontFamily: "Bricolage Grotesque", fontFamily: OG_AILE.bricolage,
}} }}
> >
{[ {[
[960, 50, 0.16], [980, 50, 0.28], [1020, 50, 0.12], [960, 50, 0.16],
[940, 70, 0.24], [980, 70, 0.12], [1000, 70, 0.3], [980, 50, 0.28],
[1060, 70, 0.18], [960, 90, 0.12], [1000, 90, 0.2], [1020, 50, 0.12],
[1040, 90, 0.1], [1080, 90, 0.26], [1080, 110, 0.14], [940, 70, 0.24],
[1120, 110, 0.22], [1140, 110, 0.12], [60, 490, 0.12], [980, 70, 0.12],
[100, 490, 0.25], [120, 490, 0.14], [40, 510, 0.24], [1000, 70, 0.3],
[80, 510, 0.12], [120, 510, 0.3], [160, 510, 0.16], [1060, 70, 0.18],
[60, 530, 0.14], [100, 530, 0.22], [140, 530, 0.1], [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], [180, 530, 0.27],
].map(([left, top, opacity], i) => ( ].map(([left, top, opacity], i) => (
<span <span
@@ -110,7 +146,7 @@ export default async function RehberOpenGraphImage({
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
fontFamily: "Clarity City", fontFamily: OG_AILE.clarity,
fontSize: 20, fontSize: 20,
fontWeight: 700, fontWeight: 700,
letterSpacing: "0.18em", letterSpacing: "0.18em",
@@ -121,35 +157,42 @@ export default async function RehberOpenGraphImage({
<span>Tercih Rehberi</span> <span>Tercih Rehberi</span>
</div> </div>
{/*
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 <div
style={{ style={{
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
maxWidth: 980, maxWidth: 1000,
fontFamily: "Cal Sans", fontSize: baslikBoyutu,
fontSize: baslik.length > 70 ? 48 : baslik.length > 52 ? 54 : 62, lineHeight: 1.06,
fontWeight: 400,
lineHeight: 1.02,
letterSpacing: "-0.025em",
color: "#2563eb", color: "#2563eb",
}} }}
> >
<span {kelimeler.map((kelime, i) => (
style={{
display: "flex",
fontFamily: "Bricolage Grotesque",
color: "#ff5a00",
fontWeight: 600,
letterSpacing: "-0.04em",
}}
>
{rehberMetniniParcala(vurgu).map((parca, i) =>
/^\d/.test(parca) ? (
<span <span
key={i} key={i}
style={{ style={{
display: "flex", display: "flex",
fontFamily: "Bricolage Grotesque", 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(kelime.metin).map((parca, j) =>
/^\d/.test(parca) ? (
<span
key={j}
style={{
display: "flex",
fontFamily: OG_AILE.bricolage,
fontStyle: "italic", fontStyle: "italic",
fontWeight: 800, fontWeight: 800,
}} }}
@@ -161,32 +204,7 @@ export default async function RehberOpenGraphImage({
), ),
)} )}
</span> </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
@@ -210,7 +228,7 @@ export default async function RehberOpenGraphImage({
style={{ style={{
display: "flex", display: "flex",
marginLeft: 2, marginLeft: 2,
fontFamily: "Bricolage Grotesque", fontFamily: OG_AILE.bricolage,
fontSize: 24, fontSize: 24,
letterSpacing: "-0.05em", letterSpacing: "-0.05em",
color: "#2563eb", color: "#2563eb",
@@ -221,8 +239,7 @@ export default async function RehberOpenGraphImage({
</span> </span>
</div> </div>
</div> </div>
</div> </div>,
),
{ ...size, fonts }, { ...size, fonts },
); );
} }

View File

@@ -21,7 +21,24 @@ type OgFont = {
style: "normal" | "italic"; 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( async function latinPair(
name: string, name: string,
weight: OgFont["weight"], weight: OgFont["weight"],
@@ -35,7 +52,7 @@ async function latinPair(
]); ]);
return [ return [
{ name, data: latin, weight, style }, { 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 { 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_SIZE = { width: 1200, height: 630 };
export const OG_CONTENT_TYPE = "image/png"; export const OG_CONTENT_TYPE = "image/png";
@@ -51,7 +51,6 @@ export async function createOgSayfaImage(opts: {
const { eyebrow, title, description, logo } = opts; const { eyebrow, title, description, logo } = opts;
return new ImageResponse( return new ImageResponse(
(
<div <div
style={{ style={{
position: "relative", position: "relative",
@@ -61,7 +60,7 @@ export async function createOgSayfaImage(opts: {
overflow: "hidden", overflow: "hidden",
background: "white", background: "white",
color: "#2563eb", color: "#2563eb",
fontFamily: "Bricolage Grotesque", fontFamily: OG_AILE.bricolage,
}} }}
> >
{PIXELS.map(([left, top, opacity], i) => ( {PIXELS.map(([left, top, opacity], i) => (
@@ -129,7 +128,7 @@ export async function createOgSayfaImage(opts: {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
fontFamily: "Clarity City", fontFamily: OG_AILE.clarity,
fontSize: 20, fontSize: 20,
fontWeight: 700, fontWeight: 700,
letterSpacing: "0.18em", letterSpacing: "0.18em",
@@ -171,7 +170,7 @@ export async function createOgSayfaImage(opts: {
style={{ style={{
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
fontFamily: "Cal Sans", fontFamily: OG_AILE.calSans,
fontSize: baslikBoyutu(title), fontSize: baslikBoyutu(title),
fontWeight: 400, fontWeight: 400,
lineHeight: 1.05, lineHeight: 1.05,
@@ -185,7 +184,7 @@ export async function createOgSayfaImage(opts: {
<div <div
style={{ style={{
display: "flex", display: "flex",
fontFamily: "Clarity City", fontFamily: OG_AILE.clarity,
fontSize: 26, fontSize: 26,
fontWeight: 700, fontWeight: 700,
lineHeight: 1.35, lineHeight: 1.35,
@@ -219,7 +218,7 @@ export async function createOgSayfaImage(opts: {
style={{ style={{
display: "flex", display: "flex",
marginLeft: 2, marginLeft: 2,
fontFamily: "Bricolage Grotesque", fontFamily: OG_AILE.bricolage,
fontSize: 24, fontSize: 24,
letterSpacing: "-0.05em", letterSpacing: "-0.05em",
color: "#2563eb", color: "#2563eb",
@@ -230,8 +229,7 @@ export async function createOgSayfaImage(opts: {
</span> </span>
</div> </div>
</div> </div>
</div> </div>,
),
{ ...OG_SIZE, fonts }, { ...OG_SIZE, fonts },
); );
} }