Skip to content

fix(ui): support takumi v2 og images#2986

Draft
harlan-zw wants to merge 1 commit into
npmx-dev:mainfrom
harlan-zw:fix/og-image-takumi-v2
Draft

fix(ui): support takumi v2 og images#2986
harlan-zw wants to merge 1 commit into
npmx-dev:mainfrom
harlan-zw:fix/og-image-takumi-v2

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

N/A

🧭 Context

While upgrading nuxt-og-image and Takumi, a few OG image routes were visibly broken. The regressions showed up as shifted absolute overlays, black repo icons, and unstable package compare output.

Examples that drove this fix:

  • /package-code/takumi-js/v/1.8.7 had the nested file tree decoration misaligned.
  • Package OG cards such as /package/vue and /package/nuxt/v/4.3.1 had provider icons rendering black instead of the muted icon color.
  • /compare/vue,react,svelte could move between runs because the compare data was not fully fixture-backed.

📚 Description

  • Upgrades nuxt-og-image to 6.7.2 and Takumi packages to 2.0.0-rc.5.
  • Updates the OG layout and package template for Takumi v2 rendering, including static provider icon classes and explicit absolute offsets.
  • Adds fixture coverage for the broken takumi-js nested tree case and the compare-package snapshot, then refreshes the affected OG snapshots.

Verified with CI=1 pnpm install --frozen-lockfile, targeted vp lint, git diff --check, and pnpm test:types.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Jul 3, 2026 8:46am
npmx.dev Ready Ready Preview, Comment Jul 3, 2026 8:46am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Jul 3, 2026 8:46am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c71323d3-42cd-456c-a82d-7af8677c8ced

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates Open Graph image generation from nuxt-og-image's component-based defineOgImageComponent API to a new defineOgImage API backed by newly introduced *.takumi.vue components (Page, Splash, Profile, BlogPost, Compare, Package). Legacy components are removed, dependencies/config updated, and fixtures/tests refreshed.

Changes

Takumi OG image migration

Layer / File(s) Summary
Shared OG layout and brand primitives
app/components/OgLayout.vue, app/components/OgBrand.vue, app/composables/useCharts.ts
Adds a dark-themed layout wrapper, a logo-rendering brand component, and a smoothPath spline helper for chart rendering.
New Takumi OG image components
app/components/OgImage/Page.takumi.vue, Splash.takumi.vue, Profile.takumi.vue, BlogPost.takumi.vue, Compare.takumi.vue, Package.takumi.vue
Introduces new OG image templates replacing the removed component-based versions, including a large Package.takumi.vue with download-chart, code-tree, and function-tree variants, repository/star/likes/download stats, and sparkline generation.
Page-level defineOgImage wiring
app/app.vue, app/components/global/BlogPostWrapper.vue, app/pages/*.vue, .storybook/preview.ts
Replaces defineOgImageComponent calls across the app and all pages with defineOgImage calls targeting the new templates, dynamic titles/descriptions, and explicit alt text; updates the Storybook global stub name.
Build/runtime config and dependency updates
nuxt.config.ts, docs/nuxt.config.ts, package.json, pnpm-workspace.yaml, patches/@nuxt__test-utils.patch, public/robots.txt, uno.config.ts, modules/og-image.ts, .env.example
Reorders modules, removes OG-image routeRules and Twitter card meta, reconfigures ogImage caching/security, disables ogImage in docs config, bumps nuxt-og-image/adds @takumi-rs dependencies, updates workspace catalogs/patches, adjusts robots allow rule and UnoCSS pipeline exclusions, and removes the legacy component-pruning module.
Test fixtures, mocks, and spec updates
modules/runtime/server/cache.ts, test/fixtures/**, test/nuxt/components/OgImagePackage.spec.ts, test/unit/a11y-component-coverage.spec.ts, test/e2e/og-image.spec.ts, scripts/generate-fixtures.ts
Adds deterministic mock download-range data generation, new npm registry/downloads/jsdelivr fixtures, updates the Package OG image spec for the Takumi component, refreshes a11y skip list, and expands e2e OG image snapshot test cases.

Sequence Diagram(s)

sequenceDiagram
  participant Page as Page component
  participant DefineOgImage as defineOgImage
  participant NuxtOgImage as nuxt-og-image module
  participant Takumi as OgImage/*.takumi.vue
  participant DataSources as Package/Repo/Downloads data

  Page->>DefineOgImage: call defineOgImage('Template.takumi', props, { alt })
  DefineOgImage->>NuxtOgImage: register OG image request
  NuxtOgImage->>Takumi: render selected takumi component with props
  Takumi->>DataSources: fetch package/repo/downloads/likes (server-side)
  DataSources-->>Takumi: return metadata and stats
  Takumi-->>NuxtOgImage: rendered image output
  NuxtOgImage-->>Page: og:image meta tag
Loading

Possibly related PRs

  • npmx-dev/npmx.dev#2292: Implements the same Takumi OG image rollout, renaming the Storybook stub and introducing/removing the same set of OG image components.
  • npmx-dev/npmx.dev#2693: Modifies the same defineOgImage('Package.takumi', ...) configuration controlling og vs whatsapp image variants.
  • npmx-dev/npmx.dev#2717: Touches app/components/OgImage/Package.takumi.vue, changing how the repository provider icon is selected.

Suggested reviewers: ghostdevv, alexdln, danielroe

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: upgrading and adjusting OG image support for Takumi v2.
Description check ✅ Passed The description is directly aligned with the changeset, covering the Takumi/nuxt-og-image upgrade, layout updates, fixtures, and snapshot refreshes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​takumi-rs/​wasm@​2.0.0-rc.5891007396100
Added@​takumi-rs/​core@​2.0.0-rc.5791008496100

View full report

@socket-security

socket-security Bot commented Jul 3, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @emnapi/runtime is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@nuxt/a11y@1.0.0-alpha.1npm/@nuxt/fonts@0.14.0npm/@unocss/nuxt@66.6.7npm/unocss@66.6.7npm/@nuxtjs/i18n@10.2.4npm/tsdown@0.21.7npm/docus@5.9.0npm/@nuxt/ui@4.6.1npm/@storybook/addon-docs@10.3.5npm/@nuxt/scripts@1.0.1npm/@nuxt/test-utils@4.0.3npm/@voidzero-dev/vite-plus-test@0.1.20npm/vite-plugin-pwa@1.3.0npm/unplugin-vue-markdown@32.0.0npm/nuxt@4.4.8npm/nuxt-og-image@6.7.2npm/@emnapi/runtime@1.11.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@emnapi/runtime@1.11.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

📊 Dependency Size Changes

Warning

This PR adds 8.5 MB of new dependencies, which exceeds the threshold of 200 kB.

📦 Package 📏 Size
@napi-rs/wasm-runtime@1.1.6 6.3 MB
@emnapi/core@1.11.1 2 MB
@takumi-rs/core-linux-x64-gnu@1.0.9 → @takumi-rs/core-linux-x64-gnu@2.0.0-rc.5 -1 MB
@takumi-rs/wasm@1.0.9 → @takumi-rs/wasm@2.0.0-rc.5 -939.8 kB
@tybys/wasm-util@0.10.3 825.1 kB
@emnapi/runtime@1.11.1 433.5 kB
@takumi-rs/helpers@1.0.9 → @takumi-rs/helpers@2.0.0-rc.5 394.6 kB
@oxc-parser/binding-linux-x64-gnu@0.135.0 → @oxc-parser/binding-linux-x64-gnu@0.138.0 -241.7 kB
@emnapi/wasi-threads@1.2.2 225.1 kB
@unocss/core@66.7.4 87 kB
unplugin@3.3.0 79.9 kB
nuxtseo-shared@5.3.2 72.1 kB
exsolve@1.1.0 55.3 kB
nypm@0.6.8 51 kB
nuxt-site-config@4.1.1 47.9 kB
nuxt-og-image@6.6.0 → nuxt-og-image@6.7.2 28.8 kB
site-config-stack@4.1.1 27.4 kB
nuxt-site-config-kit@4.1.1 25.3 kB
@takumi-rs/core@1.0.9 → @takumi-rs/core@2.0.0-rc.5 25.1 kB
vue-component-type-helpers@3.3.6 5.6 kB
oxc-parser@0.135.0 → oxc-parser@0.138.0 494 B
@unocss/config@66.7.2 → @unocss/config@66.7.4 0 B
@oxc-project/types@0.135.0 → @oxc-project/types@0.138.0 0 B

Total size change: 8.5 MB

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nuxt.config.ts (1)

78-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore a site-wide twitterCard meta tag in nuxt.config.ts
defineOgImage only covers OG images, and there’s no other twitterCard/twitter:card declaration in the repo, so X previews would lose their card type without an explicit useSeoMeta({ twitterCard: 'summary_large_image' }) or equivalent.

🤖 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 `@nuxt.config.ts` at line 78, Restore a site-wide twitterCard meta declaration
in nuxt.config.ts, since defineOgImage only handles OG images and there is no
other twitter:card setting in the app. Add the missing SEO meta via the existing
Nuxt config SEO setup (for example with useSeoMeta) so every page continues to
advertise summary_large_image for X previews; locate the relevant global
metadata block near defineOgImage and the current site-wide head/meta
configuration.
🧹 Nitpick comments (6)
app/components/OgImage/BlogPost.takumi.vue (1)

48-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid unchecked author-name indexes.

allNames[0], [1], and [2] bypass the project’s strict indexed-access convention. Add explicit fallbacks before formatting.

Proposed fix
 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 [first = '', second = '', third = ''] = allNames
+  if (allNames.length === 1) return first
+  if (allNames.length === 2) return `${first} and ${second}`
+  if (allNames.length === 3) return `${first}, ${second}, and ${third}`
   const shown = allNames.slice(0, MAX_VISIBLE_AUTHORS)
   const remaining = allNames.length - MAX_VISIBLE_AUTHORS
   return `${shown.join(', ')} and ${remaining} others`
 })

As per coding guidelines, “Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index.”

🤖 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/components/OgImage/BlogPost.takumi.vue` around lines 48 - 56, The
formattedAuthorNames computed formatter is accessing allNames[0], allNames[1],
and allNames[2] directly, which violates the strict indexed-access convention.
Update the logic in BlogPost.takumi.vue to add explicit guards/fallbacks before
composing the 1-, 2-, and 3-author strings, using named checks against allNames
in the formattedAuthorNames function so each indexed value is only used after
confirming it exists.

Source: Coding guidelines

app/pages/search.vue (1)

555-591: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated title/description logic between useSeoMeta and defineOgImage.

The title and description closures passed to defineOgImage are identical to those already computed for useSeoMeta. Consider extracting shared computed refs to avoid the strings diverging later.

♻️ Suggested refactor
+const pageTitle = computed(
+  () =>
+    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
+)
+const pageDescription = computed(() =>
+  query.value
+    ? $t('search.meta_description', { search: query.value })
+    : $t('search.meta_description_packages'),
+)
+
 useSeoMeta({
-  title: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  ogTitle: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  twitterTitle: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  description: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
-  ogDescription: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
-  twitterDescription: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
+  title: pageTitle,
+  ogTitle: pageTitle,
+  twitterTitle: pageTitle,
+  description: pageDescription,
+  ogDescription: pageDescription,
+  twitterDescription: pageDescription,
 })

 defineOgImage(
   'Page.takumi',
   {
-    title: () =>
-      `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-    description: () =>
-      query.value
-        ? $t('search.meta_description', { search: query.value })
-        : $t('search.meta_description_packages'),
+    title: pageTitle,
+    description: pageDescription,
   },
   {
     alt: () =>
       query.value ? `Search results for "${query.value}" on npmx` : 'Search npm packages on npmx',
   },
 )
🤖 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/search.vue` around lines 555 - 591, The search page repeats the
same title and description logic in both useSeoMeta and defineOgImage, which can
drift over time. Extract the shared title and description into reusable computed
refs in search.vue, then reference those values from both useSeoMeta and
defineOgImage (including the alt text branch as needed) so the metadata stays
consistent in one place.
app/pages/pds.vue (1)

13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded, untranslated strings in defineOgImage.

title/description/alt are hardcoded English literals here, while the page's useSeoMeta fields above use $t(...). Consider wiring these through i18n for consistency with the rest of the page (and similarly for other pages in this PR using literal alt/title strings, e.g. package-code, package-docs, package/[[org]]/[name].vue).

Based on learnings, $t() should be relied on consistently in useSeoMeta and OG-image-defining composables across all pages rather than hardcoded strings.

🤖 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/pds.vue` around lines 13 - 20, The `defineOgImage` call in
`pds.vue` still uses hardcoded English `title`, `description`, and `alt` strings
instead of the page’s i18n flow. Update the `defineOgImage` arguments to use
`$t(...)`-backed translations like the surrounding `useSeoMeta` fields, and
apply the same pattern anywhere else in this PR that passes literal OG/title/alt
text (for example in `package-code`, `package-docs`, and
`package/[[org]]/[name].vue`) so the composables are consistent.

Source: Learnings

pnpm-workspace.yaml (1)

20-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the Storybook versions
storybook is still on ^10.3.1 while the other Storybook packages are on ^10.3.5. If there’s no reason to keep the core package behind, bump it to match.

🤖 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 `@pnpm-workspace.yaml` around lines 20 - 27, The Storybook package set is
version-skewed because the core storybook dependency is behind the other
Storybook addons and utilities. Update the storybook entry in the storybook
package group to match the same 10.3.5 range used by the other Storybook
packages, keeping the existing package names unchanged.
modules/runtime/server/cache.ts (1)

190-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicated download-curve mock logic across cache.ts and mock-routes.cjs.

This new branch and the equivalent one in test/fixtures/mock-routes.cjs (lines 184-210) both mock the npm /downloads/range endpoint but use different hashing/curve formulas (this one adds trend/wave-period/weekend-dip; the other is a simpler sine+noise curve). Any future adjustment to one will silently diverge from the other, causing inconsistent sparkline/chart shapes between component tests and e2e snapshot tests for the same package.

Consider extracting the shared curve-generation logic into a small utility consumable from both an ESM/TS context and the CJS mock file (e.g. a plain .mjs/.cjs helper with no TS-only syntax), or generating one from the other in a build step.

🤖 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 `@modules/runtime/server/cache.ts` around lines 190 - 234, The npm downloads
mock logic is duplicated and diverging between cache.ts and mock-routes.cjs,
which will produce inconsistent sparkline shapes. Extract the shared
`/downloads/range` curve generation from the cache branch in cache.ts into a
reusable helper that both the cache handler and the mock-routes.cjs fixture can
call, keeping the hashing, trend, wave, and weekend behavior identical in both
places.
test/e2e/og-image.spec.ts (1)

34-34: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Snapshot filename isn't sanitised for query-string characters.

For path: '/compare?packages=vue,react,svelte', the name transform only replaces / and strips a leading -, leaving ?, =, and , in the generated snapshot filename (og-image-compare?packages=vue,react,svelte.png). This is unusual for a snapshot artifact name and may cause portability issues with some tooling/filesystems.

💡 Proposed fix to sanitise the slug
-        name: `og-image-${path.replace(/\//g, '-').replace(/^-/, '') || 'home'}.png`,
+        name: `og-image-${
+          path.replace(/[/?]/g, '-').replace(/[=,]/g, '_').replace(/^-/, '') || 'home'
+        }.png`,

Also applies to: 66-67

🤖 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 `@test/e2e/og-image.spec.ts` at line 34, The snapshot name generation for the
OG image tests is only partially sanitizing the `path` value, so query-string
characters from entries like `compare` still leak into the filename. Update the
slug/name transform in `og-image.spec.ts` where the snapshot path is derived to
sanitize all unsafe characters from the `path` value, not just `/` and a leading
`-`, so the generated artifact name stays filesystem-friendly for the `compare`
cases and the other affected entries.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/components/OgImage/Package.takumi.vue`:
- Around line 180-187: The optional likes lookup in Package.takumi.vue can
reject and cause the outer Promise.all flow to fail, which incorrectly 404s the
OG image. Update the fetchLikes logic in the Package component so the $fetch
branch handles failures locally and falls back to totalLikes.value = 0, keeping
likes as non-blocking metadata while preserving the deterministic
import.meta.test path.

In `@app/pages/compare.vue`:
- Around line 145-152: The OG image alt text in defineOgImage is hard-wired to
the empty-state copy and does not reflect when packages.value has selected
items. Update the defineOgImage call in compare.vue so the alt callback mirrors
the same packages.value conditional used by useSeoMeta, returning
comparison-related text when packages are present and the empty-state
translation only when none are selected. Keep the logic aligned with
packages.value and the existing meta helpers to ensure the alt description
matches the rendered image.

In `@nuxt.config.ts`:
- Around line 302-314: The ogImage security config is overly relaxed in all
environments and reuses the image-proxy secret. Update the ogImage security
block in nuxt.config.ts so restrictRuntimeImagesToOrigin is only disabled for
test/e2e or other non-production contexts, while production keeps the stricter
default. Also replace the shared NUXT_IMAGE_PROXY_SECRET usage with a dedicated
OG-image secret in the ogImage.security.secret field so the two signing domains
remain separate.

In `@patches/`@nuxt__test-utils.patch:
- Around line 1-13: The cloning logic in getVitestConfigFromNuxt is dropping
unset runtimeConfig keys because JSON.parse(JSON.stringify(...)) removes
undefined properties. Replace that clone with a method that preserves undefined
fields when building nuxtRuntimeConfig, so auth-related keys like oauthJwkOne
remain present even when unset.

In `@test/fixtures/jsdelivr/vue.json`:
- Around line 5-6: The jsDelivr vue fixture has an invalid default entrypoint
because vue.global.min.js is not present in the mocked dist contents. Update the
default field in vue.json to reference one of the existing files under dist,
such as vue.global.js or vue.global.prod.js, so the fixture matches the
available mock assets.

---

Outside diff comments:
In `@nuxt.config.ts`:
- Line 78: Restore a site-wide twitterCard meta declaration in nuxt.config.ts,
since defineOgImage only handles OG images and there is no other twitter:card
setting in the app. Add the missing SEO meta via the existing Nuxt config SEO
setup (for example with useSeoMeta) so every page continues to advertise
summary_large_image for X previews; locate the relevant global metadata block
near defineOgImage and the current site-wide head/meta configuration.

---

Nitpick comments:
In `@app/components/OgImage/BlogPost.takumi.vue`:
- Around line 48-56: The formattedAuthorNames computed formatter is accessing
allNames[0], allNames[1], and allNames[2] directly, which violates the strict
indexed-access convention. Update the logic in BlogPost.takumi.vue to add
explicit guards/fallbacks before composing the 1-, 2-, and 3-author strings,
using named checks against allNames in the formattedAuthorNames function so each
indexed value is only used after confirming it exists.

In `@app/pages/pds.vue`:
- Around line 13-20: The `defineOgImage` call in `pds.vue` still uses hardcoded
English `title`, `description`, and `alt` strings instead of the page’s i18n
flow. Update the `defineOgImage` arguments to use `$t(...)`-backed translations
like the surrounding `useSeoMeta` fields, and apply the same pattern anywhere
else in this PR that passes literal OG/title/alt text (for example in
`package-code`, `package-docs`, and `package/[[org]]/[name].vue`) so the
composables are consistent.

In `@app/pages/search.vue`:
- Around line 555-591: The search page repeats the same title and description
logic in both useSeoMeta and defineOgImage, which can drift over time. Extract
the shared title and description into reusable computed refs in search.vue, then
reference those values from both useSeoMeta and defineOgImage (including the alt
text branch as needed) so the metadata stays consistent in one place.

In `@modules/runtime/server/cache.ts`:
- Around line 190-234: The npm downloads mock logic is duplicated and diverging
between cache.ts and mock-routes.cjs, which will produce inconsistent sparkline
shapes. Extract the shared `/downloads/range` curve generation from the cache
branch in cache.ts into a reusable helper that both the cache handler and the
mock-routes.cjs fixture can call, keeping the hashing, trend, wave, and weekend
behavior identical in both places.

In `@pnpm-workspace.yaml`:
- Around line 20-27: The Storybook package set is version-skewed because the
core storybook dependency is behind the other Storybook addons and utilities.
Update the storybook entry in the storybook package group to match the same
10.3.5 range used by the other Storybook packages, keeping the existing package
names unchanged.

In `@test/e2e/og-image.spec.ts`:
- Line 34: The snapshot name generation for the OG image tests is only partially
sanitizing the `path` value, so query-string characters from entries like
`compare` still leak into the filename. Update the slug/name transform in
`og-image.spec.ts` where the snapshot path is derived to sanitize all unsafe
characters from the `path` value, not just `/` and a leading `-`, so the
generated artifact name stays filesystem-friendly for the `compare` cases and
the other affected entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59831482-ee95-47a2-8248-bc6768eadbff

📥 Commits

Reviewing files that changed from the base of the PR and between 161697c and 07aa320.

⛔ Files ignored due to path filters (15)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • test/e2e/og-image.spec.ts-snapshots/og-image-accessibility.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-blog-alpha-release.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-compare-packages-vue-react-svelte.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-for--.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-for--package-nuxt-v-3-20-2.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-home.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--babel-plugin-transform-exponentiation-operator.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--nuxt-kit.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--tanstack-react-query.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-code-takumi-js-v-1-8-7.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-code-vue-v-3-5-27.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-docs-ufo-v-1-6-3.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-nuxt-v-4-3-1.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-vue.png is excluded by !**/*.png
📒 Files selected for processing (63)
  • .env.example
  • .storybook/preview.ts
  • app/app.vue
  • app/components/OgBrand.vue
  • app/components/OgImage/BlogPost.d.vue.ts
  • app/components/OgImage/BlogPost.takumi.vue
  • app/components/OgImage/BlogPost.vue
  • app/components/OgImage/Compare.takumi.vue
  • app/components/OgImage/Default.vue
  • app/components/OgImage/Package.d.vue.ts
  • app/components/OgImage/Package.takumi.vue
  • app/components/OgImage/Package.vue
  • app/components/OgImage/Page.takumi.vue
  • app/components/OgImage/Profile.takumi.vue
  • app/components/OgImage/Splash.takumi.vue
  • app/components/OgLayout.vue
  • app/components/global/BlogPostWrapper.vue
  • app/composables/useCharts.ts
  • app/pages/about.vue
  • app/pages/accessibility.vue
  • app/pages/brand.vue
  • app/pages/compare.vue
  • app/pages/index.vue
  • app/pages/org/[org].vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue
  • app/pages/package-docs/[...path].vue
  • app/pages/package/[[org]]/[name].vue
  • app/pages/pds.vue
  • app/pages/privacy.vue
  • app/pages/profile/[identity]/index.vue
  • app/pages/recharging.vue
  • app/pages/search.vue
  • app/pages/settings.vue
  • app/pages/translation-status.vue
  • app/pages/~[username]/index.vue
  • app/pages/~[username]/orgs.vue
  • docs/nuxt.config.ts
  • modules/og-image.ts
  • modules/runtime/server/cache.ts
  • nuxt.config.ts
  • package.json
  • patches/@nuxt__test-utils.patch
  • pnpm-workspace.yaml
  • public/robots.txt
  • scripts/generate-fixtures.ts
  • test/e2e/og-image.spec.ts
  • test/fixtures/jsdelivr/takumi-js.json
  • test/fixtures/jsdelivr/vue.json
  • test/fixtures/mock-routes.cjs
  • test/fixtures/npm-api/downloads/@anthropic-ai/claude-code.json
  • test/fixtures/npm-api/downloads/@babel/plugin-transform-exponentiation-operator.json
  • test/fixtures/npm-api/downloads/@tanstack/react-query.json
  • test/fixtures/npm-api/downloads/react.json
  • test/fixtures/npm-api/downloads/svelte.json
  • test/fixtures/npm-registry/packuments/@anthropic-ai/claude-code.json
  • test/fixtures/npm-registry/packuments/@babel/plugin-transform-exponentiation-operator.json
  • test/fixtures/npm-registry/packuments/@tanstack/react-query.json
  • test/fixtures/npm-registry/packuments/react.json
  • test/fixtures/npm-registry/packuments/svelte.json
  • test/fixtures/npm-registry/packuments/takumi-js.json
  • test/nuxt/components/OgImagePackage.spec.ts
  • test/unit/a11y-component-coverage.spec.ts
  • uno.config.ts
💤 Files with no reviewable changes (6)
  • app/components/OgImage/Package.d.vue.ts
  • app/components/OgImage/BlogPost.d.vue.ts
  • app/components/OgImage/Default.vue
  • app/components/OgImage/BlogPost.vue
  • app/components/OgImage/Package.vue
  • modules/og-image.ts

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nuxt.config.ts (1)

78-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore a site-wide twitterCard meta tag in nuxt.config.ts
defineOgImage only covers OG images, and there’s no other twitterCard/twitter:card declaration in the repo, so X previews would lose their card type without an explicit useSeoMeta({ twitterCard: 'summary_large_image' }) or equivalent.

🤖 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 `@nuxt.config.ts` at line 78, Restore a site-wide twitterCard meta declaration
in nuxt.config.ts, since defineOgImage only handles OG images and there is no
other twitter:card setting in the app. Add the missing SEO meta via the existing
Nuxt config SEO setup (for example with useSeoMeta) so every page continues to
advertise summary_large_image for X previews; locate the relevant global
metadata block near defineOgImage and the current site-wide head/meta
configuration.
🧹 Nitpick comments (6)
app/components/OgImage/BlogPost.takumi.vue (1)

48-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid unchecked author-name indexes.

allNames[0], [1], and [2] bypass the project’s strict indexed-access convention. Add explicit fallbacks before formatting.

Proposed fix
 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 [first = '', second = '', third = ''] = allNames
+  if (allNames.length === 1) return first
+  if (allNames.length === 2) return `${first} and ${second}`
+  if (allNames.length === 3) return `${first}, ${second}, and ${third}`
   const shown = allNames.slice(0, MAX_VISIBLE_AUTHORS)
   const remaining = allNames.length - MAX_VISIBLE_AUTHORS
   return `${shown.join(', ')} and ${remaining} others`
 })

As per coding guidelines, “Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index.”

🤖 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/components/OgImage/BlogPost.takumi.vue` around lines 48 - 56, The
formattedAuthorNames computed formatter is accessing allNames[0], allNames[1],
and allNames[2] directly, which violates the strict indexed-access convention.
Update the logic in BlogPost.takumi.vue to add explicit guards/fallbacks before
composing the 1-, 2-, and 3-author strings, using named checks against allNames
in the formattedAuthorNames function so each indexed value is only used after
confirming it exists.

Source: Coding guidelines

app/pages/search.vue (1)

555-591: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated title/description logic between useSeoMeta and defineOgImage.

The title and description closures passed to defineOgImage are identical to those already computed for useSeoMeta. Consider extracting shared computed refs to avoid the strings diverging later.

♻️ Suggested refactor
+const pageTitle = computed(
+  () =>
+    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
+)
+const pageDescription = computed(() =>
+  query.value
+    ? $t('search.meta_description', { search: query.value })
+    : $t('search.meta_description_packages'),
+)
+
 useSeoMeta({
-  title: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  ogTitle: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  twitterTitle: () =>
-    `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-  description: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
-  ogDescription: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
-  twitterDescription: () =>
-    query.value
-      ? $t('search.meta_description', { search: query.value })
-      : $t('search.meta_description_packages'),
+  title: pageTitle,
+  ogTitle: pageTitle,
+  twitterTitle: pageTitle,
+  description: pageDescription,
+  ogDescription: pageDescription,
+  twitterDescription: pageDescription,
 })

 defineOgImage(
   'Page.takumi',
   {
-    title: () =>
-      `${query.value ? $t('search.title_search', { search: query.value }) : $t('search.title_packages')} - npmx`,
-    description: () =>
-      query.value
-        ? $t('search.meta_description', { search: query.value })
-        : $t('search.meta_description_packages'),
+    title: pageTitle,
+    description: pageDescription,
   },
   {
     alt: () =>
       query.value ? `Search results for "${query.value}" on npmx` : 'Search npm packages on npmx',
   },
 )
🤖 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/search.vue` around lines 555 - 591, The search page repeats the
same title and description logic in both useSeoMeta and defineOgImage, which can
drift over time. Extract the shared title and description into reusable computed
refs in search.vue, then reference those values from both useSeoMeta and
defineOgImage (including the alt text branch as needed) so the metadata stays
consistent in one place.
app/pages/pds.vue (1)

13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded, untranslated strings in defineOgImage.

title/description/alt are hardcoded English literals here, while the page's useSeoMeta fields above use $t(...). Consider wiring these through i18n for consistency with the rest of the page (and similarly for other pages in this PR using literal alt/title strings, e.g. package-code, package-docs, package/[[org]]/[name].vue).

Based on learnings, $t() should be relied on consistently in useSeoMeta and OG-image-defining composables across all pages rather than hardcoded strings.

🤖 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/pds.vue` around lines 13 - 20, The `defineOgImage` call in
`pds.vue` still uses hardcoded English `title`, `description`, and `alt` strings
instead of the page’s i18n flow. Update the `defineOgImage` arguments to use
`$t(...)`-backed translations like the surrounding `useSeoMeta` fields, and
apply the same pattern anywhere else in this PR that passes literal OG/title/alt
text (for example in `package-code`, `package-docs`, and
`package/[[org]]/[name].vue`) so the composables are consistent.

Source: Learnings

pnpm-workspace.yaml (1)

20-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the Storybook versions
storybook is still on ^10.3.1 while the other Storybook packages are on ^10.3.5. If there’s no reason to keep the core package behind, bump it to match.

🤖 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 `@pnpm-workspace.yaml` around lines 20 - 27, The Storybook package set is
version-skewed because the core storybook dependency is behind the other
Storybook addons and utilities. Update the storybook entry in the storybook
package group to match the same 10.3.5 range used by the other Storybook
packages, keeping the existing package names unchanged.
modules/runtime/server/cache.ts (1)

190-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicated download-curve mock logic across cache.ts and mock-routes.cjs.

This new branch and the equivalent one in test/fixtures/mock-routes.cjs (lines 184-210) both mock the npm /downloads/range endpoint but use different hashing/curve formulas (this one adds trend/wave-period/weekend-dip; the other is a simpler sine+noise curve). Any future adjustment to one will silently diverge from the other, causing inconsistent sparkline/chart shapes between component tests and e2e snapshot tests for the same package.

Consider extracting the shared curve-generation logic into a small utility consumable from both an ESM/TS context and the CJS mock file (e.g. a plain .mjs/.cjs helper with no TS-only syntax), or generating one from the other in a build step.

🤖 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 `@modules/runtime/server/cache.ts` around lines 190 - 234, The npm downloads
mock logic is duplicated and diverging between cache.ts and mock-routes.cjs,
which will produce inconsistent sparkline shapes. Extract the shared
`/downloads/range` curve generation from the cache branch in cache.ts into a
reusable helper that both the cache handler and the mock-routes.cjs fixture can
call, keeping the hashing, trend, wave, and weekend behavior identical in both
places.
test/e2e/og-image.spec.ts (1)

34-34: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Snapshot filename isn't sanitised for query-string characters.

For path: '/compare?packages=vue,react,svelte', the name transform only replaces / and strips a leading -, leaving ?, =, and , in the generated snapshot filename (og-image-compare?packages=vue,react,svelte.png). This is unusual for a snapshot artifact name and may cause portability issues with some tooling/filesystems.

💡 Proposed fix to sanitise the slug
-        name: `og-image-${path.replace(/\//g, '-').replace(/^-/, '') || 'home'}.png`,
+        name: `og-image-${
+          path.replace(/[/?]/g, '-').replace(/[=,]/g, '_').replace(/^-/, '') || 'home'
+        }.png`,

Also applies to: 66-67

🤖 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 `@test/e2e/og-image.spec.ts` at line 34, The snapshot name generation for the
OG image tests is only partially sanitizing the `path` value, so query-string
characters from entries like `compare` still leak into the filename. Update the
slug/name transform in `og-image.spec.ts` where the snapshot path is derived to
sanitize all unsafe characters from the `path` value, not just `/` and a leading
`-`, so the generated artifact name stays filesystem-friendly for the `compare`
cases and the other affected entries.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/components/OgImage/Package.takumi.vue`:
- Around line 180-187: The optional likes lookup in Package.takumi.vue can
reject and cause the outer Promise.all flow to fail, which incorrectly 404s the
OG image. Update the fetchLikes logic in the Package component so the $fetch
branch handles failures locally and falls back to totalLikes.value = 0, keeping
likes as non-blocking metadata while preserving the deterministic
import.meta.test path.

In `@app/pages/compare.vue`:
- Around line 145-152: The OG image alt text in defineOgImage is hard-wired to
the empty-state copy and does not reflect when packages.value has selected
items. Update the defineOgImage call in compare.vue so the alt callback mirrors
the same packages.value conditional used by useSeoMeta, returning
comparison-related text when packages are present and the empty-state
translation only when none are selected. Keep the logic aligned with
packages.value and the existing meta helpers to ensure the alt description
matches the rendered image.

In `@nuxt.config.ts`:
- Around line 302-314: The ogImage security config is overly relaxed in all
environments and reuses the image-proxy secret. Update the ogImage security
block in nuxt.config.ts so restrictRuntimeImagesToOrigin is only disabled for
test/e2e or other non-production contexts, while production keeps the stricter
default. Also replace the shared NUXT_IMAGE_PROXY_SECRET usage with a dedicated
OG-image secret in the ogImage.security.secret field so the two signing domains
remain separate.

In `@patches/`@nuxt__test-utils.patch:
- Around line 1-13: The cloning logic in getVitestConfigFromNuxt is dropping
unset runtimeConfig keys because JSON.parse(JSON.stringify(...)) removes
undefined properties. Replace that clone with a method that preserves undefined
fields when building nuxtRuntimeConfig, so auth-related keys like oauthJwkOne
remain present even when unset.

In `@test/fixtures/jsdelivr/vue.json`:
- Around line 5-6: The jsDelivr vue fixture has an invalid default entrypoint
because vue.global.min.js is not present in the mocked dist contents. Update the
default field in vue.json to reference one of the existing files under dist,
such as vue.global.js or vue.global.prod.js, so the fixture matches the
available mock assets.

---

Outside diff comments:
In `@nuxt.config.ts`:
- Line 78: Restore a site-wide twitterCard meta declaration in nuxt.config.ts,
since defineOgImage only handles OG images and there is no other twitter:card
setting in the app. Add the missing SEO meta via the existing Nuxt config SEO
setup (for example with useSeoMeta) so every page continues to advertise
summary_large_image for X previews; locate the relevant global metadata block
near defineOgImage and the current site-wide head/meta configuration.

---

Nitpick comments:
In `@app/components/OgImage/BlogPost.takumi.vue`:
- Around line 48-56: The formattedAuthorNames computed formatter is accessing
allNames[0], allNames[1], and allNames[2] directly, which violates the strict
indexed-access convention. Update the logic in BlogPost.takumi.vue to add
explicit guards/fallbacks before composing the 1-, 2-, and 3-author strings,
using named checks against allNames in the formattedAuthorNames function so each
indexed value is only used after confirming it exists.

In `@app/pages/pds.vue`:
- Around line 13-20: The `defineOgImage` call in `pds.vue` still uses hardcoded
English `title`, `description`, and `alt` strings instead of the page’s i18n
flow. Update the `defineOgImage` arguments to use `$t(...)`-backed translations
like the surrounding `useSeoMeta` fields, and apply the same pattern anywhere
else in this PR that passes literal OG/title/alt text (for example in
`package-code`, `package-docs`, and `package/[[org]]/[name].vue`) so the
composables are consistent.

In `@app/pages/search.vue`:
- Around line 555-591: The search page repeats the same title and description
logic in both useSeoMeta and defineOgImage, which can drift over time. Extract
the shared title and description into reusable computed refs in search.vue, then
reference those values from both useSeoMeta and defineOgImage (including the alt
text branch as needed) so the metadata stays consistent in one place.

In `@modules/runtime/server/cache.ts`:
- Around line 190-234: The npm downloads mock logic is duplicated and diverging
between cache.ts and mock-routes.cjs, which will produce inconsistent sparkline
shapes. Extract the shared `/downloads/range` curve generation from the cache
branch in cache.ts into a reusable helper that both the cache handler and the
mock-routes.cjs fixture can call, keeping the hashing, trend, wave, and weekend
behavior identical in both places.

In `@pnpm-workspace.yaml`:
- Around line 20-27: The Storybook package set is version-skewed because the
core storybook dependency is behind the other Storybook addons and utilities.
Update the storybook entry in the storybook package group to match the same
10.3.5 range used by the other Storybook packages, keeping the existing package
names unchanged.

In `@test/e2e/og-image.spec.ts`:
- Line 34: The snapshot name generation for the OG image tests is only partially
sanitizing the `path` value, so query-string characters from entries like
`compare` still leak into the filename. Update the slug/name transform in
`og-image.spec.ts` where the snapshot path is derived to sanitize all unsafe
characters from the `path` value, not just `/` and a leading `-`, so the
generated artifact name stays filesystem-friendly for the `compare` cases and
the other affected entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59831482-ee95-47a2-8248-bc6768eadbff

📥 Commits

Reviewing files that changed from the base of the PR and between 161697c and 07aa320.

⛔ Files ignored due to path filters (15)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • test/e2e/og-image.spec.ts-snapshots/og-image-accessibility.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-blog-alpha-release.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-compare-packages-vue-react-svelte.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-for--.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-for--package-nuxt-v-3-20-2.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-home.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--babel-plugin-transform-exponentiation-operator.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--nuxt-kit.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package--tanstack-react-query.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-code-takumi-js-v-1-8-7.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-code-vue-v-3-5-27.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-docs-ufo-v-1-6-3.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-nuxt-v-4-3-1.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/og-image-package-vue.png is excluded by !**/*.png
📒 Files selected for processing (63)
  • .env.example
  • .storybook/preview.ts
  • app/app.vue
  • app/components/OgBrand.vue
  • app/components/OgImage/BlogPost.d.vue.ts
  • app/components/OgImage/BlogPost.takumi.vue
  • app/components/OgImage/BlogPost.vue
  • app/components/OgImage/Compare.takumi.vue
  • app/components/OgImage/Default.vue
  • app/components/OgImage/Package.d.vue.ts
  • app/components/OgImage/Package.takumi.vue
  • app/components/OgImage/Package.vue
  • app/components/OgImage/Page.takumi.vue
  • app/components/OgImage/Profile.takumi.vue
  • app/components/OgImage/Splash.takumi.vue
  • app/components/OgLayout.vue
  • app/components/global/BlogPostWrapper.vue
  • app/composables/useCharts.ts
  • app/pages/about.vue
  • app/pages/accessibility.vue
  • app/pages/brand.vue
  • app/pages/compare.vue
  • app/pages/index.vue
  • app/pages/org/[org].vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue
  • app/pages/package-docs/[...path].vue
  • app/pages/package/[[org]]/[name].vue
  • app/pages/pds.vue
  • app/pages/privacy.vue
  • app/pages/profile/[identity]/index.vue
  • app/pages/recharging.vue
  • app/pages/search.vue
  • app/pages/settings.vue
  • app/pages/translation-status.vue
  • app/pages/~[username]/index.vue
  • app/pages/~[username]/orgs.vue
  • docs/nuxt.config.ts
  • modules/og-image.ts
  • modules/runtime/server/cache.ts
  • nuxt.config.ts
  • package.json
  • patches/@nuxt__test-utils.patch
  • pnpm-workspace.yaml
  • public/robots.txt
  • scripts/generate-fixtures.ts
  • test/e2e/og-image.spec.ts
  • test/fixtures/jsdelivr/takumi-js.json
  • test/fixtures/jsdelivr/vue.json
  • test/fixtures/mock-routes.cjs
  • test/fixtures/npm-api/downloads/@anthropic-ai/claude-code.json
  • test/fixtures/npm-api/downloads/@babel/plugin-transform-exponentiation-operator.json
  • test/fixtures/npm-api/downloads/@tanstack/react-query.json
  • test/fixtures/npm-api/downloads/react.json
  • test/fixtures/npm-api/downloads/svelte.json
  • test/fixtures/npm-registry/packuments/@anthropic-ai/claude-code.json
  • test/fixtures/npm-registry/packuments/@babel/plugin-transform-exponentiation-operator.json
  • test/fixtures/npm-registry/packuments/@tanstack/react-query.json
  • test/fixtures/npm-registry/packuments/react.json
  • test/fixtures/npm-registry/packuments/svelte.json
  • test/fixtures/npm-registry/packuments/takumi-js.json
  • test/nuxt/components/OgImagePackage.spec.ts
  • test/unit/a11y-component-coverage.spec.ts
  • uno.config.ts
💤 Files with no reviewable changes (6)
  • app/components/OgImage/Package.d.vue.ts
  • app/components/OgImage/BlogPost.d.vue.ts
  • app/components/OgImage/Default.vue
  • app/components/OgImage/BlogPost.vue
  • app/components/OgImage/Package.vue
  • modules/og-image.ts
🛑 Comments failed to post (5)
app/components/OgImage/Package.takumi.vue (1)

180-187: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t let optional likes failures 404 the OG image.

The non-test $fetch('/api/social/likes/...') branch can reject, and the outer Promise.all catch turns that into a 404. Treat likes like optional metadata and fall back to 0.

Proposed fix
 const fetchLikes = import.meta.test
   ? // need deterministic likes for testing
     Promise.resolve().then(() => {
       totalLikes.value = 83
     })
-  : $fetch<{ totalLikes: number }>(`/api/social/likes/${name}`).then(d => {
+  : $fetch<{ totalLikes: number }>(`/api/social/likes/${name}`).then(d => {
       totalLikes.value = d?.totalLikes ?? 0
+    }).catch(() => {
+      totalLikes.value = 0
     })
📝 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.

const fetchLikes = import.meta.test
  ? // need deterministic likes for testing
    Promise.resolve().then(() => {
      totalLikes.value = 83
    })
  : $fetch<{ totalLikes: number }>(`/api/social/likes/${name}`).then(d => {
      totalLikes.value = d?.totalLikes ?? 0
    }).catch(() => {
      totalLikes.value = 0
    })
🤖 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/components/OgImage/Package.takumi.vue` around lines 180 - 187, The
optional likes lookup in Package.takumi.vue can reject and cause the outer
Promise.all flow to fail, which incorrectly 404s the OG image. Update the
fetchLikes logic in the Package component so the $fetch branch handles failures
locally and falls back to totalLikes.value = 0, keeping likes as non-blocking
metadata while preserving the deterministic import.meta.test path.
app/pages/compare.vue (1)

145-152: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

OG image alt doesn't reflect selected packages.

The alt is hard-wired to the empty-state translation regardless of packages.value, while useSeoMeta below (Lines 238-263) conditionally switches title/description based on packages.value.length > 0. When packages are selected, the image will show a real comparison but the alt text will still say something like "Compare npm packages side-by-side," misdescribing the actual content for screen-reader users.

♿️ Proposed fix
 defineOgImage(
   'Compare.takumi',
   {
     packages: () => packages.value.toSorted((a, b) => a.localeCompare(b)),
     emptyDescription: () => $t('compare.packages.meta_description_empty'),
   },
-  { alt: () => $t('compare.packages.meta_description_empty') },
+  {
+    alt: () =>
+      packages.value.length > 0
+        ? $t('compare.packages.meta_description', { packages: packages.value.join(', ') })
+        : $t('compare.packages.meta_description_empty'),
+  },
 )
📝 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.

defineOgImage(
  'Compare.takumi',
  {
    packages: () => packages.value.toSorted((a, b) => a.localeCompare(b)),
    emptyDescription: () => $t('compare.packages.meta_description_empty'),
  },
  {
    alt: () =>
      packages.value.length > 0
        ? $t('compare.packages.meta_description', { packages: packages.value.join(', ') })
        : $t('compare.packages.meta_description_empty'),
  },
)
🤖 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/compare.vue` around lines 145 - 152, The OG image alt text in
defineOgImage is hard-wired to the empty-state copy and does not reflect when
packages.value has selected items. Update the defineOgImage call in compare.vue
so the alt callback mirrors the same packages.value conditional used by
useSeoMeta, returning comparison-related text when packages are present and the
empty-state translation only when none are selected. Keep the logic aligned with
packages.value and the existing meta helpers to ensure the alt description
matches the rendered image.
nuxt.config.ts (1)

302-314: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- nuxt.config.ts around target lines ---\n'
sed -n '288,328p' nuxt.config.ts | cat -n

printf '\n--- search for NUXT_IMAGE_PROXY_SECRET ---\n'
rg -n "NUXT_IMAGE_PROXY_SECRET|restrictRuntimeImagesToOrigin|ogImage:" -S . || true

Repository: npmx-dev/npmx.dev

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- nuxt.config.ts around target lines ---'
sed -n '288,328p' nuxt.config.ts | cat -n

echo
echo '--- search for NUXT_IMAGE_PROXY_SECRET / restrictRuntimeImagesToOrigin / ogImage ---'
rg -n "NUXT_IMAGE_PROXY_SECRET|restrictRuntimeImagesToOrigin|ogImage:" -S . || true

Repository: npmx-dev/npmx.dev

Length of output: 2587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- nuxt.config.ts ogImage block ---'
sed -n '330,355p' nuxt.config.ts | cat -n

echo
echo '--- modules/image-proxy.ts ---'
sed -n '1,120p' modules/image-proxy.ts | cat -n

echo
echo '--- docs/nuxt.config.ts ogImage block ---'
sed -n '1,40p' docs/nuxt.config.ts | cat -n

Repository: npmx-dev/npmx.dev

Length of output: 3332


Limit the OG-image security relaxation to non-production
restrictRuntimeImagesToOrigin: false disables origin pinning everywhere, including production. Gate that override to test/e2e only so release builds keep the stricter default, and use a dedicated OG-image secret instead of reusing NUXT_IMAGE_PROXY_SECRET to keep the two signing domains separate.

🤖 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 `@nuxt.config.ts` around lines 302 - 314, The ogImage security config is overly
relaxed in all environments and reuses the image-proxy secret. Update the
ogImage security block in nuxt.config.ts so restrictRuntimeImagesToOrigin is
only disabled for test/e2e or other non-production contexts, while production
keeps the stricter default. Also replace the shared NUXT_IMAGE_PROXY_SECRET
usage with a dedicated OG-image secret in the ogImage.security.secret field so
the two signing domains remain separate.
patches/@nuxt__test-utils.patch (1)

1-13: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect runtimeConfig shape to check for non-JSON-safe values
rg -n "runtimeConfig" nuxt.config.ts -A5

Repository: npmx-dev/npmx.dev

Length of output: 284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full runtimeConfig block and any obvious non-JSON-safe usages nearby.
sed -n '34,70p' nuxt.config.ts

printf '\n---\n'

# Search for runtimeConfig declarations/usages that might introduce Dates, Maps, Sets, functions, or circular refs.
rg -n "runtimeConfig|structuredClone|JSON\.parse|JSON\.stringify|Date\(|new Map|new Set|BigInt|Symbol|function\(" -S . \
  --glob '!dist/**' --glob '!node_modules/**'

Repository: npmx-dev/npmx.dev

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant patch hunk and the source of runtimeConfig values.
git diff -- patches/@nuxt__test-utils.patch
printf '\n--- nuxt.config.ts ---\n'
sed -n '34,70p' nuxt.config.ts

Repository: npmx-dev/npmx.dev

Length of output: 1175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find direct uses of the runtimeConfig keys.
rg -n "oauthJwkOne|sessionPassword|imageProxySecret|upstash|orgToken|runtimeConfig\." -S . \
  --glob '!dist/**' --glob '!node_modules/**' --glob '!patches/**'

Repository: npmx-dev/npmx.dev

Length of output: 19649


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look specifically for reads of the missing field and any presence checks on runtimeConfig.
rg -n "oauthJwkOne|hasOwnProperty\(['\"]oauthJwkOne['\"]\)|['\"]oauthJwkOne['\"] in|runtimeConfig\.(sessionPassword|imageProxySecret|github|oauthJwkOne|upstash|public)" -S . \
  --glob '!dist/**' --glob '!node_modules/**' --glob '!patches/**'

Repository: npmx-dev/npmx.dev

Length of output: 594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const runtimeConfig = {
  sessionPassword: '',
  imageProxySecret: '',
  github: { orgToken: '' },
  oauthJwkOne: undefined,
  upstash: { redisRestUrl: '', redisRestToken: '' },
  public: { algolia: { appId: 'OFCNCOG2CU', apiKey: 'x', indexName: 'npm-search' } },
}

const cloned = JSON.parse(JSON.stringify(runtimeConfig))
const structured = structuredClone(runtimeConfig)

console.log('original keys:', Object.keys(runtimeConfig).join(', '))
console.log('json keys    :', Object.keys(cloned).join(', '))
console.log('structured   :', Object.keys(structured).join(', '))
console.log('has oauthJwkOne original/json/structured:',
  Object.prototype.hasOwnProperty.call(runtimeConfig, 'oauthJwkOne'),
  Object.prototype.hasOwnProperty.call(cloned, 'oauthJwkOne'),
  Object.prototype.hasOwnProperty.call(structured, 'oauthJwkOne'),
)
JS

Repository: npmx-dev/npmx.dev

Length of output: 461


Preserve unset runtimeConfig fields JSON.parse(JSON.stringify(...)) drops oauthJwkOne when it is unset, and that key is read by the auth code. Use a clone method that keeps undefined properties.

🤖 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 `@patches/`@nuxt__test-utils.patch around lines 1 - 13, The cloning logic in
getVitestConfigFromNuxt is dropping unset runtimeConfig keys because
JSON.parse(JSON.stringify(...)) removes undefined properties. Replace that clone
with a method that preserves undefined fields when building nuxtRuntimeConfig,
so auth-related keys like oauthJwkOne remain present even when unset.
test/fixtures/jsdelivr/vue.json (1)

5-6: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'File contents:\n'
cat -n test/fixtures/jsdelivr/vue.json | sed -n '1,120p'

printf '\nSearch for referenced default path in fixture tree:\n'
rg -n 'vue\.global\.min\.js|/dist/vue\.global\.min\.js' test/fixtures/jsdelivr -S || true

printf '\nList nearby fixture files:\n'
find test/fixtures/jsdelivr -maxdepth 2 -type f | sort | sed -n '1,200p'

Repository: npmx-dev/npmx.dev

Length of output: 4635


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n test/fixtures/jsdelivr/vue.json | sed -n '1,120p'

Repository: npmx-dev/npmx.dev

Length of output: 4308


Use an existing file for the default entrypoint (test/fixtures/jsdelivr/vue.json:5).
default points to /dist/vue.global.min.js, but the fixture only contains vue.global.js and vue.global.prod.js under dist. Update it to a file that exists in the mock.

🤖 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 `@test/fixtures/jsdelivr/vue.json` around lines 5 - 6, The jsDelivr vue fixture
has an invalid default entrypoint because vue.global.min.js is not present in
the mocked dist contents. Update the default field in vue.json to reference one
of the existing files under dist, such as vue.global.js or vue.global.prod.js,
so the fixture matches the available mock assets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant