[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: better OG images (#462)

authored by

Felix Schneider and committed by
GitHub
9ef1401f db316969

+190 -19
+77 -6
app/components/OgImage/Default.vue
··· 1 + <script setup lang="ts"> 2 + interface Props { 3 + primaryColor?: string 4 + title?: string 5 + description?: string 6 + } 7 + 8 + const props = withDefaults(defineProps<Props>(), { 9 + primaryColor: '#60a5fa', 10 + title: 'npmx', 11 + description: 'A better browser for the **npm registry**', 12 + }) 13 + </script> 14 + 1 15 <template> 2 - <div class="h-full w-full flex flex-col justify-center items-center bg-[#0a0a0a] text-[#fafafa]"> 3 - <h1 class="text-6xl font-medium tracking-tight" style="font-family: 'Geist Mono'"> 4 - <span class="text-fg-subtle" style="letter-spacing: -0.1em">./</span> npmx 5 - </h1> 6 - <h1 class="text-3xl font-medium tracking-tight">a better browser for the npm registry</h1> 16 + <div 17 + class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden" 18 + > 19 + <div class="relative z-10 flex flex-col gap-6"> 20 + <div class="flex items-start gap-4"> 21 + <div 22 + class="flex items-start justify-center w-16 h-16 rounded-xl bg-gradient-to-tr from-[#3b82f6] shadow-lg" 23 + :style="{ backgroundColor: props.primaryColor }" 24 + > 25 + <svg 26 + width="36" 27 + height="36" 28 + viewBox="0 0 24 24" 29 + fill="none" 30 + stroke="white" 31 + stroke-width="2.5" 32 + stroke-linecap="round" 33 + stroke-linejoin="round" 34 + > 35 + <path d="m7.5 4.27 9 5.15" /> 36 + <path 37 + 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" 38 + /> 39 + <path d="m3.3 7 8.7 5 8.7-5" /> 40 + <path d="M12 22V12" /> 41 + </svg> 42 + </div> 43 + 44 + <h1 45 + class="text-8xl font-bold tracking-tighter" 46 + style="font-family: 'Geist Sans', sans-serif" 47 + > 48 + <span class="opacity-80" :style="{ color: props.primaryColor }">./</span>{{ props.title }} 49 + </h1> 50 + </div> 51 + 52 + <div 53 + class="flex flex-wrap items-center gap-x-3 text-4xl font-light text-[#a3a3a3]" 54 + style="font-family: 'Geist Sans', sans-serif" 55 + > 56 + <template v-for="(part, index) in props.description.split(/(\*\*.*?\*\*)/)" :key="index"> 57 + <span 58 + v-if="part.startsWith('**') && part.endsWith('**')" 59 + class="px-3 py-1 rounded-lg border font-normal" 60 + :style="{ 61 + color: props.primaryColor, 62 + backgroundColor: props.primaryColor + '10', 63 + borderColor: props.primaryColor + '30', 64 + boxShadow: `0 0 20px ${props.primaryColor}25`, 65 + }" 66 + > 67 + {{ part.replaceAll('**', '') }} 68 + </span> 69 + <span v-else-if="part.trim() !== ''"> 70 + {{ part }} 71 + </span> 72 + </template> 73 + </div> 74 + </div> 7 75 8 - <p class="absolute bottom-12 text-lg text-[#404040]">npmx.dev</p> 76 + <div 77 + class="absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl" 78 + :style="{ backgroundColor: props.primaryColor + '10' }" 79 + /> 9 80 </div> 10 81 </template>
+76 -10
app/components/OgImage/Package.vue
··· 5 5 version: string 6 6 downloads?: string 7 7 license?: string 8 + primaryColor?: string 8 9 }>(), 9 10 { 10 11 downloads: '', 11 12 license: '', 13 + primaryColor: '#60a5fa', 12 14 }, 13 15 ) 14 16 </script> 15 17 16 18 <template> 17 19 <div 18 - class="h-full w-full flex flex-col justify-center items-center bg-[#0a0a0a] text-[#fafafa]" 19 - style="font-family: 'Geist Mono'" 20 + class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden" 20 21 > 21 - <h1 class="text-6xl font-medium tracking-tight"> 22 - {{ name }} 23 - </h1> 22 + <div class="relative z-10 flex flex-col gap-6"> 23 + <div class="flex items-start gap-4"> 24 + <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 }" 27 + > 28 + <svg 29 + width="36" 30 + height="36" 31 + viewBox="0 0 24 24" 32 + fill="none" 33 + stroke="white" 34 + stroke-width="2.5" 35 + stroke-linecap="round" 36 + stroke-linejoin="round" 37 + > 38 + <path d="m7.5 4.27 9 5.15" /> 39 + <path 40 + 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" 41 + /> 42 + <path d="m3.3 7 8.7 5 8.7-5" /> 43 + <path d="M12 22V12" /> 44 + </svg> 45 + </div> 24 46 25 - <div class="flex items-center gap-6 mt-6 text-xl text-fg-subtle"> 26 - <span>v{{ version }}</span> 27 - <span v-if="downloads">{{ downloads }}/wk</span> 28 - <span v-if="license">{{ license }}</span> 47 + <h1 48 + class="text-8xl font-bold tracking-tighter" 49 + style="font-family: 'Geist Sans', sans-serif" 50 + > 51 + <span :style="{ color: primaryColor }" class="opacity-80">./</span>{{ name }} 52 + </h1> 53 + </div> 54 + 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 }} 69 + </span> 70 + <span v-if="downloads"> 71 + <span>• {{ downloads }} </span> 72 + <span class="flex items-center gap-0"> 73 + <svg 74 + width="30" 75 + height="30" 76 + viewBox="0 0 24 24" 77 + fill="none" 78 + stroke="currentColor" 79 + stroke-width="2" 80 + stroke-linecap="round" 81 + stroke-linejoin="round" 82 + class="text-white/70" 83 + > 84 + <circle cx="12" cy="12" r="10" class="opacity-40" /> 85 + <path d="M12 8v8m-3-3l3 3 3-3" /> 86 + </svg> 87 + <span>/wk</span> 88 + </span> 89 + </span> 90 + <span v-if="license"> • {{ license }}</span> 91 + </div> 29 92 </div> 30 93 31 - <p class="absolute bottom-12 text-lg text-[#404040]">npmx.dev</p> 94 + <div 95 + class="absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl" 96 + :style="{ backgroundColor: primaryColor + '10' }" 97 + /> 32 98 </div> 33 99 </template>
+1
app/pages/@[org].vue
··· 131 131 defineOgImageComponent('Default', { 132 132 title: () => `@${orgName.value}`, 133 133 description: () => (packageCount.value ? `${packageCount.value} packages` : 'npm organization'), 134 + primaryColor: '#60a5fa', 134 135 }) 135 136 </script> 136 137
+1
app/pages/[...package].vue
··· 347 347 version: () => displayVersion.value?.version ?? '', 348 348 downloads: () => (downloads.value ? formatNumber(downloads.value.downloads) : ''), 349 349 license: () => pkg.value?.license ?? '', 350 + primaryColor: '#60a5fa', 350 351 }) 351 352 352 353 // We're using only @click because it catches touch events and enter hits
+3 -2
app/pages/about.vue
··· 13 13 }) 14 14 15 15 defineOgImageComponent('Default', { 16 - title: () => $t('about.title'), 17 - description: () => $t('tagline'), 16 + primaryColor: '#60a5fa', 17 + title: 'About npmx', 18 + description: 'A better browser for the **npm registry**', 18 19 }) 19 20 20 21 const pmLinks = {
+6
app/pages/code/[...path].vue
··· 271 271 }, 272 272 description: () => `Browse source code for ${packageName.value}@${version.value}`, 273 273 }) 274 + 275 + defineOgImageComponent('Default', { 276 + title: () => `${pkg.value?.name ?? 'Package'} - Code`, 277 + description: () => pkg.value?.license ?? '', 278 + primaryColor: '#60a5fa', 279 + }) 274 280 </script> 275 281 276 282 <template>
+6
app/pages/docs/[...path].vue
··· 96 96 title: () => pageTitle.value, 97 97 }) 98 98 99 + defineOgImageComponent('Default', { 100 + title: () => `${pkg.value?.name ?? 'Package'} - Docs`, 101 + description: () => pkg.value?.license ?? '', 102 + primaryColor: '#60a5fa', 103 + }) 104 + 99 105 const showLoading = computed(() => docsStatus.value === 'pending') 100 106 const showEmptyState = computed(() => docsData.value?.status !== 'ok') 101 107 </script>
+5 -1
app/pages/index.vue
··· 25 25 description: () => $t('seo.home.description'), 26 26 }) 27 27 28 - defineOgImageComponent('Default') 28 + defineOgImageComponent('Default', { 29 + primaryColor: '#60a5fa', 30 + title: 'npmx', 31 + description: 'A better browser for the **npm registry**', 32 + }) 29 33 </script> 30 34 31 35 <template>
+1
app/pages/search.vue
··· 699 699 defineOgImageComponent('Default', { 700 700 title: 'npmx', 701 701 description: () => (query.value ? `Search results for "${query.value}"` : 'Search npm packages'), 702 + primaryColor: '#60a5fa', 702 703 }) 703 704 </script> 704 705
+1
app/pages/settings.vue
··· 21 21 defineOgImageComponent('Default', { 22 22 title: () => $t('settings.title'), 23 23 description: () => $t('settings.tagline'), 24 + primaryColor: '#60a5fa', 24 25 }) 25 26 </script> 26 27
+1
app/pages/~[username]/index.vue
··· 161 161 defineOgImageComponent('Default', { 162 162 title: () => `~${username.value}`, 163 163 description: () => (results.value ? `${results.value.total} packages` : 'npm user profile'), 164 + primaryColor: '#60a5fa', 164 165 }) 165 166 </script> 166 167
+12
app/pages/~[username]/orgs.vue
··· 101 101 title: () => `@${username.value} Organizations - npmx`, 102 102 description: () => `npm organizations for ${username.value}`, 103 103 }) 104 + 105 + defineOgImageComponent('Default', { 106 + title: () => `@${username.value}`, 107 + description: () => { 108 + if (isLoading.value) return 'npm organizations' 109 + if (orgs.value.length === 0) return 'No organizations found' 110 + 111 + const count = orgs.value.length 112 + return `${count} ${count === 1 ? 'organization' : 'organizations'}` 113 + }, 114 + primaryColor: '#60a5fa', 115 + }) 104 116 </script> 105 117 106 118 <template>