Skip to content

Commit 2ba61c8

Browse files
authored
feature: Add OG images for tw-shimmer and safe-content-frame (assistant-ui#2978)
1 parent 03d96e2 commit 2ba61c8

6 files changed

Lines changed: 318 additions & 5 deletions

File tree

apps/docs/app/api/og/route.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@ export async function GET(request: NextRequest) {
7575
gap: 32,
7676
}}
7777
>
78-
<svg width="120" height="120" viewBox="0 0 32 32" fill="none">
78+
<svg
79+
width="120"
80+
height="120"
81+
viewBox="0 0 32 32"
82+
fill="none"
83+
style={{ marginBottom: -10 }}
84+
>
7985
<rect width="32" height="32" rx="6" fill="#000000" />
8086
<g
8187
transform="translate(4,4)"
@@ -91,7 +97,7 @@ export async function GET(request: NextRequest) {
9197
</svg>
9298
<span
9399
style={{
94-
fontSize: 100,
100+
fontSize: 120,
95101
fontWeight: 600,
96102
color: "#ffffff",
97103
fontFamily: fontSans,
@@ -101,17 +107,23 @@ export async function GET(request: NextRequest) {
101107
assistant-ui
102108
</span>
103109
</div>
104-
<span
110+
<div
105111
style={{
112+
display: "flex",
113+
flexDirection: "column",
114+
alignItems: "center",
106115
fontSize: 48,
107116
fontWeight: 400,
117+
gap: 10,
108118
color: "#a3a3a3",
119+
textAlign: "center",
109120
fontFamily: fontSans,
110121
letterSpacing: "-0.01em",
111122
}}
112123
>
113-
The UX of ChatGPT in your own app
114-
</span>
124+
<span>An open-source React toolkit for</span>
125+
<span>production AI chat experiences</span>
126+
</div>
115127
</div>
116128
);
117129

apps/docs/app/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { Provider } from "./provider";
77
import { cn } from "@/lib/utils";
88

99
const getMetadataBase = () => {
10+
const appUrl = process.env["NEXT_PUBLIC_APP_URL"];
11+
if (appUrl) {
12+
return new URL(appUrl);
13+
}
14+
1015
if (process.env.NODE_ENV === "production") {
1116
return new URL("https://www.assistant-ui.com");
1217
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ImageResponse } from "next/og";
2+
import { readFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
import { OG_SIZE, OgTemplate } from "@/lib/og-template";
5+
6+
export const alt = "Safe Content Frame";
7+
export const size = OG_SIZE;
8+
export const contentType = "image/png";
9+
10+
export default async function Image() {
11+
const [geistSemiBold, geistRegular, geistMedium, geistMono] =
12+
await Promise.all([
13+
readFile(join(process.cwd(), "assets/Geist-SemiBold.ttf")),
14+
readFile(join(process.cwd(), "assets/Geist-Regular.ttf")),
15+
readFile(join(process.cwd(), "assets/Geist-Medium.ttf")),
16+
readFile(join(process.cwd(), "assets/GeistMono-Regular.ttf")),
17+
]);
18+
19+
return new ImageResponse(
20+
<OgTemplate subtleBranding>
21+
<span
22+
style={{
23+
fontSize: 90,
24+
fontWeight: 600,
25+
color: "#ffffff",
26+
textAlign: "center",
27+
fontFamily: "Geist",
28+
letterSpacing: "-0.02em",
29+
}}
30+
>
31+
Safe Content Frame
32+
</span>
33+
<span
34+
style={{
35+
fontSize: 38,
36+
fontWeight: 400,
37+
color: "#a3a3a3",
38+
fontFamily: "Geist",
39+
letterSpacing: "-0.01em",
40+
textAlign: "left",
41+
}}
42+
>
43+
Render untrusted HTML securely in sandboxed iframes
44+
</span>
45+
</OgTemplate>,
46+
{
47+
...size,
48+
fonts: [
49+
{
50+
name: "Geist",
51+
data: geistSemiBold,
52+
style: "normal",
53+
weight: 600,
54+
},
55+
{
56+
name: "Geist",
57+
data: geistRegular,
58+
style: "normal",
59+
weight: 400,
60+
},
61+
{
62+
name: "Geist",
63+
data: geistMedium,
64+
style: "normal",
65+
weight: 500,
66+
},
67+
{
68+
name: "GeistMono",
69+
data: geistMono,
70+
style: "normal",
71+
weight: 400,
72+
},
73+
],
74+
},
75+
);
76+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { ImageResponse } from "next/og";
2+
import { readFile } from "node:fs/promises";
3+
import { join } from "node:path";
4+
import { OG_SIZE, OgTemplate } from "@/lib/og-template";
5+
6+
export const alt = "tw-shimmer";
7+
export const size = OG_SIZE;
8+
export const contentType = "image/png";
9+
10+
export default async function Image() {
11+
const [geistRegular, geistMedium, geistMono, shimmerTextPng] =
12+
await Promise.all([
13+
readFile(join(process.cwd(), "assets/Geist-Regular.ttf")),
14+
readFile(join(process.cwd(), "assets/Geist-Medium.ttf")),
15+
readFile(join(process.cwd(), "assets/GeistMono-Regular.ttf")),
16+
readFile(join(process.cwd(), "assets/tw-shimmer-text.png"), "base64"),
17+
]);
18+
19+
const shimmerTextSrc = `data:image/png;base64,${shimmerTextPng}`;
20+
21+
return new ImageResponse(
22+
<OgTemplate subtleBranding>
23+
<img
24+
src={shimmerTextSrc}
25+
alt="tw-shimmer"
26+
height={100}
27+
style={{ objectFit: "contain", marginBottom: 20 }}
28+
/>
29+
<span
30+
style={{
31+
fontSize: 42,
32+
fontWeight: 400,
33+
color: "#a3a3a3",
34+
fontFamily: "Geist",
35+
letterSpacing: "-0.01em",
36+
textAlign: "center",
37+
}}
38+
>
39+
Zero-dependency CSS-only shimmer
40+
</span>
41+
</OgTemplate>,
42+
{
43+
...size,
44+
fonts: [
45+
{
46+
name: "Geist",
47+
data: geistRegular,
48+
style: "normal",
49+
weight: 400,
50+
},
51+
{
52+
name: "Geist",
53+
data: geistMedium,
54+
style: "normal",
55+
weight: 500,
56+
},
57+
{
58+
name: "GeistMono",
59+
data: geistMono,
60+
style: "normal",
61+
weight: 400,
62+
},
63+
],
64+
},
65+
);
66+
}
85.8 KB
Loading

apps/docs/lib/og-template.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { ReactNode } from "react";
2+
3+
export const OG_SIZE = {
4+
width: 1200,
5+
height: 630,
6+
};
7+
8+
/** The assistant-ui chat bubble logo as JSX for OG images */
9+
export function OgLogo({
10+
size = 64,
11+
color = "white",
12+
}: {
13+
size?: number;
14+
color?: string;
15+
}) {
16+
return (
17+
<svg width={size} height={size} viewBox="0 0 32 32" fill="none">
18+
<rect width="32" height="32" rx="6" fill="#000000" />
19+
<g
20+
transform="translate(4,4)"
21+
fill="black"
22+
stroke={color}
23+
strokeWidth="2.5"
24+
strokeLinecap="round"
25+
strokeLinejoin="round"
26+
>
27+
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z" />
28+
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" />
29+
</g>
30+
</svg>
31+
);
32+
}
33+
34+
/** Shared header with logo on left and URL on right */
35+
export function OgHeader({
36+
fontSans = "Geist",
37+
fontMono = "GeistMono",
38+
subtle = false,
39+
}: {
40+
fontSans?: string;
41+
fontMono?: string;
42+
/** Use smaller, more muted branding for sub-projects */
43+
subtle?: boolean;
44+
}) {
45+
const logoSize = subtle ? 40 : 64;
46+
const titleSize = subtle ? 28 : 44;
47+
const urlSize = subtle ? 22 : 32;
48+
const titleColor = subtle ? "#888888" : "#e5e5e5";
49+
const urlColor = subtle ? "#666666" : "#a3a3a3";
50+
const gap = subtle ? 12 : 20;
51+
52+
return (
53+
<div
54+
style={{
55+
display: "flex",
56+
alignItems: "center",
57+
justifyContent: "space-between",
58+
width: "100%",
59+
}}
60+
>
61+
<div
62+
style={{
63+
display: "flex",
64+
alignItems: "center",
65+
gap,
66+
}}
67+
>
68+
<OgLogo size={logoSize} color={titleColor} />
69+
<span
70+
style={{
71+
fontSize: titleSize,
72+
fontWeight: 500,
73+
color: titleColor,
74+
fontFamily: fontSans,
75+
letterSpacing: "-0.01em",
76+
}}
77+
>
78+
assistant-ui
79+
</span>
80+
</div>
81+
<span
82+
style={{
83+
fontSize: urlSize,
84+
fontWeight: 400,
85+
color: urlColor,
86+
fontFamily: fontMono,
87+
}}
88+
>
89+
assistant-ui.com
90+
</span>
91+
</div>
92+
);
93+
}
94+
95+
/**
96+
* Reusable OG image template with header, centered content, and optional background decoration.
97+
*/
98+
export function OgTemplate({
99+
children,
100+
backgroundDecoration,
101+
subtleBranding = false,
102+
}: {
103+
children: ReactNode;
104+
backgroundDecoration?: ReactNode;
105+
/** Use smaller, more muted assistant-ui branding for sub-projects */
106+
subtleBranding?: boolean;
107+
}) {
108+
return (
109+
<div
110+
style={{
111+
height: "100%",
112+
width: "100%",
113+
display: "flex",
114+
flexDirection: "column",
115+
backgroundColor: "#0a0a0a",
116+
padding: "60px 60px 90px 60px",
117+
position: "relative",
118+
}}
119+
>
120+
{backgroundDecoration && (
121+
<div
122+
style={{
123+
position: "absolute",
124+
top: 0,
125+
left: 0,
126+
right: 0,
127+
bottom: 0,
128+
display: "flex",
129+
alignItems: "center",
130+
justifyContent: "center",
131+
}}
132+
>
133+
{backgroundDecoration}
134+
</div>
135+
)}
136+
137+
<OgHeader subtle={subtleBranding} />
138+
139+
<div
140+
style={{
141+
display: "flex",
142+
flexDirection: "column",
143+
flex: 1,
144+
justifyContent: "center",
145+
alignItems: "center",
146+
gap: 24,
147+
zIndex: 1,
148+
}}
149+
>
150+
{children}
151+
</div>
152+
</div>
153+
);
154+
}

0 commit comments

Comments
 (0)