Add OG image for social media previews (#1861)
* 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>
This commit is contained in:
parent
4376e6e19a
commit
39c03c9b07
9 changed files with 139 additions and 5 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 3.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 3.4 MiB |
BIN
web/app/[locale]/assets/og-screenshot.png
Normal file
BIN
web/app/[locale]/assets/og-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -21,7 +21,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
|||
url,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
card: "summary_large_image",
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
|||
url,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
card: "summary_large_image",
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
|||
url,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
card: "summary_large_image",
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
|||
url,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
card: "summary_large_image",
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export async function generateMetadata({
|
|||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary",
|
||||
card: "summary_large_image",
|
||||
title: t("title"),
|
||||
description: t("ogDescription"),
|
||||
},
|
||||
|
|
|
|||
134
web/app/[locale]/opengraph-image.tsx
Normal file
134
web/app/[locale]/opengraph-image.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
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" },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue