* Add dynamic OG image and use large Twitter cards Generate a 1200x630 OG image with the cmux logo, tagline, and description using next/og ImageResponse. Switch Twitter card type from "summary" to "summary_large_image" across all pages so shared links show a full-width preview instead of the tiny favicon thumbnail. * Use Geist font and app screenshot in OG image, update landing/README images Replace the centered text-only OG image with a split layout: branding on the left (logo, name, tagline) and a full app screenshot on the right. Load Geist Regular/SemiBold from Google Fonts for consistent typography. Replace the homepage landing image and README screenshot with a new screenshot showing cmux with multiple workspaces, tabs, browser panel, and code diffs. * Fine-tune OG image layout and update homepage/README screenshots Apply tuned values from OG editor: 112px logo, 48px title with -8 translateY, 34px subtitle at #cfcfcf, 320px fade height. Use Geist font loaded from Google Fonts. Render at 2x (2400x1260) for sharper previews on social platforms. Remove GitHub URL from footer. Add pre-resized og-screenshot.png (2208px wide) for the OG image to avoid Satori downscale blur. Update homepage landing image and README screenshot with new app screenshot. --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import { ImageResponse } from "next/og";
|
|
import { readFile } from "fs/promises";
|
|
import { join } from "path";
|
|
|
|
export const runtime = "nodejs";
|
|
export const size = { width: 1200, height: 630 };
|
|
export const contentType = "image/png";
|
|
export const alt = "cmux — The terminal built for multitasking";
|
|
|
|
const S = 2; // render at 2x for sharper images on social platforms
|
|
|
|
export default async function Image() {
|
|
const [logoData, screenshotData, geistRegular, geistSemiBold] =
|
|
await Promise.all([
|
|
readFile(join(process.cwd(), "public", "logo.png")),
|
|
readFile(
|
|
join(process.cwd(), "app", "[locale]", "assets", "og-screenshot.png")
|
|
),
|
|
fetch(
|
|
"https://fonts.gstatic.com/s/geist/v4/gyBhhwUxId8gMGYQMKR3pzfaWI_RnOM4nQ.ttf"
|
|
).then((res) => res.arrayBuffer()),
|
|
fetch(
|
|
"https://fonts.gstatic.com/s/geist/v4/gyBhhwUxId8gMGYQMKR3pzfaWI_RQuQ4nQ.ttf"
|
|
).then((res) => res.arrayBuffer()),
|
|
]);
|
|
|
|
const logoSrc = `data:image/png;base64,${logoData.toString("base64")}`;
|
|
const screenshotSrc = `data:image/png;base64,${screenshotData.toString("base64")}`;
|
|
|
|
return new ImageResponse(
|
|
(
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
backgroundColor: "#0a0a0a",
|
|
fontFamily: "Geist",
|
|
paddingBottom: 28 * S,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
flex: 1,
|
|
}}
|
|
>
|
|
{/* Screenshot */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flex: 1,
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
}}
|
|
>
|
|
<img src={screenshotSrc} width={size.width * S} />
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 320 * S,
|
|
background:
|
|
"linear-gradient(to bottom, rgba(10,10,10,0), rgba(10,10,10,1))",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Branding bar */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
marginTop: -60 * S,
|
|
paddingLeft: 25 * S,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 20 * S,
|
|
}}
|
|
>
|
|
<img
|
|
src={logoSrc}
|
|
width={112 * S}
|
|
height={112 * S}
|
|
style={{ borderRadius: 20 * S }}
|
|
/>
|
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
<div
|
|
style={{
|
|
fontSize: 48 * S,
|
|
fontWeight: 600,
|
|
color: "#ededed",
|
|
letterSpacing: "-0.02em",
|
|
lineHeight: 1,
|
|
marginTop: -8 * S,
|
|
}}
|
|
>
|
|
cmux
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: 34 * S,
|
|
fontWeight: 400,
|
|
color: "#cfcfcf",
|
|
marginTop: 5 * S,
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
The terminal built for multitasking
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
{
|
|
width: size.width * S,
|
|
height: size.height * S,
|
|
fonts: [
|
|
{ name: "Geist", data: geistRegular, weight: 400, style: "normal" },
|
|
{ name: "Geist", data: geistSemiBold, weight: 600, style: "normal" },
|
|
],
|
|
}
|
|
);
|
|
}
|