Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 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
52ed139
Merge upstream/main into feat/og-image-v7
harlan-zw Mar 27, 2026
c02a6a8
fix: convert remaining pages to defineOgImage API and sync with upstream
harlan-zw Mar 27, 2026
c3687a1
fix: remove unused i18n key and disable long-title validation
harlan-zw Mar 27, 2026
0908bb7
chore: bump nuxt-og-image to 6.2.6, takumi to 1.0.0-beta.20
harlan-zw Mar 27, 2026
295722d
chore: bump deps, fix test build prerender
harlan-zw Mar 27, 2026
a09e45c
chore: clean up PR scope, revert unrelated changes
harlan-zw Mar 27, 2026
95b7ff2
fix: blog OG avatar stacking, remove htmlValidator regexes, add ogIma…
harlan-zw Mar 27, 2026
327c7b8
fix: patch @nuxt/test-utils structuredClone crash with nuxt-og-image v6
harlan-zw Mar 27, 2026
ebe0a5b
fix: remove @nuxt/test-utils patch, fix upstream in nuxt-site-config
harlan-zw Mar 27, 2026
4d97433
fix: patch @nuxt/test-utils structuredClone for cross-context objects
harlan-zw Mar 27, 2026
fe4d826
Merge branch 'main' into feat/og-image-v7
danielroe Mar 27, 2026
8e5041e
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 27, 2026
0879add
Merge remote-tracking branch 'origin/main' into feat/og-image-v7
harlan-zw Mar 28, 2026
515f99b
chore: reduce PR scope, revert unrelated i18n and branding changes
harlan-zw Mar 28, 2026
3ebfe93
Merge remote-tracking branch 'origin/main' into feat/og-image-v7
harlan-zw Apr 10, 2026
3e65f8b
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 10, 2026
8f545f7
refactor(og-image): convert Compare to takumi
harlan-zw Apr 10, 2026
809c97a
chore(og-image): upgrade to nuxt-og-image 6.3.4 + takumi rc.17
harlan-zw Apr 10, 2026
37b2883
fix(ci): conditional ogImage strict mode + restore lunaria format
harlan-zw Apr 10, 2026
bd59666
fix(html-validator): skip /_og/ prerender routes
harlan-zw Apr 10, 2026
9a795dd
test(og-image): add compare snapshot + disable origin restriction
harlan-zw Apr 10, 2026
b4f2fff
fix(og-image): bump to nuxt-og-image 6.3.5, add compare snapshot
harlan-zw Apr 10, 2026
a5327ce
fix(og-image): bump 6.3.5 + skip OG prerender
harlan-zw Apr 10, 2026
72665b2
fix(og-image): bump to 6.3.6, remove workarounds
harlan-zw Apr 10, 2026
f3ebfd6
fix(html-validator): disable long-title rule for OG image HTML
harlan-zw Apr 10, 2026
bdedac2
fix(og-image): bump to 6.3.7, clean config
harlan-zw Apr 10, 2026
434ed43
fix(docs): bump docus to 5.9.0 for og-image v6 compat
harlan-zw Apr 10, 2026
a65b654
Revert "fix(docs): bump docus to 5.9.0 for og-image v6 compat"
harlan-zw Apr 10, 2026
5f17b6d
fix(docs): disable og-image in docs app
harlan-zw Apr 10, 2026
7821938
chore: progress
harlan-zw Apr 10, 2026
499fc7e
chore: progress
harlan-zw Apr 11, 2026
662ba9b
Merge branch 'main' into feat/og-image-v7
harlan-zw Apr 11, 2026
dc5162a
fix(og-image): update test to derive downloads from weeklyValues
harlan-zw Apr 11, 2026
224e32f
fix(og-image): fix $fetch mock type error in OgImagePackage test
harlan-zw Apr 11, 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#secure password, can use openssl rand -hex 32
NUXT_SESSION_PASSWORD=""

#HMAC secret for image proxy URL signing, can use openssl rand -hex 32
#HMAC secret for image-proxy and OG image URL signing, can use openssl rand -hex 32
NUXT_IMAGE_PROXY_SECRET=""
2 changes: 1 addition & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ globalThis['__NUXT_COLOR_MODE__'] ??= {
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()
globalThis.defineOgImage = fn()

// Subscribe to locale changes from storybook-i18n addon (once, outside decorator)
let currentI18nInstance: any = null
Expand Down
4 changes: 4 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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` }"
/>
</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
}
})
Comment on lines +14 to +25
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine the relevant lines
find . -name "BlogPost.takumi.vue" -type f

Repository: npmx-dev/npmx.dev

Length of output: 105


🏁 Script executed:

# Verify JavaScript Date behavior with invalid dates
node <<'EOF'
// Test 1: Does invalid Date throw when calling toLocaleDateString?
try {
  const invalidDate = new Date('invalid-date-string');
  const result = invalidDate.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  });
  console.log('Test 1 - Invalid date toLocaleDateString result:', result);
  console.log('Test 1 - Did NOT throw');
} catch (e) {
  console.log('Test 1 - Threw error:', e.message);
}

// Test 2: Check if getTime() returns NaN for invalid date
const invalidDate = new Date('invalid-date-string');
console.log('Test 2 - getTime() for invalid date:', invalidDate.getTime());
console.log('Test 2 - Number.isNaN(getTime()):', Number.isNaN(invalidDate.getTime()));

// Test 3: Check timezone effect
const testDate = new Date('2024-01-15T00:00:00Z');
const localResult = testDate.toLocaleDateString('en-US', {
  year: 'numeric',
  month: 'short',
  day: 'numeric',
});
const utcResult = testDate.toLocaleDateString('en-US', {
  year: 'numeric',
  month: 'short',
  day: 'numeric',
  timeZone: 'UTC',
});
console.log('Test 3 - Without timeZone: UTC:', localResult);
console.log('Test 3 - With timeZone: UTC:', utcResult);
console.log('Test 3 - Are they same?', localResult === utcResult);
EOF

Repository: npmx-dev/npmx.dev

Length of output: 341


🏁 Script executed:

cat -n ./app/components/OgImage/BlogPost.takumi.vue | head -40

Repository: npmx-dev/npmx.dev

Length of output: 1087


🏁 Script executed:

# Check imports at the top of the file
cat -n ./app/components/OgImage/BlogPost.takumi.vue | head -15

Repository: npmx-dev/npmx.dev

Length of output: 443


🏁 Script executed:

# Check if computed is imported in this file
rg "import.*computed" ./app/components/OgImage/BlogPost.takumi.vue

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Read the entire file to check for imports and overall structure
cat ./app/components/OgImage/BlogPost.takumi.vue

Repository: npmx-dev/npmx.dev

Length of output: 3322


Add missing import for computed, and fix invalid date handling with UTC consistency.

The computed function is used but not imported from 'vue', so this code will not work. Additionally, line 17 will not throw for an invalid Date, so the catch block never executes and this can render Invalid Date. For server-side OG image rendering, omitting timeZone: 'UTC' can also shift the calendar day across environments.

Suggested change
 <script setup lang="ts">
+import { computed } from 'vue'
 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 parsed = new Date(date)
+  if (Number.isNaN(parsed.getTime())) return date
+
+  return parsed.toLocaleDateString('en-US', {
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric',
+    timeZone: 'UTC',
+  })
 })
📝 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 formattedDate = computed(() => {
if (!date) return ''
try {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
return date
}
})
<script setup lang="ts">
import { computed } from 'vue'
import type { ResolvedAuthor } from '#shared/schemas/blog'
const {
title,
authors = [],
date = '',
} = defineProps<{
title: string
authors?: ResolvedAuthor[]
date?: string
}>()
const formattedDate = computed(() => {
if (!date) return ''
const parsed = new Date(date)
if (Number.isNaN(parsed.getTime())) return date
return parsed.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
})


const MAX_VISIBLE_AUTHORS = 2

const getInitials = (name: string) =>
name
.split(' ')
.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 class="flex flex-row items-center">
<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