mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-23 04:58:44 +02:00
Replace the raw logo OG image with a designed 1200×630 hero card generated at build time using satori + @resvg/resvg-js. - Add generate-og.ts script with branded dark card design (gradient orbs, geometric rings, logo, title, tagline) - Wire generation into build pipeline (runs before astro build) - Update meta tags with absolute URLs, dimensions, and twitter:image - Configure site URL (https://aryx.app) in astro.config.mjs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
314 lines
9.5 KiB
TypeScript
314 lines
9.5 KiB
TypeScript
import satori from 'satori';
|
|
import { Resvg } from '@resvg/resvg-js';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const scriptDir = import.meta.dir ?? fileURLToPath(new URL('.', import.meta.url));
|
|
const websiteDir = join(scriptDir, '..');
|
|
|
|
const fontUrls = {
|
|
instrumentSerif:
|
|
'https://fonts.gstatic.com/s/instrumentserif/v5/jizBRFtNs2ka5fXjeivQ4LroWlx-2zI.ttf',
|
|
outfit400:
|
|
'https://fonts.gstatic.com/s/outfit/v15/QGYyz_MVcBeNP4NjuGObqx1XmO1I4TC1C4E.ttf',
|
|
outfit300:
|
|
'https://fonts.gstatic.com/s/outfit/v15/QGYyz_MVcBeNP4NjuGObqx1XmO1I4W61C4E.ttf',
|
|
};
|
|
|
|
const colors = {
|
|
deep: '#08080a',
|
|
border: '#2a2a34',
|
|
warm50: '#ede9e2',
|
|
warm300: '#a09a90',
|
|
brand: '#245CF9',
|
|
brandBright: '#248CFD',
|
|
accent: '#8A29E6',
|
|
};
|
|
|
|
type SatoriNode = string | SatoriElement;
|
|
interface SatoriElement {
|
|
type: string;
|
|
props: Record<string, unknown> & { children?: SatoriNode | SatoriNode[] };
|
|
}
|
|
|
|
function toDataUri(filePath: string, mime: string): string {
|
|
return `data:${mime};base64,${readFileSync(filePath).toString('base64')}`;
|
|
}
|
|
|
|
function orb(
|
|
top: number | undefined,
|
|
bottom: number | undefined,
|
|
left: number | undefined,
|
|
right: number | undefined,
|
|
size: number,
|
|
color: string,
|
|
opacity: number,
|
|
spread = 0.7,
|
|
): SatoriElement {
|
|
return {
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
position: 'absolute',
|
|
...(top !== undefined && { top }),
|
|
...(bottom !== undefined && { bottom }),
|
|
...(left !== undefined && { left }),
|
|
...(right !== undefined && { right }),
|
|
width: size,
|
|
height: size,
|
|
borderRadius: '50%',
|
|
background: `radial-gradient(circle, ${color.replace(')', `,${opacity})`).replace('rgb', 'rgba')} 0%, ${color.replace(')', ',0.02)').replace('rgb', 'rgba')} ${spread * 100}%, transparent 100%)`,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function dot(top: number, left: number, size: number, color: string): SatoriElement {
|
|
return {
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
position: 'absolute',
|
|
top,
|
|
left,
|
|
width: size,
|
|
height: size,
|
|
borderRadius: '50%',
|
|
backgroundColor: color,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function generate() {
|
|
console.log('Generating OG image…');
|
|
|
|
const [instrumentSerif, outfit400, outfit300] = await Promise.all(
|
|
Object.values(fontUrls).map((url) => fetch(url).then((r) => r.arrayBuffer())),
|
|
);
|
|
|
|
const logoUri = toDataUri(join(websiteDir, 'public', 'images', 'logo.png'), 'image/png');
|
|
|
|
const element: SatoriElement = {
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
display: 'flex',
|
|
width: 1200,
|
|
height: 630,
|
|
backgroundColor: colors.deep,
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
},
|
|
children: [
|
|
// ── Background ambient glows (subtle, behind everything) ──
|
|
orb(-120, undefined, 60, undefined, 500, 'rgb(36,92,249)', 0.07, 0.65),
|
|
orb(undefined, -80, undefined, 200, 450, 'rgb(138,41,230)', 0.05, 0.6),
|
|
|
|
// ── Top gradient accent bar ──
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 4,
|
|
background: `linear-gradient(90deg, ${colors.brand}, ${colors.accent})`,
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── Main content ──
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
display: 'flex',
|
|
flexDirection: 'row' as const,
|
|
width: '100%',
|
|
height: '100%',
|
|
padding: '0 80px',
|
|
alignItems: 'center',
|
|
},
|
|
children: [
|
|
// Left column — text
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
children: [
|
|
// Logo
|
|
{
|
|
type: 'img',
|
|
props: {
|
|
src: logoUri,
|
|
width: 56,
|
|
height: 56,
|
|
style: { borderRadius: 14 },
|
|
},
|
|
},
|
|
// Product name
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
fontFamily: 'Instrument Serif',
|
|
fontSize: 76,
|
|
color: colors.warm50,
|
|
marginTop: 24,
|
|
lineHeight: 1,
|
|
letterSpacing: -1,
|
|
},
|
|
children: 'Aryx',
|
|
},
|
|
},
|
|
// Tagline
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
display: 'flex',
|
|
flexWrap: 'wrap' as const,
|
|
fontFamily: 'Outfit',
|
|
fontSize: 26,
|
|
fontWeight: 300,
|
|
color: colors.warm300,
|
|
marginTop: 18,
|
|
lineHeight: 1.45,
|
|
},
|
|
children: [
|
|
'Your control room for ',
|
|
{
|
|
type: 'span',
|
|
props: {
|
|
style: { color: colors.brand },
|
|
children: 'Copilot-powered',
|
|
},
|
|
},
|
|
' work',
|
|
],
|
|
},
|
|
},
|
|
// Badge pill
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
marginTop: 30,
|
|
fontFamily: 'Outfit',
|
|
fontSize: 14,
|
|
color: colors.brand,
|
|
border: '1px solid rgba(36,92,249,0.2)',
|
|
background: 'rgba(36,92,249,0.06)',
|
|
borderRadius: 100,
|
|
padding: '7px 16px',
|
|
alignSelf: 'flex-start',
|
|
},
|
|
children: [
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
width: 6,
|
|
height: 6,
|
|
borderRadius: '50%',
|
|
backgroundColor: colors.brand,
|
|
},
|
|
},
|
|
},
|
|
'Desktop AI Workspace',
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// ── Decorative orbs (right side) ──
|
|
// Large blue orb
|
|
orb(40, undefined, undefined, 40, 380, 'rgb(36,92,249)', 0.22, 0.55),
|
|
// Large purple orb (overlapping)
|
|
orb(undefined, 20, undefined, -40, 340, 'rgb(138,41,230)', 0.18, 0.5),
|
|
// Bright blue accent orb
|
|
orb(180, undefined, undefined, 220, 140, 'rgb(36,140,253)', 0.3, 0.5),
|
|
|
|
// ── Thin ring ──
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
position: 'absolute',
|
|
top: 100,
|
|
right: 80,
|
|
width: 240,
|
|
height: 240,
|
|
borderRadius: '50%',
|
|
border: '1px solid rgba(36,92,249,0.12)',
|
|
},
|
|
},
|
|
},
|
|
// Smaller ring
|
|
{
|
|
type: 'div',
|
|
props: {
|
|
style: {
|
|
position: 'absolute',
|
|
bottom: 80,
|
|
right: 160,
|
|
width: 160,
|
|
height: 160,
|
|
borderRadius: '50%',
|
|
border: '1px solid rgba(138,41,230,0.10)',
|
|
},
|
|
},
|
|
},
|
|
|
|
// ── Accent dots ──
|
|
dot(140, 820, 8, 'rgba(36,92,249,0.4)'),
|
|
dot(340, 950, 6, 'rgba(138,41,230,0.35)'),
|
|
dot(480, 780, 5, 'rgba(36,140,253,0.3)'),
|
|
dot(200, 1050, 4, 'rgba(36,92,249,0.25)'),
|
|
dot(100, 1100, 7, 'rgba(138,41,230,0.2)'),
|
|
],
|
|
},
|
|
};
|
|
|
|
const svg = await satori(element, {
|
|
width: 1200,
|
|
height: 630,
|
|
fonts: [
|
|
{ name: 'Instrument Serif', data: instrumentSerif, weight: 400, style: 'normal' as const },
|
|
{ name: 'Outfit', data: outfit400, weight: 400, style: 'normal' as const },
|
|
{ name: 'Outfit', data: outfit300, weight: 300, style: 'normal' as const },
|
|
],
|
|
});
|
|
|
|
const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } });
|
|
const png = resvg.render().asPng();
|
|
|
|
const outputPath = join(websiteDir, 'public', 'images', 'og.png');
|
|
writeFileSync(outputPath, png);
|
|
|
|
const sizeKb = (png.length / 1024).toFixed(1);
|
|
console.log(`✓ OG image generated → public/images/og.png (${sizeKb} KB)`);
|
|
}
|
|
|
|
generate().catch((err) => {
|
|
console.error('Failed to generate OG image:', err);
|
|
process.exit(1);
|
|
});
|