Skip to content

Commit 6a86fa3

Browse files
committed
feat: og images
1 parent 4d97d7f commit 6a86fa3

23 files changed

Lines changed: 1458 additions & 508 deletions

.storybook/preview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ globalThis['__NUXT_COLOR_MODE__'] ??= {
1515
removeColorScheme: fn(),
1616
}
1717
// @ts-expect-error - dynamic global name
18-
globalThis.defineOgImageComponent = fn()
18+
globalThis.defineOgImage = fn()
1919

2020
const preview: Preview = {
2121
parameters: {

app/components/OgBrand.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
const props = withDefaults(
3+
defineProps<{
4+
height?: number
5+
}>(),
6+
{
7+
height: 60,
8+
},
9+
)
10+
11+
const width = computed(() => Math.round(props.height * (602 / 170)))
12+
</script>
13+
14+
<template>
15+
<img
16+
src="/logo.svg"
17+
alt="npmx"
18+
:width="width"
19+
:height="height"
20+
:style="{ width: `${width}px`, height: `${height}px` }"
21+
/>
22+
</template>
Lines changed: 38 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
<script setup lang="ts">
22
import type { ResolvedAuthor } from '#shared/schemas/blog'
33
4-
const props = withDefaults(
5-
defineProps<{
6-
title: string
7-
authors?: ResolvedAuthor[]
8-
date?: string
9-
primaryColor?: string
10-
}>(),
11-
{
12-
authors: () => [],
13-
date: '',
14-
primaryColor: '#60a5fa',
15-
},
16-
)
4+
const {
5+
title,
6+
authors = [],
7+
date = '',
8+
} = defineProps<{
9+
title: string
10+
authors?: ResolvedAuthor[]
11+
date?: string
12+
}>()
1713
1814
const formattedDate = computed(() => {
19-
if (!props.date) return ''
15+
if (!date) return ''
2016
try {
21-
return new Date(props.date).toLocaleDateString('en-US', {
17+
return new Date(date).toLocaleDateString('en-US', {
2218
year: 'numeric',
2319
month: 'short',
2420
day: 'numeric',
2521
})
2622
} catch {
27-
return props.date
23+
return date
2824
}
2925
})
3026
@@ -39,71 +35,54 @@ const getInitials = (name: string) =>
3935
.slice(0, 2)
4036
4137
const visibleAuthors = computed(() => {
42-
if (props.authors.length <= 3) return props.authors
43-
return props.authors.slice(0, MAX_VISIBLE_AUTHORS)
38+
if (authors.length <= 3) return authors
39+
return authors.slice(0, MAX_VISIBLE_AUTHORS)
4440
})
4541
4642
const extraCount = computed(() => {
47-
if (props.authors.length <= 3) return 0
48-
return props.authors.length - MAX_VISIBLE_AUTHORS
43+
if (authors.length <= 3) return 0
44+
return authors.length - MAX_VISIBLE_AUTHORS
4945
})
5046
5147
const formattedAuthorNames = computed(() => {
52-
const allNames = props.authors.map(a => a.name)
48+
const allNames = authors.map(a => a.name)
5349
if (allNames.length === 0) return ''
5450
if (allNames.length === 1) return allNames[0]
5551
if (allNames.length === 2) return `${allNames[0]} and ${allNames[1]}`
5652
if (allNames.length === 3) return `${allNames[0]}, ${allNames[1]}, and ${allNames[2]}`
57-
// More than 3: show first 2 + others
5853
const shown = allNames.slice(0, MAX_VISIBLE_AUTHORS)
5954
const remaining = allNames.length - MAX_VISIBLE_AUTHORS
6055
return `${shown.join(', ')} and ${remaining} others`
6156
})
6257
</script>
6358

6459
<template>
65-
<div
66-
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
67-
>
68-
<!-- npmx logo - top right -->
69-
<div
70-
class="absolute top-12 z-10 flex items-center gap-1 text-5xl font-bold tracking-tight"
71-
style="font-family: 'Geist', sans-serif; right: 6rem"
72-
>
73-
<span :style="{ color: primaryColor }" class="opacity-80">./</span>
74-
<span class="text-white">npmx</span>
75-
</div>
60+
<OgLayout>
61+
<div class="px-15 py-12 flex flex-col justify-center gap-5 h-full">
62+
<OgBrand :height="48" />
7663

77-
<div class="relative z-10 flex flex-col gap-2">
78-
<!-- Date -->
79-
<span
80-
v-if="formattedDate"
81-
class="text-3xl text-[#a3a3a3] font-light"
82-
style="font-family: 'Geist', sans-serif"
83-
>
84-
{{ formattedDate }}
85-
</span>
64+
<!-- Date + Title -->
65+
<div class="flex flex-col gap-2">
66+
<span v-if="formattedDate" class="text-3xl text-fg-muted">
67+
{{ formattedDate }}
68+
</span>
8669

87-
<!-- Blog title -->
88-
<h1
89-
class="text-6xl font-semibold tracking-tight leading-snug w-9/10"
90-
style="font-family: 'Geist', sans-serif; letter-spacing: -0.03em"
91-
>
92-
{{ title }}
93-
</h1>
70+
<div
71+
class="lg:text-6xl text-5xl tracking-tighter font-mono leading-tight"
72+
:style="{ lineClamp: 2, textOverflow: 'ellipsis' }"
73+
>
74+
{{ title }}
75+
</div>
76+
</div>
9477

9578
<!-- Authors -->
96-
<div
97-
v-if="authors.length"
98-
class="flex items-center gap-4 self-start justify-start flex-nowrap"
99-
style="font-family: 'Geist', sans-serif"
100-
>
79+
<div v-if="authors.length" class="flex items-center gap-4 flex-nowrap">
10180
<!-- Stacked avatars -->
10281
<span>
10382
<span
10483
v-for="(author, index) in visibleAuthors"
10584
:key="author.name"
106-
class="flex items-center justify-center rounded-full border border-[#050505] bg-[#1a1a1a] overflow-hidden w-12 h-12"
85+
class="flex items-center justify-center rounded-full border border-bg bg-bg-muted overflow-hidden w-12 h-12"
10786
:style="{ marginLeft: index > 0 ? '-20px' : '0' }"
10887
>
10988
<img
@@ -114,29 +93,22 @@ const formattedAuthorNames = computed(() => {
11493
height="48"
11594
class="w-full h-full object-cover"
11695
/>
117-
<span v-else style="font-size: 20px; color: #666; font-weight: 500">
96+
<span v-else class="text-5 text-fg-muted font-medium">
11897
{{ getInitials(author.name) }}
11998
</span>
12099
</span>
121100
<!-- +N badge -->
122101
<span
123102
v-if="extraCount > 0"
124-
class="flex items-center justify-center text-lg font-medium text-[#a3a3a3] rounded-full border border-[#050505] bg-[#262626] overflow-hidden w-12 h-12"
103+
class="flex items-center justify-center text-lg font-medium text-fg-muted rounded-full border border-bg bg-bg-muted overflow-hidden w-12 h-12"
125104
:style="{ marginLeft: '-20px' }"
126105
>
127106
+{{ extraCount }}
128107
</span>
129108
</span>
130109
<!-- Names -->
131-
<span style="font-size: 24px; color: #a3a3a3; font-weight: 300">{{
132-
formattedAuthorNames
133-
}}</span>
110+
<span class="text-6 text-fg-muted font-light">{{ formattedAuthorNames }}</span>
134111
</div>
135112
</div>
136-
137-
<div
138-
class="absolute -top-32 -inset-ie-32 w-[550px] h-[550px] rounded-full blur-3xl"
139-
:style="{ backgroundColor: primaryColor + '10' }"
140-
/>
141-
</div>
113+
</OgLayout>
142114
</template>

app/components/OgImage/Package.takumi.vue

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const KIND_ICONS: Record<string, string> = {
1616
import type { JsDelivrFileNode } from '#shared/types'
1717
import { joinURL } from 'ufo'
1818
import { smoothPath, useCharts } from '~/composables/useCharts'
19-
import { useSiteConfig } from '#site-config/app/composables'
2019
2120
const { name, version, variant } = defineProps<{
2221
name: string
@@ -64,8 +63,6 @@ const formattedLikes = computed(() =>
6463
totalLikes.value ? compactFormat.format(totalLikes.value) : '',
6564
)
6665
67-
const { name: siteName } = useSiteConfig()
68-
6966
const pkgNameParts = computed(() => {
7067
const n = pkg.value?.name
7168
if (!n?.startsWith('@')) return { org: null, short: n }
@@ -234,20 +231,9 @@ const sparklineSrc = computed(() => {
234231
</script>
235232

236233
<template>
237-
<div class="flex flex-col justify-center w-full bg-bg text-fg relative overflow-hidden font-sans">
238-
<div class="absolute -top-10 force-left-12 size-[700px] rounded-full blur-3xl bg-fg/3" />
239-
240-
<div class="p-15 flex flex-col gap-12">
241-
<div class="flex gap-4">
242-
<img
243-
src="/logo-icon.svg"
244-
width="60"
245-
height="60"
246-
alt="npmx logo"
247-
class="w-[60px] h-[60px]"
248-
/>
249-
<h1 class="text-5xl tracking-tighter font-mono">{{ siteName }}</h1>
250-
</div>
234+
<OgLayout>
235+
<div class="px-15 py-12 flex flex-col justify-center gap-12 h-full">
236+
<OgBrand :height="48" />
251237

252238
<div class="flex flex-col max-w-full gap-3">
253239
<div
@@ -277,20 +263,29 @@ const sparklineSrc = computed(() => {
277263

278264
<div class="flex items-center gap-5 text-4xl text-fg-muted">
279265
<div v-if="repositoryUrl" class="flex items-center gap-2">
280-
<div class="i-simple-icons:github w-8 h-8 text-fg-muted" />
266+
<div
267+
class="i-simple-icons:github shrink-0 text-fg-muted"
268+
:style="{ width: '32px', height: '32px' }"
269+
/>
281270
<span v-if="repoRef" class="max-w-[500px]" :style="{ textOverflow: 'ellipsis' }">
282271
{{ repoRef.owner }}<span class="opacity-50">/</span>{{ repoRef.repo }}
283272
</span>
284273
<span v-else>{{ $t('package.links.repo') }}</span>
285274
</div>
286275

287276
<span v-if="formattedStars" class="flex items-center gap-2" data-testid="stars">
288-
<div class="i-lucide:star w-8 h-8 text-fg-muted" :style="{ fill: 'white' }" />
277+
<div
278+
class="i-lucide:star shrink-0 text-fg-muted"
279+
:style="{ width: '32px', height: '32px', fill: 'white' }"
280+
/>
289281
<span>{{ formattedStars }}</span>
290282
</span>
291283

292284
<span v-if="formattedLikes" class="flex items-center gap-2" data-testid="likes">
293-
<div class="i-lucide:heart w-8 h-8 text-fg-muted" :style="{ fill: 'white' }" />
285+
<div
286+
class="i-lucide:heart shrink-0 text-fg-muted"
287+
:style="{ width: '32px', height: '32px', fill: 'white' }"
288+
/>
294289
<span>{{ formattedLikes }}</span>
295290
</span>
296291

@@ -299,7 +294,10 @@ const sparklineSrc = computed(() => {
299294
class="flex items-center gap-2"
300295
data-testid="license"
301296
>
302-
<div class="i-lucide:scale w-8 h-8 text-fg-subtle flex-shrink-0 self-center" />
297+
<div
298+
class="i-lucide:scale shrink-0 text-fg-subtle self-center"
299+
:style="{ width: '32px', height: '32px' }"
300+
/>
303301
<span>{{ pkg.license }}</span>
304302
</div>
305303
</div>
@@ -380,5 +378,5 @@ const sparklineSrc = computed(() => {
380378
}}</span>
381379
</div>
382380
</div>
383-
</div>
381+
</OgLayout>
384382
</template>
Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
11
<script setup lang="ts">
2-
defineProps<{
2+
const { title, description } = defineProps<{
33
title?: string
44
description?: string
55
}>()
6-
7-
const { name: siteName } = useSiteConfig()
86
</script>
97

108
<template>
11-
<div
12-
data-theme="dark"
13-
class="flex w-full h-full items-center justify-center bg-bg text-fg font-sans"
14-
>
15-
<div class="absolute -top-10 force-left-12 size-[700px] rounded-full blur-3xl bg-fg/3" />
16-
17-
<div class="p-15 flex flex-col gap-12">
18-
<div class="flex gap-4">
19-
<img
20-
src="/logo-icon.svg"
21-
width="60"
22-
height="60"
23-
alt="npmx logo"
24-
class="w-[60px] h-[60px]"
25-
/>
26-
<h1 class="text-5xl tracking-tighter font-mono">{{ siteName }}</h1>
27-
</div>
9+
<OgLayout>
10+
<div class="px-15 py-12 flex flex-col justify-center gap-12 h-full">
11+
<OgBrand :height="48" />
2812

2913
<div class="flex flex-col max-w-full gap-3">
3014
<div
@@ -43,5 +27,5 @@ const { name: siteName } = useSiteConfig()
4327
{{ description }}
4428
</div>
4529
</div>
46-
</div>
30+
</OgLayout>
4731
</template>

0 commit comments

Comments
 (0)