Skip to content

Commit 0051355

Browse files
committed
Sets up a much more nuanced package opengraph image
1 parent 8825b0a commit 0051355

File tree

2 files changed

+342
-53
lines changed

2 files changed

+342
-53
lines changed

app/components/OgImage/Package.vue

Lines changed: 281 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,87 @@
11
<script setup lang="ts">
2-
withDefaults(
3-
defineProps<{
4-
name: string
5-
version: string
6-
downloads?: string
7-
license?: string
8-
primaryColor?: string
9-
}>(),
10-
{
11-
downloads: '',
12-
license: '',
13-
primaryColor: '#60a5fa',
14-
},
15-
)
2+
interface Props {
3+
name: string
4+
version: string
5+
downloads?: string
6+
license?: string
7+
primaryColor?: string
8+
description?: string
9+
repoOwner?: string
10+
repoName?: string
11+
stars?: number
12+
forks?: number
13+
directDepsCount?: number
14+
updatedAt?: string
15+
hasTypes?: boolean
16+
hasESM?: boolean
17+
hasCJS?: boolean
18+
}
19+
20+
const props = withDefaults(defineProps<Props>(), {
21+
downloads: '',
22+
license: '',
23+
primaryColor: '#60a5fa',
24+
description: '',
25+
repoOwner: '',
26+
repoName: '',
27+
stars: 0,
28+
forks: 0,
29+
directDepsCount: 0,
30+
updatedAt: '',
31+
hasTypes: false,
32+
hasESM: false,
33+
hasCJS: false,
34+
})
35+
36+
// Dynamic font sizing based on name length
37+
// OG images are 1200px wide, with 64px padding on each side = 1072px content width
38+
// Icon is 80px + 24px gap = 104px, leaving ~968px for name + version
39+
const titleFontSize = computed(() => {
40+
const len = props.name.length
41+
if (len <= 12) return 96 // text-8xl
42+
if (len <= 18) return 80
43+
if (len <= 24) return 64
44+
if (len <= 32) return 52
45+
if (len <= 40) return 44
46+
return 36 // very long names
47+
})
48+
49+
const versionFontSize = computed(() => {
50+
// Version scales proportionally but stays readable
51+
const base = titleFontSize.value
52+
return Math.max(42, Math.round(base * 0.5))
53+
})
54+
55+
const truncatedVersion = computed(() => {
56+
const v = props.version
57+
if (v.length <= 12) return v
58+
return v.slice(0, 11) + ''
59+
})
1660
</script>
1761

1862
<template>
1963
<div
20-
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
64+
class="h-full w-full flex flex-col justify-between px-16 py-14 bg-[#050505] text-[#fafafa] relative overflow-hidden"
65+
style="font-family: 'Geist', sans-serif"
2166
>
22-
<div class="relative z-10 flex flex-col gap-6">
23-
<div class="flex items-start gap-4">
67+
<!-- Top section -->
68+
<div class="relative z-10 flex flex-col gap-4">
69+
<!-- Short name layout: icon + ./name + version in one row -->
70+
<div v-if="props.name.length < 24" class="flex flex-row items-end gap-6 align-baseline mb-6">
2471
<div
25-
class="flex items-start justify-center w-16 h-16 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]"
26-
:style="{ backgroundColor: primaryColor }"
72+
class="w-20 h-20 rounded-xl shadow-lg flex items-center justify-center"
73+
:style="{ backgroundColor: props.primaryColor }"
2774
>
2875
<svg
29-
width="36"
30-
height="36"
76+
width="42"
77+
height="42"
3178
viewBox="0 0 24 24"
3279
fill="none"
3380
stroke="white"
3481
stroke-width="2.5"
3582
stroke-linecap="round"
3683
stroke-linejoin="round"
84+
:style="{ marginTop: '36px' }"
3785
>
3886
<path d="m7.5 4.27 9 5.15" />
3987
<path
@@ -45,55 +93,236 @@ withDefaults(
4593
</div>
4694

4795
<h1
48-
class="text-8xl font-bold tracking-tighter"
49-
style="font-family: 'Geist Sans', sans-serif"
96+
class="font-bold tracking-tight leading-none mt-0"
97+
:style="{ fontSize: `${titleFontSize}px`, marginBottom: '-8px' }"
5098
>
51-
<span :style="{ color: primaryColor }" class="opacity-80">./</span>{{ name }}
99+
<span class="opacity-80" :style="{ color: props.primaryColor }">./</span>{{ props.name }}
52100
</h1>
53-
</div>
54101

55-
<div
56-
class="flex items-center gap-3 text-4xl font-light text-[#a3a3a3]"
57-
style="font-family: 'Geist Sans', sans-serif"
58-
>
59-
<span
60-
class="px-3 py-1 rounded-lg border"
61-
:style="{
62-
color: primaryColor,
63-
backgroundColor: primaryColor + '10',
64-
borderColor: primaryColor + '30',
65-
boxShadow: `0 0 20px ${primaryColor}25`,
66-
}"
67-
>
68-
{{ version }}
102+
<span class="pb-1" :style="{ fontSize: `${versionFontSize}px`, color: props.primaryColor }">
103+
v{{ truncatedVersion }}
69104
</span>
70-
<span v-if="downloads">
71-
<span>• {{ downloads }} </span>
72-
<span class="flex items-center gap-0">
105+
</div>
106+
107+
<!-- Long name layout: icon + name on first row, version below -->
108+
<template v-else>
109+
<div class="flex flex-row items-center gap-6">
110+
<div
111+
class="w-20 h-20 rounded-xl shadow-lg flex items-center justify-center"
112+
:style="{ backgroundColor: props.primaryColor }"
113+
>
73114
<svg
74-
width="30"
75-
height="30"
115+
width="42"
116+
height="42"
76117
viewBox="0 0 24 24"
77118
fill="none"
78-
stroke="currentColor"
79-
stroke-width="2"
119+
stroke="white"
120+
stroke-width="2.5"
80121
stroke-linecap="round"
81122
stroke-linejoin="round"
82-
class="text-white/70"
123+
:style="{ marginTop: '36px' }"
83124
>
84-
<circle cx="12" cy="12" r="10" class="opacity-40" />
85-
<path d="M12 8v8m-3-3l3 3 3-3" />
125+
<path d="m7.5 4.27 9 5.15" />
126+
<path
127+
d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"
128+
/>
129+
<path d="m3.3 7 8.7 5 8.7-5" />
130+
<path d="M12 22V12" />
86131
</svg>
87-
<span>/wk</span>
132+
</div>
133+
134+
<h1
135+
class="font-bold tracking-tight leading-none"
136+
:style="{ fontSize: `${titleFontSize}px` }"
137+
>
138+
{{ props.name }}
139+
</h1>
140+
</div>
141+
142+
<!-- ./ and version on second row, under the icon -->
143+
<div class="flex flex-row items-baseline gap-6">
144+
<div
145+
class="w-20 text-right opacity-80"
146+
:style="{
147+
fontSize: `${versionFontSize}px`,
148+
color: props.primaryColor,
149+
paddingLeft: '22px',
150+
}"
151+
>
152+
./
153+
</div>
154+
<span :style="{ fontSize: `${versionFontSize}px`, color: props.primaryColor }">
155+
v{{ truncatedVersion }}
88156
</span>
157+
</div>
158+
</template>
159+
160+
<!-- Description (can extend under stats, will be faded out) -->
161+
<div v-if="props.description" class="text-4xl text-[#a3a3a3] max-w-[1000px]">
162+
{{ props.description }}
163+
</div>
164+
</div>
165+
166+
<!-- Module format badges - top right (unchanged size) -->
167+
<div class="absolute top-8 right-10 flex flex-row items-center gap-2 text-xl">
168+
<span
169+
v-if="props.hasTypes"
170+
class="px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
171+
>
172+
Types
173+
</span>
174+
<span
175+
v-if="props.hasESM"
176+
class="px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
177+
>
178+
ESM
179+
</span>
180+
<span
181+
v-if="props.hasCJS"
182+
class="px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
183+
>
184+
CJS
185+
</span>
186+
</div>
187+
188+
<!-- Gradient fade above stats -->
189+
<div
190+
class="absolute left-0 right-0 h-24 z-10"
191+
:style="{ bottom: '140px', background: 'linear-gradient(to bottom, transparent, #050505)' }"
192+
/>
193+
194+
<!-- Bottom stats (fixed at bottom) -->
195+
<div class="absolute bottom-14 left-16 right-16 z-20 flex flex-col gap-4 bg-[#050505]">
196+
<!-- GitHub stats row -->
197+
<div
198+
v-if="props.repoOwner && props.repoName"
199+
class="flex flex-row items-center gap-12 text-3xl text-[#737373]"
200+
>
201+
<span class="flex flex-row items-center gap-3">
202+
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
203+
<path
204+
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
205+
/>
206+
</svg>
207+
{{ props.repoOwner }}/{{ props.repoName }}
208+
</span>
209+
<span v-if="props.stars" class="flex flex-row items-center gap-3">
210+
<svg
211+
width="28"
212+
height="28"
213+
viewBox="0 0 24 24"
214+
fill="none"
215+
stroke="currentColor"
216+
stroke-width="2"
217+
stroke-linecap="round"
218+
stroke-linejoin="round"
219+
>
220+
<polygon
221+
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
222+
/>
223+
</svg>
224+
{{ props.stars.toLocaleString() }}
225+
</span>
226+
<span v-if="props.forks" class="flex flex-row items-center gap-3">
227+
<svg
228+
width="28"
229+
height="28"
230+
viewBox="0 0 24 24"
231+
fill="none"
232+
stroke="currentColor"
233+
stroke-width="2"
234+
stroke-linecap="round"
235+
stroke-linejoin="round"
236+
>
237+
<circle cx="12" cy="18" r="3" />
238+
<circle cx="6" cy="6" r="3" />
239+
<circle cx="18" cy="6" r="3" />
240+
<path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" />
241+
<path d="M12 12v3" />
242+
</svg>
243+
{{ props.forks.toLocaleString() }}
244+
</span>
245+
</div>
246+
247+
<!-- npm stats row -->
248+
<div class="flex flex-row items-center gap-12 text-3xl text-[#a3a3a3]">
249+
<span v-if="props.downloads" class="flex flex-row items-center gap-3">
250+
<svg
251+
width="28"
252+
height="28"
253+
viewBox="0 0 24 24"
254+
fill="none"
255+
stroke="currentColor"
256+
stroke-width="2"
257+
stroke-linecap="round"
258+
stroke-linejoin="round"
259+
>
260+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
261+
<polyline points="7 10 12 15 17 10" />
262+
<line x1="12" x2="12" y1="15" y2="3" />
263+
</svg>
264+
{{ props.downloads }}/wk
265+
</span>
266+
<span v-if="props.directDepsCount !== undefined" class="flex flex-row items-center gap-3">
267+
<svg
268+
width="28"
269+
height="28"
270+
viewBox="0 0 24 24"
271+
fill="none"
272+
stroke="currentColor"
273+
stroke-width="2"
274+
stroke-linecap="round"
275+
stroke-linejoin="round"
276+
>
277+
<path
278+
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
279+
/>
280+
</svg>
281+
{{ props.directDepsCount }} deps
282+
</span>
283+
<span v-if="props.license" class="flex flex-row items-center gap-3">
284+
<svg
285+
width="28"
286+
height="28"
287+
viewBox="0 0 24 24"
288+
fill="none"
289+
stroke="currentColor"
290+
stroke-width="2"
291+
stroke-linecap="round"
292+
stroke-linejoin="round"
293+
>
294+
<circle cx="12" cy="12" r="10" />
295+
<path d="m15 9-6 6" />
296+
<path d="M9 9h.01" />
297+
<path d="M15 15h.01" />
298+
</svg>
299+
{{ props.license }}
300+
</span>
301+
<span v-if="props.updatedAt" class="flex flex-row items-center gap-3">
302+
<svg
303+
width="28"
304+
height="28"
305+
viewBox="0 0 24 24"
306+
fill="none"
307+
stroke="currentColor"
308+
stroke-width="2"
309+
stroke-linecap="round"
310+
stroke-linejoin="round"
311+
>
312+
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
313+
<line x1="16" x2="16" y1="2" y2="6" />
314+
<line x1="8" x2="8" y1="2" y2="6" />
315+
<line x1="3" x2="21" y1="10" y2="10" />
316+
</svg>
317+
{{ props.updatedAt }}
89318
</span>
90-
<span v-if="license"> • {{ license }}</span>
91319
</div>
92320
</div>
93321

322+
<!-- Background glow -->
94323
<div
95324
class="absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl"
96-
:style="{ backgroundColor: primaryColor + '10' }"
325+
:style="{ backgroundColor: props.primaryColor + '10' }"
97326
/>
98327
</div>
99328
</template>

0 commit comments

Comments
 (0)