Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ const footerSections = computed<Array<{ label: string; links: FooterLink[] }>>((
name: t('footer.about'),
href: '/about',
},
{
name: t('footer.sponsors'),
href: '/sponsors',
},
{
name: t('footer.brand'),
href: '/brand',
Expand Down
10 changes: 10 additions & 0 deletions app/composables/useCommandPaletteGlobalCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ export function useCommandPaletteGlobalCommands() {
),
to: { name: 'noodles' },
},
{
id: 'sponsors',
group: 'npmx',
label: t('sponsors_page.title'),
keywords: [t('sponsors_page.title')],
iconClass: 'i-lucide:heart',
active: route.name === 'sponsors',
activeLabel: activeLabel(route.name === 'sponsors', t('command_palette.here')),
to: { name: 'sponsors' },
},
{
id: 'brand',
group: 'npmx',
Expand Down
5 changes: 5 additions & 0 deletions app/pages/about.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ const communityContributors = computed(
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('about.sponsors.title') }}
</h2>
<p class="text-fg-muted text-sm leading-relaxed mb-4">
<LinkBase to="/sponsors" no-new-tab-icon>
{{ $t('sponsors_page.cta') }}
</LinkBase>
</p>
<h3 class="block text-sm text-fg uppercase tracking-wider mb-3">
{{ $t('about.sponsors.gold') }}
</h3>
Expand Down
250 changes: 250 additions & 0 deletions app/pages/sponsors.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<script setup lang="ts">
import { SPONSORS } from '~/assets/logos/sponsors'

const SPONSORSHIP_TIER_PRICES = {
silver: 500,
gold: 1000,
} as const

const { t } = useI18n()
const currencyFormatter = useNumberFormatter({
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
})

function formatTierPrice(amount: number) {
return `${currencyFormatter.value.format(amount)}${t('sponsors_page.tiers.per_month')}`
}

useSeoMeta({
title: () => `${$t('sponsors_page.title')} - npmx`,
ogTitle: () => `${$t('sponsors_page.title')} - npmx`,
twitterTitle: () => `${$t('sponsors_page.title')} - npmx`,
description: () => $t('sponsors_page.meta_description'),
ogDescription: () => $t('sponsors_page.meta_description'),
twitterDescription: () => $t('sponsors_page.meta_description'),
})

defineOgImage(
'Page.takumi',
{
title: () => $t('sponsors_page.title'),
description: () => $t('sponsors_page.meta_description'),
},
{ alt: () => `${$t('sponsors_page.title')} — npmx` },
)
</script>

<template>
<main class="container flex-1 py-12 sm:py-16">
<header class="mb-12">
<div class="flex items-baseline justify-between gap-4 mb-4">
<h1 class="font-mono text-3xl sm:text-4xl font-medium">
{{ $t('sponsors_page.heading') }}
</h1>
<BackButton />
</div>
<p class="text-fg-muted text-lg leading-relaxed">
{{ $t('sponsors_page.intro') }}
</p>
</header>

<div class="space-y-12" :class="$style.grid">
<section :class="$style.intro">
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('sponsors_page.what_we_do.title') }}
</h2>
<p class="text-fg-muted leading-relaxed">
{{ $t('sponsors_page.what_we_do.description') }}
</p>
</section>

<aside :class="$style.sponsors">
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('about.sponsors.title') }}
</h2>
<h3 class="block text-sm text-fg uppercase tracking-wider mb-3">
{{ $t('about.sponsors.gold') }}
</h3>
<AboutLogoList
:list="SPONSORS.gold"
class="grid gap-3 grid-cols-[repeat(auto-fill,minmax(160px,1fr))] lg:grid-cols-1"
/>
<h3 class="block text-sm text-fg uppercase tracking-wider mb-3 mt-6">
{{ $t('about.sponsors.silver') }}
</h3>
<AboutLogoList
:list="SPONSORS.silver"
class="grid gap-3 grid-cols-[repeat(auto-fill,minmax(160px,1fr))] lg:grid-cols-1"
/>
</aside>

<section :class="$style.content">
<div>
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('sponsors_page.what_this_means_for_you.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-4">
{{ $t('sponsors_page.what_this_means_for_you.description') }}
</p>
<div class="grid gap-4 sm:grid-cols-12">
<section class="border border-border bg-bg-elevated rounded-lg p-5 sm:col-span-7">
<div class="grid grid-cols-2 gap-4">
<div>
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">250+</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.people.contributors') }}
</p>
</div>
<div>
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">700+</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.people.community_members') }}
</p>
</div>
</div>
</section>
<section class="border border-border rounded-lg p-5 bg-bg-subtle sm:col-span-5">
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">200000+</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.visitors.description') }}
</p>
</section>
<section class="border border-border rounded-lg p-5 bg-bg-subtle sm:col-span-4">
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">3400+</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.stars.title') }}
</p>
</section>
<section class="border border-border rounded-lg p-5 bg-bg-subtle sm:col-span-8">
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">
{{ $t('sponsors_page.what_this_means_for_you.cards.community.title') }}
</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.community.description')
}}<sup>*</sup>
</p>
</section>
<section class="border border-border rounded-lg p-5 bg-bg-subtle sm:col-span-6">
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">
{{ $t('sponsors_page.what_this_means_for_you.cards.adoption.title') }}
</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.adoption.description') }}
</p>
</section>
<section class="border border-border rounded-lg p-5 bg-bg-subtle sm:col-span-6">
<p class="font-mono text-2xl text-fg uppercase tracking-wider mb-2">
{{ $t('sponsors_page.what_this_means_for_you.cards.default_source.title') }}
</p>
<p class="text-fg-muted text-sm leading-relaxed m-0">
{{ $t('sponsors_page.what_this_means_for_you.cards.default_source.description') }}
</p>
</section>
</div>
Comment on lines +91 to +145

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Locale-format the headline metrics instead of hardcoding raw numerals.

250+, 700+, 200000+, and 3400+ bypass localisation, so translated pages will still show unformatted English numerals. Since this page already formats currency via useNumberFormatter, these counts should go through $n() or the same formatter and append the + separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/sponsors.vue` around lines 91 - 145, The headline metrics in
sponsors.vue are hardcoded numerals, so they bypass locale formatting and won’t
adapt on translated pages. Update the metric blocks in the sponsors page
template to use the existing number formatting approach already used elsewhere
(such as the `useNumberFormatter`/`$n()` pattern), and keep the `+` suffix
separate from the formatted number. Focus on the static values in the section
cards (`250`, `700`, `200000`, `3400`) and preserve the current labels and
layout.

</div>

<div class="mt-6">
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('sponsors_page.what_support_means.title') }}
</h2>
<p class="text-fg-muted leading-relaxed">
{{ $t('sponsors_page.what_support_means.description') }}
</p>
</div>

<div class="mt-6">
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('sponsors_page.tiers.title') }}
</h2>

<div class="border border-fg/80 rounded-xl p-5 bg-bg-muted">
<div class="flex items-center justify-between gap-3 mb-2">
<h3 class="font-mono text-base text-fg uppercase tracking-wider">
{{ $t('sponsors_page.tiers.silver.name') }}
</h3>
<span class="i-lucide:coins size-4 text-fg-subtle" aria-hidden="true" />
</div>
<p class="text-sm text-fg-subtle font-mono">
{{ formatTierPrice(SPONSORSHIP_TIER_PRICES.silver) }}
</p>
</div>

<div
class="border border-badge-yellow/80 rounded-xl p-5 bg-linear-to-br from-badge-yellow/8 to-bg-subtle/40 relative overflow-hidden mt-4"
>
<div class="flex items-center justify-between gap-3 mb-2 relative">
<h3 class="font-mono text-base text-fg uppercase tracking-wider">
{{ $t('sponsors_page.tiers.gold.name') }}
</h3>
<span class="i-lucide:medal size-4 text-badge-yellow" aria-hidden="true" />
</div>
<p class="text-sm text-fg-subtle font-mono">
{{ formatTierPrice(SPONSORSHIP_TIER_PRICES.gold) }}
</p>
</div>

<div
class="border border-badge-blue/80 rounded-xl p-5 bg-linear-to-br from-badge-blue/8 to-bg-subtle/40 relative overflow-hidden mt-4"
>
<div class="flex items-center justify-between gap-3 mb-2">
<h3 class="font-mono text-base text-fg uppercase tracking-wider">
{{ $t('sponsors_page.tiers.platinum.name') }}
</h3>
<span class="i-lucide:crown size-4 text-badge-blue" aria-hidden="true" />
</div>
<p class="text-sm text-fg-subtle font-mono">
{{ $t('sponsors_page.tiers.custom') }}
</p>
</div>
</div>
</section>
</div>

<p class="text-fg-subtle text-xs leading-relaxed mt-12">
<i18n-t keypath="sponsors_page.community_growth_footnote" tag="span" scope="global">
<template #link>
<LinkBase to="https://osscar.dev/"> OSSCAR </LinkBase>
</template>
</i18n-t>
</p>
</main>
</template>

<style module>
.grid {
display: grid;
column-gap: 2rem;
row-gap: 1rem;
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
'intro'
'sponsors'
'content';
}

@media (min-width: 64rem) {
.grid {
grid-template-columns: 1fr 20rem;
grid-template-areas: 'intro sponsors' 'content sponsors';
}
}

.grid > * {
min-width: 0;
}

.intro {
grid-area: intro;
}

.sponsors {
grid-area: sponsors;
align-self: start;
}

.content {
grid-area: content;
}
</style>
59 changes: 59 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
"footer": {
"about": "about",
"sponsors": "sponsors",
"blog": "blog",
"docs": "docs",
"source": "source",
Expand Down Expand Up @@ -1268,6 +1269,64 @@
}
}
},
"sponsors_page": {
"title": "Sponsors",
"heading": "sponsors",
"meta_description": "Support npmx and help us accelerate ecosystem work around security, trust, optimization, and research.",
"intro": "Support npmx and help us grow the ecosystem work we do for developers and maintainers.",
"what_we_do": {
"title": "What we do",
"description": "We're building an ecosystem that solves problems around security, trust, optimization, and research for the tools we use in everyday development - quickly, conveniently, and with high quality. This project is built by developers for developers. As authors of widely used libraries, we understand team needs and actively study what maintainers and projects need most."
},
"what_support_means": {
"title": "What support means",
"description": "Our plans and connections keep growing, along with our desire to share what we've built and learn from others. Your support helps fund the project, external talks, conferences, and the broader ecosystem, and, most importantly, enables us to keep growing our community."
},
"cta": "See sponsorship tiers",
"community_growth_footnote": "* According to {link} Q1 2026 research.",
"what_this_means_for_you": {
"title": "What this means for you",
"description": "npmx is not only about improving developer experience and closing critical everyday gaps. In our first six months, we have already become the default source for thousands of teams and a huge number of developers. You get not only a more stable tool for your teams, but also visibility in front of a constantly growing audience of top-tier engineers.",
"cards": {
"people": {
"contributors": "contributors",
"community_members": "community members"
},
"visitors": {
"description": "unique monthly visitors"
},
"stars": {
"title": "stars"
},
"community": {
"title": "fast-growing",
"description": "We have one of the fastest-growing communities in the ecosystem"
},
"adoption": {
"title": "adoption",
"description": "charts at conferences, talks, and articles rely on our data"
},
"default_source": {
"title": "default source",
"description": "pnpm made npmx the default source, many packages have been configured to link to npmx"
}
}
},
"tiers": {
"title": "Sponsorship tiers",
"per_month": "/month",
"custom": "Custom",
"silver": {
"name": "Silver"
},
"gold": {
"name": "Gold"
},
"platinum": {
"name": "Platinum"
}
}
},
"account_menu": {
"connect": "connect",
"account": "Account",
Expand Down
Loading
Loading