Skip to content

Commit 8f545f7

Browse files
committed
refactor(og-image): convert Compare to takumi
- Create Compare.takumi.vue wrapped in OgLayout/OgBrand with theme tokens - Switch compare.vue page to defineOgImage('Compare.takumi', ...) - Remove legacy /__og-image__ route rules (Compare was the last consumer) - Update robots.txt to allow new /_og/d/* path - Add compare to og-image e2e snapshot coverage - Rename testCases -> TEST_CASES (screaming snake)
1 parent 3e65f8b commit 8f545f7

File tree

6 files changed

+45
-107
lines changed

6 files changed

+45
-107
lines changed
Lines changed: 30 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
<script setup lang="ts">
2-
import { computed, ref } from 'vue'
32
import { encodePackageName } from '#shared/utils/npm'
43
5-
const props = withDefaults(
6-
defineProps<{
7-
packages?: string | string[]
8-
emptyDescription?: string
9-
primaryColor?: string
10-
}>(),
11-
{
12-
packages: () => [],
13-
emptyDescription: 'Compare npm packages side-by-side',
14-
primaryColor: '#60a5fa',
15-
},
16-
)
4+
const { packages = [], emptyDescription = 'Compare npm packages side-by-side' } = defineProps<{
5+
packages?: string | string[]
6+
emptyDescription?: string
7+
}>()
178
189
const ACCENT_COLORS = [
1910
'#60a5fa',
@@ -37,7 +28,7 @@ const GRID_MAX = 12
3728
const SUMMARY_TOP_COUNT = 3
3829
3930
const displayPackages = computed(() => {
40-
const raw = props.packages
31+
const raw = packages
4132
return (typeof raw === 'string' ? raw.split(',') : raw).map(p => p.trim()).filter(Boolean)
4233
})
4334
@@ -57,7 +48,7 @@ interface PkgStats {
5748
color: string
5849
}
5950
60-
const stats = ref<PkgStats[]>([])
51+
const stats = shallowRef<PkgStats[]>([])
6152
6253
const FETCH_TIMEOUT_MS = 2500
6354
@@ -122,61 +113,27 @@ const summaryRemainder = computed(() =>
122113
</script>
123114

124115
<template>
125-
<div
126-
class="h-full w-full flex flex-col justify-center relative overflow-hidden bg-[#050505] text-[#fafafa] px-20"
127-
style="font-family: 'Geist Mono', sans-serif"
128-
>
129-
<div class="relative z-10 flex flex-col gap-5">
130-
<!-- Icon + title row -->
131-
<div class="flex items-start gap-4">
132-
<div
133-
class="flex items-center justify-center w-16 h-16 p-3.5 rounded-xl shadow-lg"
134-
:style="{ background: `linear-gradient(to top right, #3b82f6, ${primaryColor})` }"
135-
>
136-
<svg
137-
width="36"
138-
height="36"
139-
viewBox="0 0 24 24"
140-
fill="none"
141-
stroke="white"
142-
stroke-width="2.5"
143-
stroke-linecap="round"
144-
stroke-linejoin="round"
145-
>
146-
<path d="m7.5 4.27 9 5.15" />
147-
<path
148-
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"
149-
/>
150-
<path d="m3.3 7 8.7 5 8.7-5" />
151-
<path d="M12 22V12" />
152-
</svg>
153-
</div>
116+
<OgLayout>
117+
<div class="px-15 py-12 flex flex-col justify-center gap-10 h-full">
118+
<OgBrand :height="48" />
154119

155-
<h1 class="text-7xl font-bold tracking-tight">
156-
<span
157-
class="opacity-80 tracking-[-0.1em]"
158-
:style="{ color: primaryColor }"
159-
style="margin-right: 0.25rem"
160-
>./</span
161-
>compare
120+
<div class="flex items-baseline gap-3">
121+
<h1 class="text-7xl font-mono tracking-tighter leading-none">
122+
<span class="opacity-50">./</span>compare
162123
</h1>
163124
</div>
164125

165126
<!-- Empty state -->
166-
<div
167-
v-if="displayPackages.length === 0"
168-
class="text-4xl text-[#a3a3a3]"
169-
style="font-family: 'Geist', sans-serif"
170-
>
127+
<div v-if="displayPackages.length === 0" class="text-4xl text-fg-muted">
171128
{{ emptyDescription }}
172129
</div>
173130

174131
<!-- FULL layout (1-4 packages): name + downloads + version badge + bar -->
175132
<div v-else-if="layoutTier === 'full'" class="flex flex-col gap-2">
176133
<div v-for="pkg in stats" :key="pkg.name" class="flex flex-col gap-1">
177-
<div class="flex items-center gap-3" style="font-family: 'Geist', sans-serif">
134+
<div class="flex items-center gap-3">
178135
<span
179-
class="text-2xl font-semibold tracking-tight"
136+
class="text-2xl font-semibold tracking-tight font-mono"
180137
:style="{
181138
color: pkg.color,
182139
maxWidth: '400px',
@@ -189,7 +146,7 @@ const summaryRemainder = computed(() =>
189146
</span>
190147
<span
191148
v-if="pkg.version"
192-
class="text-lg px-2 py-0.5 rounded-md border"
149+
class="text-lg px-2 py-0.5 rounded-md border font-mono"
193150
:style="{
194151
color: pkg.color,
195152
backgroundColor: pkg.color + '10',
@@ -198,7 +155,7 @@ const summaryRemainder = computed(() =>
198155
>
199156
{{ pkg.version }}
200157
</span>
201-
<span class="text-3xl font-bold text-[#fafafa]">
158+
<span class="text-3xl font-bold text-fg">
202159
{{ formatDownloads(pkg.downloads) }}/wk
203160
</span>
204161
</div>
@@ -215,9 +172,9 @@ const summaryRemainder = computed(() =>
215172
<!-- COMPACT layout (5-6 packages): name + downloads + thinner bar, no version -->
216173
<div v-else-if="layoutTier === 'compact'" class="flex flex-col gap-2">
217174
<div v-for="pkg in stats" :key="pkg.name" class="flex flex-col gap-0.5">
218-
<div class="flex items-center gap-2" style="font-family: 'Geist', sans-serif">
175+
<div class="flex items-center gap-2">
219176
<span
220-
class="text-xl font-semibold tracking-tight"
177+
class="text-xl font-semibold tracking-tight font-mono"
221178
:style="{
222179
color: pkg.color,
223180
maxWidth: '300px',
@@ -230,7 +187,7 @@ const summaryRemainder = computed(() =>
230187
</span>
231188
<span
232189
v-if="pkg.version"
233-
class="text-sm px-1.5 py-0.5 rounded border"
190+
class="text-sm px-1.5 py-0.5 rounded border font-mono"
234191
:style="{
235192
color: pkg.color,
236193
backgroundColor: pkg.color + '10',
@@ -239,9 +196,7 @@ const summaryRemainder = computed(() =>
239196
>
240197
{{ pkg.version }}
241198
</span>
242-
<span class="text-xl font-bold text-[#fafafa]">
243-
{{ formatDownloads(pkg.downloads) }}/wk
244-
</span>
199+
<span class="text-xl font-bold text-fg"> {{ formatDownloads(pkg.downloads) }}/wk </span>
245200
</div>
246201
<div
247202
class="h-3 rounded-sm"
@@ -261,7 +216,6 @@ const summaryRemainder = computed(() =>
261216
flexWrap: 'wrap',
262217
rowGap: 24,
263218
columnGap: 40,
264-
fontFamily: 'Geist, sans-serif',
265219
}"
266220
>
267221
<span
@@ -275,7 +229,7 @@ const summaryRemainder = computed(() =>
275229
}"
276230
>
277231
<span
278-
class="font-semibold tracking-tight"
232+
class="font-semibold tracking-tight font-mono"
279233
:style="{
280234
fontSize: '18px',
281235
maxWidth: '220px',
@@ -287,25 +241,23 @@ const summaryRemainder = computed(() =>
287241
>{{ pkg.name }}</span
288242
>
289243
<span :style="{ display: 'flex', alignItems: 'baseline', gap: 2 }">
290-
<span class="text-2xl font-bold text-[#e5e5e5]">{{
291-
formatDownloads(pkg.downloads)
292-
}}</span>
293-
<span class="text-sm font-medium text-[#d4d4d4]">/wk</span>
244+
<span class="text-2xl font-bold text-fg">{{ formatDownloads(pkg.downloads) }}</span>
245+
<span class="text-sm font-medium text-fg-muted">/wk</span>
294246
</span>
295247
</span>
296248
</div>
297249

298250
<!-- SUMMARY layout (13+ packages): package count + top names -->
299-
<div v-else class="flex flex-col gap-3" style="font-family: 'Geist', sans-serif">
300-
<div class="text-2xl text-[#a3a3a3]">
301-
<span class="text-4xl font-bold text-[#fafafa]">{{ displayPackages.length }}</span>
251+
<div v-else class="flex flex-col gap-3">
252+
<div class="text-2xl text-fg-muted">
253+
<span class="text-4xl font-bold text-fg">{{ displayPackages.length }}</span>
302254
packages
303255
</div>
304256
<div :style="{ display: 'flex', alignItems: 'baseline', gap: 8, whiteSpace: 'nowrap' }">
305257
<span
306258
v-for="(name, i) in summaryTopNames"
307259
:key="name"
308-
class="text-xl font-semibold"
260+
class="text-xl font-semibold font-mono"
309261
:style="{
310262
color: ACCENT_COLORS[i % ACCENT_COLORS.length],
311263
maxWidth: '280px',
@@ -316,24 +268,11 @@ const summaryRemainder = computed(() =>
316268
}"
317269
>{{ name }}{{ i < summaryTopNames.length - 1 ? ',' : '' }}</span
318270
>
319-
<span v-if="summaryRemainder > 0" class="text-xl text-[#737373]">
271+
<span v-if="summaryRemainder > 0" class="text-xl text-fg-subtle">
320272
+{{ summaryRemainder }} more
321273
</span>
322274
</div>
323275
</div>
324276
</div>
325-
326-
<!-- Branding -->
327-
<div
328-
class="absolute bottom-6 inset-ie-20 text-lg font-semibold tracking-tight text-[#525252]"
329-
style="font-family: 'Geist Mono', sans-serif"
330-
>
331-
<span :style="{ color: primaryColor }" class="opacity-80 tracking-[-0.1em]">./</span>npmx
332-
</div>
333-
334-
<div
335-
class="absolute -top-32 -inset-ie-32 w-[550px] h-[550px] rounded-full blur-3xl"
336-
:style="{ backgroundColor: primaryColor + '10' }"
337-
/>
338-
</div>
277+
</OgLayout>
339278
</template>

app/pages/compare.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,14 @@ async function exportComparisonDataAsMarkdown() {
139139
await copy(markdown)
140140
}
141141
142-
defineOgImageComponent('Compare', {
143-
packages: () => packages.value.toSorted((a, b) => a.localeCompare(b)),
144-
emptyDescription: () => $t('compare.packages.meta_description_empty'),
145-
})
142+
defineOgImage(
143+
'Compare.takumi',
144+
{
145+
packages: () => packages.value.toSorted((a, b) => a.localeCompare(b)),
146+
emptyDescription: () => $t('compare.packages.meta_description_empty'),
147+
},
148+
{ alt: () => $t('compare.packages.meta_description_empty') },
149+
)
146150
147151
const { announce } = useCommandPalette()
148152

nuxt.config.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,6 @@ export default defineNuxtConfig({
128128
'/:pkg/.well-known/skills/**': { isr: 3600 },
129129
'/:scope/:pkg/.well-known/skills/**': { isr: 3600 },
130130
'/_og/d/**': getISRConfig(60 * 60 * 24), // 1 day
131-
'/__og-image__/**': getISRConfig(3600),
132-
'/__og-image__/image/compare/**': {
133-
isr: {
134-
expiration: 3600,
135-
passQuery: true,
136-
allowQuery: ['packages', '_query'],
137-
},
138-
},
139131
'/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' },
140132
'/opensearch.xml': { isr: true },
141133
'/oauth-client-metadata.json': { prerender: true },

public/robots.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Allow: /pds$
77
Allow: /privacy$
88
Allow: /translation-status$
99
Allow: /recharging$
10-
Allow: /__og-image__/*
10+
Allow: /_og/d/*
1111
Allow: /opensearch.xml$
1212

1313
Disallow: /

test/e2e/og-image.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { expect, test } from './test-utils'
1010
* - Static pages (Page.takumi)
1111
* - Packages (Package.takumi with download-chart, code-tree, function-tree variants)
1212
*/
13-
const testCases = [
13+
const TEST_CASES = [
1414
// Default OG image template
1515
{ path: '/', label: 'home page' },
1616

@@ -30,14 +30,17 @@ const testCases = [
3030
// Blog post OG image template
3131
{ path: '/blog/alpha-release', label: 'blog post' },
3232

33+
// Compare OG image template
34+
{ path: '/compare?packages=vue,react,svelte', label: 'compare' },
35+
3336
// Package code-tree variant (file tree decoration)
3437
{ path: '/package-code/vue/v/3.5.27', label: 'code-tree variant' },
3538

3639
// Package function-tree variant (API symbols decoration)
3740
{ path: '/package-docs/ufo/v/1.6.3', label: 'function-tree variant' },
3841
] as const
3942

40-
for (const { path, label } of testCases) {
43+
for (const { path, label } of TEST_CASES) {
4144
test.describe(`${label} (${path})`, () => {
4245
test(`og image snapshot`, async ({ page, goto, baseURL }) => {
4346
await goto(path, { waitUntil: 'domcontentloaded' })

test/unit/a11y-component-coverage.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const SKIPPED_COMPONENTS: Record<string, string> = {
2525
'OgBrand.vue': 'OG Image component - server-rendered image, not interactive UI',
2626
'OgLayout.vue': 'OG Image component - server-rendered image, not interactive UI',
2727
'OgImage/BlogPost.takumi.vue': 'OG Image component - server-rendered image, not interactive UI',
28-
'OgImage/Compare.vue': 'OG Image component - server-rendered image, not interactive UI',
28+
'OgImage/Compare.takumi.vue': 'OG Image component - server-rendered image, not interactive UI',
2929
'OgImage/Package.takumi.vue': 'OG Image component - server-rendered image, not interactive UI',
3030
'OgImage/Page.takumi.vue': 'OG Image component - server-rendered image, not interactive UI',
3131
'OgImage/Profile.takumi.vue': 'OG Image component - server-rendered image, not interactive UI',

0 commit comments

Comments
 (0)