feat(landing): OG-картинка и метаданные

opengraph-image и twitter-image 1200×630 через next/og: знак, заголовок,
штамп «ЧТ · обжарка», лента недели с ЧТ/ПТ красным, без фото и дат.
Статичные TTF Golos Text и Martian Mono для Satori. Метаданные из
site.meta: title, description, openGraph, twitter, metadataBase
kvarta.unuchak.com, canonical. Иконка — SVG-знак из семи штрихов.
This commit is contained in:
2026-10-17 16:56:52 +03:00
parent d8d67c036f
commit 7f4f4a199e
8 changed files with 119 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#f3ebdd"/><g fill="#2a1810"><rect x="4" y="6" width="2.4" height="20"/><rect x="7.8" y="6" width="2.4" height="20"/><rect x="11.6" y="6" width="2.4" height="20"/><rect x="19.2" y="6" width="2.4" height="20"/><rect x="23" y="6" width="2.4" height="20"/><rect x="26.8" y="6" width="2.4" height="20"/></g><rect x="15.4" y="6" width="2.4" height="20" fill="#b3221b"/></svg>

After

Width:  |  Height:  |  Size: 465 B

+15
View File
@@ -11,8 +11,23 @@ const golos = Golos_Text({ subsets: ['cyrillic', 'latin'], variable: '--font-gol
const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font-martian', axes: ['wdth'], display: 'swap' });
export const metadata: Metadata = {
metadataBase: new URL('https://kvarta.unuchak.com'),
title: site.meta.title,
description: site.meta.description,
alternates: { canonical: '/' },
openGraph: {
type: 'website',
locale: 'ru_RU',
url: '/',
siteName: site.footer.name,
title: site.meta.ogTitle,
description: site.meta.ogDescription,
},
twitter: {
card: 'summary_large_image',
title: site.meta.ogTitle,
description: site.meta.ogDescription,
},
};
export const viewport: Viewport = {
Binary file not shown.
Binary file not shown.
Binary file not shown.
+101
View File
@@ -0,0 +1,101 @@
// OG 1200×630 по DESIGN.md: знак и слово, заголовок, штамп, лента недели ПН–ВС с ЧТ/ПТ красным.
// Без фото и без дат: картинка кешируется. Satori не умеет SVG-фильтры и variable-шрифты —
// штамп без «краски», шрифты — статичные TTF из ./og-fonts.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ImageResponse } from 'next/og';
import { CONTACT_PHONE } from '@/content/form';
import { site } from '@/content/site';
export const alt = site.meta.ogTitle;
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
const C = {
cream: '#f3ebdd',
ink: '#2a1810',
muted: '#5e4636',
rule: '#c9b595',
cherry: '#b3221b',
};
const DAYS = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'ВС'];
export default async function OpengraphImage() {
const dir = join(process.cwd(), 'src/app/og-fonts');
const font = (file: string) => readFile(join(dir, file));
const [golos900, golos500, martian700] = await Promise.all([font('golos-900.ttf'), font('golos-500.ttf'), font('martian-700.ttf')]);
const [roast, delivery, cup] = site.freshness.days;
return new ImageResponse(
(
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', background: C.cream, color: C.ink, padding: 64, fontFamily: 'Golos Text' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
<div style={{ display: 'flex', gap: 7 }}>
{DAYS.map((d, i) => (
<div key={d} style={{ width: 7, height: 44, background: i === 3 ? C.cherry : C.ink }} />
))}
</div>
<div style={{ fontSize: 56, fontWeight: 900, letterSpacing: -1 }}>{site.footer.name}</div>
</div>
<div style={{ marginTop: 32, fontSize: 76, fontWeight: 900, lineHeight: 0.95, letterSpacing: -2.5, maxWidth: 760 }}>
{site.hero.title}
</div>
</div>
<div
style={{
display: 'flex',
marginTop: 24,
padding: 6,
border: `5px solid ${C.cherry}`,
transform: 'rotate(-6deg)',
opacity: 0.92,
}}
>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', border: `2px solid ${C.cherry}`, padding: '18px 30px', color: C.cherry, fontFamily: 'Martian Mono' }}>
<div style={{ fontSize: 30, letterSpacing: 2 }}>{site.hero.stamp.toUpperCase()}</div>
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 'auto' }}>
<div style={{ display: 'flex', borderBottom: `2px solid ${C.ink}` }}>
{DAYS.map((d, i) => {
const hot = i === 3 || i === 4;
return (
<div key={d} style={{ display: 'flex', flex: 1, flexDirection: 'column', paddingBottom: 14, paddingLeft: i === 0 ? 0 : 16, borderLeft: i === 0 ? 'none' : `1px solid ${C.rule}` }}>
<div style={{ fontSize: 30, fontWeight: 900, color: hot ? C.cherry : C.ink }}>{d}</div>
</div>
);
})}
</div>
<div style={{ display: 'flex', height: 76, fontFamily: 'Martian Mono', fontSize: 18, color: C.cherry, letterSpacing: 1 }}>
<div style={{ display: 'flex', flex: 3 }} />
<div style={{ display: 'flex', flex: 1, paddingLeft: 16, paddingTop: 14 }}>{roast?.label.toUpperCase()}</div>
<div style={{ display: 'flex', flex: 3, flexDirection: 'column', paddingLeft: 16, paddingTop: 14 }}>
<div style={{ display: 'flex' }}>{delivery?.label.toUpperCase()}</div>
<div style={{ display: 'flex', marginTop: 10, height: 10, border: `2px solid ${C.cherry}`, borderTop: 'none' }} />
<div style={{ display: 'flex', marginTop: 8 }}>
{cup ? `${cup.day} · ${cup.label}`.toUpperCase() : ''}
</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 22, fontFamily: 'Martian Mono', fontSize: 20, color: C.muted }}>
<div>kvarta.unuchak.com</div>
<div>{CONTACT_PHONE.display}</div>
</div>
</div>
</div>
),
{
...size,
fonts: [
{ name: 'Golos Text', data: golos900, weight: 900, style: 'normal' },
{ name: 'Golos Text', data: golos500, weight: 500, style: 'normal' },
{ name: 'Martian Mono', data: martian700, weight: 700, style: 'normal' },
],
},
);
}
+1
View File
@@ -0,0 +1 @@
export { default, alt, size, contentType } from './opengraph-image';