Skip to content

Commit 836ce12

Browse files
committed
feat: better sidebar
1 parent 1c3a6e8 commit 836ce12

12 files changed

Lines changed: 834 additions & 300 deletions

File tree

apps/web/app/api/og/logo.png

33.6 KB
Loading

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

Lines changed: 232 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ export const runtime = "edge";
44

55
const OG_WIDTH = 1200;
66
const OG_HEIGHT = 630;
7-
const DEFAULT_TITLE = "Claws.supply";
8-
const DEFAULT_DESCRIPTION = "OpenClaw AI agent template marketplace.";
7+
const TITLE_MAX_LENGTH = 110;
8+
const DESCRIPTION_MAX_LENGTH = 180;
9+
const LOGO_PATH = new URL("./logo.png", import.meta.url);
910

1011
function truncateText(value: string, maxLength: number) {
1112
const normalized = value.trim();
@@ -17,13 +18,96 @@ function truncateText(value: string, maxLength: number) {
1718
return `${normalized.slice(0, Math.max(maxLength - 1, 1)).trimEnd()}…`;
1819
}
1920

20-
export function GET(request: Request) {
21+
function normalizeText(
22+
value: string | null,
23+
maxLength: number,
24+
): string | undefined {
25+
if (!value) {
26+
return undefined;
27+
}
28+
29+
const normalized = value.trim();
30+
if (normalized.length === 0) {
31+
return undefined;
32+
}
33+
34+
return truncateText(normalized, maxLength);
35+
}
36+
37+
function arrayBufferToBase64(buffer: ArrayBuffer): string {
38+
const bytes = new Uint8Array(buffer);
39+
const chunkSize = 0x8000;
40+
let binary = "";
41+
42+
for (let index = 0; index < bytes.length; index += chunkSize) {
43+
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
44+
}
45+
46+
return btoa(binary);
47+
}
48+
49+
async function loadLogoDataUri(): Promise<string | null> {
50+
try {
51+
const response = await fetch(LOGO_PATH);
52+
if (!response.ok) {
53+
return null;
54+
}
55+
56+
const contentType = response.headers.get("content-type") ?? "image/png";
57+
const arrayBuffer = await response.arrayBuffer();
58+
const base64 = arrayBufferToBase64(arrayBuffer);
59+
return `data:${contentType};base64,${base64}`;
60+
} catch {
61+
return null;
62+
}
63+
}
64+
65+
const logoDataUriPromise = loadLogoDataUri();
66+
67+
function getTitleFontSize(titleLength: number, hasDescription: boolean) {
68+
if (!hasDescription) {
69+
if (titleLength <= 32) return 88;
70+
if (titleLength <= 56) return 76;
71+
if (titleLength <= 84) return 66;
72+
return 58;
73+
}
74+
75+
if (titleLength <= 28) return 74;
76+
if (titleLength <= 54) return 64;
77+
if (titleLength <= 80) return 56;
78+
return 50;
79+
}
80+
81+
function getDescriptionFontSize(
82+
descriptionLength: number,
83+
hasTitle: boolean,
84+
) {
85+
if (!hasTitle) {
86+
if (descriptionLength <= 80) return 46;
87+
if (descriptionLength <= 130) return 40;
88+
return 34;
89+
}
90+
91+
if (descriptionLength <= 84) return 34;
92+
if (descriptionLength <= 136) return 30;
93+
return 27;
94+
}
95+
96+
export async function GET(request: Request) {
2197
const { searchParams } = new URL(request.url);
22-
const title = truncateText(searchParams.get("title") ?? DEFAULT_TITLE, 110);
23-
const description = truncateText(
24-
searchParams.get("description") ?? DEFAULT_DESCRIPTION,
25-
180,
98+
const title = normalizeText(searchParams.get("title"), TITLE_MAX_LENGTH);
99+
const description = normalizeText(
100+
searchParams.get("description"),
101+
DESCRIPTION_MAX_LENGTH,
26102
);
103+
const hasText = Boolean(title || description);
104+
const logoDataUri = await logoDataUriPromise;
105+
const titleFontSize = title
106+
? getTitleFontSize(title.length, Boolean(description))
107+
: 0;
108+
const descriptionFontSize = description
109+
? getDescriptionFontSize(description.length, Boolean(title))
110+
: 0;
27111

28112
return new ImageResponse(
29113
(
@@ -36,6 +120,7 @@ export function GET(request: Request) {
36120
color: "#f5f5f5",
37121
position: "relative",
38122
fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
123+
overflow: "hidden",
39124
}}
40125
>
41126
<div
@@ -48,49 +133,154 @@ export function GET(request: Request) {
48133
opacity: 0.25,
49134
}}
50135
/>
136+
<div
137+
style={{
138+
position: "absolute",
139+
inset: 0,
140+
background:
141+
"radial-gradient(90% 70% at 16% 12%, rgba(249,115,22,0.16) 0%, rgba(249,115,22,0) 58%)",
142+
}}
143+
/>
144+
<div
145+
style={{
146+
position: "absolute",
147+
inset: "38px",
148+
border: "1px solid rgba(245,245,245,0.12)",
149+
}}
150+
/>
51151
<div
52152
style={{
53153
display: "flex",
54154
flexDirection: "column",
55-
justifyContent: "space-between",
56155
width: "100%",
57-
padding: "56px 64px",
156+
height: "100%",
58157
position: "relative",
158+
padding: "56px 64px",
59159
}}
60160
>
61-
<div
62-
style={{
63-
fontSize: 26,
64-
letterSpacing: "0.18em",
65-
textTransform: "uppercase",
66-
color: "#f97316",
67-
fontWeight: 700,
68-
}}
69-
>
70-
claws.supply
71-
</div>
72-
<div style={{ display: "flex", flexDirection: "column", gap: 18, maxWidth: 980 }}>
73-
<div
74-
style={{
75-
fontSize: title.length > 60 ? 58 : 70,
76-
lineHeight: 1.08,
77-
fontWeight: 700,
78-
letterSpacing: "-0.02em",
79-
}}
80-
>
81-
{title}
82-
</div>
83-
<div
84-
style={{
85-
fontSize: 34,
86-
lineHeight: 1.25,
87-
color: "#d4d4d8",
88-
letterSpacing: "-0.01em",
89-
}}
90-
>
91-
{description}
92-
</div>
93-
</div>
161+
{hasText
162+
? (
163+
<>
164+
{logoDataUri
165+
? (
166+
<img
167+
src={logoDataUri}
168+
alt="Claws.supply logo"
169+
style={{
170+
position: "absolute",
171+
top: 50,
172+
left: 56,
173+
width: 116,
174+
height: 116,
175+
objectFit: "contain",
176+
border: "1px solid rgba(249,115,22,0.34)",
177+
boxShadow: "0 0 0 6px rgba(0,0,0,0.44)",
178+
background: "rgba(0,0,0,0.26)",
179+
}}
180+
/>
181+
)
182+
: (
183+
<div
184+
style={{
185+
position: "absolute",
186+
top: 70,
187+
left: 64,
188+
fontSize: 22,
189+
letterSpacing: "0.12em",
190+
textTransform: "uppercase",
191+
color: "#f97316",
192+
fontWeight: 700,
193+
}}
194+
>
195+
claws.supply
196+
</div>
197+
)}
198+
<div
199+
style={{
200+
display: "flex",
201+
flexDirection: "column",
202+
justifyContent: "flex-end",
203+
gap: 16,
204+
marginTop: "auto",
205+
maxWidth: 1040,
206+
paddingBottom: 6,
207+
}}
208+
>
209+
{title
210+
? (
211+
<div
212+
style={{
213+
fontSize: titleFontSize,
214+
lineHeight: 1.08,
215+
fontWeight: 700,
216+
letterSpacing: "-0.03em",
217+
color: "#fafafa",
218+
wordBreak: "break-word",
219+
}}
220+
>
221+
{title}
222+
</div>
223+
)
224+
: null}
225+
{description
226+
? (
227+
<div
228+
style={{
229+
fontSize: descriptionFontSize,
230+
lineHeight: 1.24,
231+
color: "#d6d3d1",
232+
letterSpacing: "-0.01em",
233+
wordBreak: "break-word",
234+
}}
235+
>
236+
{description}
237+
</div>
238+
)
239+
: null}
240+
</div>
241+
</>
242+
)
243+
: (
244+
<div
245+
style={{
246+
display: "flex",
247+
alignItems: "center",
248+
justifyContent: "center",
249+
width: "100%",
250+
height: "100%",
251+
}}
252+
>
253+
{logoDataUri
254+
? (
255+
<img
256+
src={logoDataUri}
257+
alt="Claws.supply logo"
258+
style={{
259+
width: 252,
260+
height: 252,
261+
objectFit: "contain",
262+
border: "1px solid rgba(249,115,22,0.4)",
263+
boxShadow:
264+
"0 0 0 10px rgba(0,0,0,0.42), 0 0 56px rgba(249,115,22,0.2)",
265+
background: "rgba(0,0,0,0.28)",
266+
}}
267+
/>
268+
)
269+
: (
270+
<div
271+
style={{
272+
fontSize: 72,
273+
lineHeight: 1.08,
274+
fontWeight: 700,
275+
letterSpacing: "-0.03em",
276+
color: "#fafafa",
277+
}}
278+
>
279+
claws.supply
280+
</div>
281+
)}
282+
</div>
283+
)}
94284
</div>
95285
</div>
96286
),

0 commit comments

Comments
 (0)