Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a04f84f
feat: nuxt og image v6
harlan-zw Feb 26, 2026
0198696
chore: sync
harlan-zw Feb 26, 2026
b76f0e2
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Feb 26, 2026
fb61483
chore: sync
harlan-zw Feb 26, 2026
b4b7ffc
chore: sync
harlan-zw Feb 26, 2026
440bdb1
chore: sync
harlan-zw Feb 26, 2026
6e89cb9
chore: sync
harlan-zw Feb 26, 2026
b366eaa
chore: sync
harlan-zw Feb 26, 2026
17e7ef7
chore: sync
harlan-zw Feb 26, 2026
6c3add5
Merge remote-tracking branch 'origin/main' into feat/og-image-v6
danielroe Feb 26, 2026
b69a6df
fix: small fixes
danielroe Feb 26, 2026
25f3f96
Merge remote-tracking branch 'origin/main' into feat/og-image-v6
danielroe Feb 26, 2026
347cbcc
chore: reorder
danielroe Feb 26, 2026
8d8a3ee
ci: bump memory
danielroe Feb 26, 2026
8e1c597
Merge remote-tracking branch 'origin/main' into feat/og-image-v6
danielroe Feb 26, 2026
2cdcc35
chore: sync
harlan-zw Feb 26, 2026
114f2e3
Merge remote-tracking branch 'origin/feat/og-image-v6' into feat/og-i…
harlan-zw Feb 26, 2026
499668f
chore: opacity
harlan-zw Feb 26, 2026
4d5afa5
fix: use utc dates, handle division by zero, + add tanstack fixture
danielroe Feb 27, 2026
b196d5a
Merge remote-tracking branch 'origin/main' into feat/og-image-v6
danielroe Feb 27, 2026
a1259f7
chore: lint
danielroe Feb 27, 2026
49a9014
fix: rename og-image snapshot for home path
danielroe Feb 27, 2026
6e62678
fix: remove UnoCSS pipeline exclude that broke a11y tests
danielroe Feb 27, 2026
9d7060c
chore: opps
danielroe Feb 27, 2026
83fa423
fix: preserve UnoCSS default pipeline excludes alongside takumi exclude
danielroe Feb 27, 2026
449232c
ci: switch browser tests to x64 runner for Takumi WASM compat
danielroe Feb 27, 2026
9800fc1
revert: restore browser test ARM runner
danielroe Feb 27, 2026
b1cae8f
chore: bump og image
harlan-zw Feb 28, 2026
81c6227
chore: bump takumi
harlan-zw Feb 28, 2026
f65e30f
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Feb 28, 2026
2382633
chore: sync lock
harlan-zw Feb 28, 2026
42896a1
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Mar 2, 2026
1f12d07
chore: blog post
harlan-zw Mar 2, 2026
ff5c315
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Mar 2, 2026
f794422
chore: sync
harlan-zw Mar 2, 2026
44eb192
chore: sync
harlan-zw Mar 2, 2026
f2de5ad
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Mar 14, 2026
4d97d7f
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/o…
harlan-zw Mar 17, 2026
6a86fa3
feat: og images
harlan-zw Mar 17, 2026
533a466
chore: takumi v1 beta
harlan-zw Mar 17, 2026
00b80ec
fix: og image alts
harlan-zw Mar 17, 2026
a2e4207
Merge branch 'main' into feat/og-image-v6
harlan-zw Mar 17, 2026
3a762ab
chore: misc issues
harlan-zw Mar 17, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ jobs:
run: pnpm build:test
env:
VALIDATE_HTML: true
NODE_OPTIONS: --max-old-space-size=6144

- name: 🖥️ Test project (browser)
run: pnpm test:browser:prebuilt
Expand All @@ -171,6 +172,8 @@ jobs:

- name: 🏗️ Build project
run: pnpm build:test
env:
NODE_OPTIONS: --max-old-space-size=6144

- name: ♿ Accessibility audit (Lighthouse - ${{ matrix.mode }} mode)
run: pnpm test:a11y:prebuilt
Expand Down
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ globalThis['__NUXT_COLOR_MODE__'] ??= {
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()
globalThis.defineOgImage = fn()

const preview: Preview = {
parameters: {
Expand Down
4 changes: 4 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ if (import.meta.client) {
useEventListener(document, 'click', handleModalLightDismiss)
}
}
// title and description will be inferred
// this will be overridden by upstream pages that use different templates
defineOgImage('Page.takumi', {}, { alt: 'npmx — a fast, modern browser for the npm registry' })
</script>

<template>
Expand Down
22 changes: 22 additions & 0 deletions app/components/OgBrand.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
height?: number
}>(),
{
height: 60,
},
)
const width = computed(() => Math.round(props.height * (602 / 170)))
</script>

<template>
<img
src="/logo.svg"
alt="npmx"
:width="width"
:height="height"
:style="{ width: `${width}px`, height: `${height}px` }"
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

height is not defined in scope – use props.height.

On line 19, :height="height" references height which isn't directly available in scope. Since props is the return value of withDefaults, you need to access props.height.

🐛 Proposed fix
     :width="width"
-    :height="height"
-    :style="{ width: `${width}px`, height: `${height}px` }"
+    :height="props.height"
+    :style="{ width: `${width}px`, height: `${props.height}px` }"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:width="width"
:height="height"
:style="{ width: `${width}px`, height: `${height}px` }"
:width="width"
:height="props.height"
:style="{ width: `${width}px`, height: `${props.height}px` }"

/>
</template>
13 changes: 0 additions & 13 deletions app/components/OgImage/BlogPost.d.vue.ts

This file was deleted.

114 changes: 114 additions & 0 deletions app/components/OgImage/BlogPost.takumi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script setup lang="ts">
import type { ResolvedAuthor } from '#shared/schemas/blog'
const {
title,
authors = [],
date = '',
} = defineProps<{
title: string
authors?: ResolvedAuthor[]
date?: string
}>()
const formattedDate = computed(() => {
if (!date) return ''
try {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
return date
}
})
const MAX_VISIBLE_AUTHORS = 2
const getInitials = (name: string) =>
name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
Comment on lines +29 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential runtime error if name contains consecutive spaces.

If name contains consecutive spaces (e.g., "John Doe"), split(' ') produces empty strings, and accessing n[0] on an empty string returns undefined. Calling .toUpperCase() on undefined throws a TypeError.

🛡️ Proposed fix to filter empty parts
 const getInitials = (name: string) =>
   name
     .split(' ')
+    .filter(n => n.length > 0)
     .map(n => n[0])
     .join('')
     .toUpperCase()
     .slice(0, 2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getInitials = (name: string) =>
name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
const getInitials = (name: string) =>
name
.split(' ')
.filter(n => n.length > 0)
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2)

const visibleAuthors = computed(() => {
if (authors.length <= 3) return authors
return authors.slice(0, MAX_VISIBLE_AUTHORS)
})
const extraCount = computed(() => {
if (authors.length <= 3) return 0
return authors.length - MAX_VISIBLE_AUTHORS
})
const formattedAuthorNames = computed(() => {
const allNames = authors.map(a => a.name)
if (allNames.length === 0) return ''
if (allNames.length === 1) return allNames[0]
if (allNames.length === 2) return `${allNames[0]} and ${allNames[1]}`
if (allNames.length === 3) return `${allNames[0]}, ${allNames[1]}, and ${allNames[2]}`
const shown = allNames.slice(0, MAX_VISIBLE_AUTHORS)
const remaining = allNames.length - MAX_VISIBLE_AUTHORS
return `${shown.join(', ')} and ${remaining} others`
})
</script>

<template>
<OgLayout>
<div class="px-15 py-12 flex flex-col justify-center gap-5 h-full">
<OgBrand :height="48" />

<!-- Date + Title -->
<div class="flex flex-col gap-2">
<span v-if="formattedDate" class="text-3xl text-fg-muted">
{{ formattedDate }}
</span>

<div
class="lg:text-6xl text-5xl tracking-tighter font-mono leading-tight"
:style="{ lineClamp: 2, textOverflow: 'ellipsis' }"
>
{{ title }}
</div>
</div>

<!-- Authors -->
<div v-if="authors.length" class="flex items-center gap-4 flex-nowrap">
<!-- Stacked avatars -->
<span>
<span
v-for="(author, index) in visibleAuthors"
:key="author.name"
class="flex items-center justify-center rounded-full border border-bg bg-bg-muted overflow-hidden w-12 h-12"
:style="{ marginLeft: index > 0 ? '-20px' : '0' }"
>
<img
v-if="author.avatar"
:src="author.avatar"
:alt="author.name"
width="48"
height="48"
class="w-full h-full object-cover"
/>
<span v-else class="text-5 text-fg-muted font-medium">
{{ getInitials(author.name) }}
</span>
</span>
<!-- +N badge -->
<span
v-if="extraCount > 0"
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"
:style="{ marginLeft: '-20px' }"
>
+{{ extraCount }}
</span>
</span>
<!-- Names -->
<span class="text-6 text-fg-muted font-light">{{ formattedAuthorNames }}</span>
</div>
</div>
</OgLayout>
</template>
142 changes: 0 additions & 142 deletions app/components/OgImage/BlogPost.vue

This file was deleted.

Loading
Loading