Skip to content

feat: add share card#2164

Open
ShroXd wants to merge 50 commits intonpmx-dev:mainfrom
ShroXd:feat/share-card
Open

feat: add share card#2164
ShroXd wants to merge 50 commits intonpmx-dev:mainfrom
ShroXd:feat/share-card

Conversation

@ShroXd
Copy link
Copy Markdown
Contributor

@ShroXd ShroXd commented Mar 20, 2026

🔗 Linked issue

Resolves #2146

🧭 Context

Add a share button to help user generate a well-designed, shareable card and post it on social media or send it to friends.

📚 Description

Some implementation details need to be mentioned here:

  1. Share card is generated via nuxt-og-image, inline styles and hard code icon are used to ensure rendering compatibility
  2. Colors are pre-calculated to avoid on-the-fly computation. Since ACCENT_COLORS is widely used, a temporary ACCENT_COLOR_TOKENS is introduced in this PR — refactoring will be completed once merged
  3. Weekly download data on the share card is fetched directly from npm, so it may not strictly match the data shown on the package page

Demo

PixPin_2026-03-29_23-08-00.mp4

ALT of nuxt

nuxt 4.4.2 (latest) — 1.4M weekly downloads — MIT license — via npmx.dev

Screenshot

Scenario Screenshot
Light mode image
Dark mode image

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 12, 2026 7:10am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 12, 2026 7:10am
npmx-lunaria Ignored Ignored Apr 12, 2026 7:10am

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 55.55556% with 28 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Package/ShareModal.vue 65.30% 14 Missing and 3 partials ⚠️
app/utils/colors.ts 0.00% 8 Missing and 2 partials ⚠️
app/components/Package/Header.vue 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ShroXd ShroXd changed the title (WIP) feat: add share card feat: add share card Mar 29, 2026
@ShroXd ShroXd marked this pull request as ready for review March 30, 2026 01:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0cf8d391-e4bb-4702-8795-274471d9ddf8

📥 Commits

Reviewing files that changed from the base of the PR and between e67f886 and 0d8808d.

📒 Files selected for processing (12)
  • app/components/Package/Header.vue
  • app/components/Package/ShareModal.vue
  • app/pages/package/[[org]]/[name].vue
  • app/utils/colors.ts
  • app/utils/formatters.ts
  • app/utils/string.ts
  • i18n/locales/en.json
  • i18n/schema.json
  • modules/security-headers.ts
  • nuxt.config.ts
  • server/api/card/[...pkg].get.ts
  • test/unit/a11y-component-coverage.spec.ts
✅ Files skipped from review due to trivial changes (5)
  • test/unit/a11y-component-coverage.spec.ts
  • i18n/schema.json
  • i18n/locales/en.json
  • app/components/Package/Header.vue
  • app/components/Package/ShareModal.vue
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/utils/formatters.ts
  • app/utils/colors.ts
  • app/utils/string.ts

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added share functionality for package cards with light and dark theme options
    • New share button in package headers
    • Ability to download package cards as PNG images
    • Copy sharing link and alternative text to clipboard
    • Customisable accent colours for package cards
  • Documentation

    • Added translations for share functionality
  • Tests

    • Added end-to-end tests for share card rendering

Walkthrough

Adds a shareable OpenGraph package card system: new ShareCard OG component and types, OG image page and API redirect, package ShareModal and header button, ISR routeRules, utilities (formatDate, withAlpha, truncate), accent/theme constants, tests, and minor date formatting refactor in BlogPost component.

Changes

Cohort / File(s) Summary
OG image component & types
app/components/OgImage/ShareCard.vue, app/components/OgImage/ShareCard.d.vue.ts
New ShareCard Vue component for package OG images; added TypeScript declaration to avoid circular type resolution.
Package UI: header, modal, skeleton
app/components/Package/Header.vue, app/components/Package/ShareModal.vue, app/components/Package/Skeleton.vue
Added share button and modal, modal supports image preloading, copy/download actions; skeleton placeholder added for share button.
Pages & OG rendering entry
app/pages/share-card/[[org]]/[name].vue, app/pages/package/[[org]]/[name].vue
New share-card page registering OG component and redirecting to package page; package page now computes primaryColor from user-selected accent.
Server redirect API
server/api/card/[...pkg].get.ts
New API endpoint that validates package name, normalises PNG suffix, validates theme/color query, and 302-redirects to the internal OG-image URL with allowed query params.
Utilities & constants
app/utils/formatters.ts, app/utils/colors.ts, app/utils/string.ts, shared/utils/constants.ts
Added formatDate helper (used by BlogPost), withAlpha color helper, truncate string util, and ACCENT_COLOR_TOKENS/SHARE_CARD_THEMES constants and types.
Nuxt config & security
nuxt.config.ts, modules/security-headers.ts
RouteRules updated for ISR on card and OG-image routes with explicit allowlist for theme and color; CSP now conditionally includes upgrade-insecure-requests only outside dev.
Tests & i18n
test/e2e/og-image.spec.ts, test/unit/a11y-component-coverage.spec.ts, i18n/locales/en.json, i18n/schema.json
Added e2e snapshot tests for share card, skipped OG image and modal in a11y coverage, and added localization keys/schema entries for share actions.
Minor refactor
app/components/OgImage/BlogPost.vue
Replaced inline date parsing with call to shared formatDate helper.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser as Browser
  participant API as /api/card/... (redirect)
  participant OG as /__og-image__/image/... (OG renderer)
  participant Share as ShareCard component (server)
  participant Registry as Package metadata APIs

  Browser->>API: GET /api/card/nuxt.png?theme=dark&color=sky
  API->>API: validate package, theme, color\nbuild OG-image URL
  API-->>Browser: 302 Redirect -> /__og-image__/image/share-card/nuxt/og.png?...
  Browser->>OG: GET redirected OG-image URL
  OG->>Share: render ShareCard with props (name, theme, color)
  Share->>Registry: fetch package metadata, downloads, repo meta
  Registry-->>Share: package data
  Share-->>OG: return rendered image bytes (PNG)
  OG-->>Browser: 200 image/png
Loading

Possibly related PRs

Suggested reviewers

  • danielroe
  • alexdln
  • graphieros
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add share card' directly and clearly describes the main feature addition, following conventional commits format and matching the PR's primary objective.
Description check ✅ Passed The description explains the feature context, implementation details (nuxt-og-image usage, colour pre-calculation, npm data fetching), provides a demo URL, includes ALT text example, and references the resolved issue #2146.
Linked Issues check ✅ Passed The PR successfully implements all primary objectives from issue #2146: adds a share button to the package page, generates a shareable card via nuxt-og-image, supports configurable themes and colours, and follows the design mockup.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the share card feature. Minor supporting changes (security headers, colour utilities) are necessary dependencies for the feature implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
test/unit/a11y-component-coverage.spec.ts (1)

31-34: Avoid long-term a11y skip for Package/ShareModal.vue.

Line 32 skips an interactive modal in a core user flow; that can mask regressions. Consider keeping this as a short-lived skip with a tracked follow-up to add at least one focused modal a11y smoke test.

app/components/Package/ShareModal.vue (2)

68-79: The async keyword is unnecessary here.

downloadCard is declared async but contains no await expressions. The try/finally block handles synchronous operations only.

♻️ Proposed simplification
-async function downloadCard() {
+function downloadCard() {
   const a = document.createElement('a')
   a.href = cardUrl.value
   a.download = `${props.packageName.replace('/', '-')}-card.png`
   document.body.appendChild(a)
   try {
     a.click()
   } finally {
     document.body.removeChild(a)
   }
   showAlt.value = true
 }

31-33: Alt text may be redundant for non-latest versions.

When isLatest is false, the tag falls back to resolvedVersion, producing alt text like "nuxt 4.4.2 (4.4.2)" which duplicates the version. Consider omitting the parenthetical for non-latest versions, or showing the actual dist-tag if available.

♻️ Proposed improvement
 const altText = computed(() => {
-  const tag = props.isLatest ? 'latest' : props.resolvedVersion
-  const parts: string[] = [`${props.packageName} ${props.resolvedVersion} (${tag})`]
+  const versionPart = props.isLatest
+    ? `${props.resolvedVersion} (latest)`
+    : props.resolvedVersion
+  const parts: string[] = [`${props.packageName} ${versionPart}`]

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56dab79f-1b89-43a9-acfd-ce896de2de51

📥 Commits

Reviewing files that changed from the base of the PR and between 7688cd7 and 6040dfb.

⛔ Files ignored due to path filters (3)
  • 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/share-card-nuxt-dark.png is excluded by !**/*.png
  • test/e2e/og-image.spec.ts-snapshots/share-card-nuxt-light.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • app/components/OgImage/BlogPost.vue
  • app/components/OgImage/ShareCard.d.vue.ts
  • app/components/OgImage/ShareCard.vue
  • app/components/Package/Header.vue
  • app/components/Package/ShareModal.vue
  • app/components/Package/Skeleton.vue
  • app/pages/package/[[org]]/[name].vue
  • app/pages/share-card/[[org]]/[name].vue
  • app/utils/colors.ts
  • app/utils/formatters.ts
  • app/utils/string.ts
  • nuxt.config.ts
  • server/api/card/[...pkg].get.ts
  • shared/utils/constants.ts
  • test/e2e/og-image.spec.ts
  • test/unit/a11y-component-coverage.spec.ts

Comment thread app/components/Package/Header.vue Outdated
Comment thread app/utils/colors.ts
Comment thread app/utils/formatters.ts
Comment thread app/utils/string.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 768e278c-0830-4137-959a-4d5dd790c78a

📥 Commits

Reviewing files that changed from the base of the PR and between 6040dfb and e67f886.

📒 Files selected for processing (3)
  • app/components/Package/Header.vue
  • nuxt.config.ts
  • test/unit/a11y-component-coverage.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • test/unit/a11y-component-coverage.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • nuxt.config.ts

Comment thread app/components/Package/Header.vue
Copy link
Copy Markdown
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

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

could you take a look at the code rabbit comments?

Comment thread nuxt.config.ts
Comment on lines +138 to +144
'/__og-image__/**': {
isr: {
expiration: 3600,
passQuery: true,
allowQuery: ['theme', 'color'],
},
},
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.

why change this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since the share card supports custom themes and accent colors, these need to be included as query params so the og image can be cached correctly.

Comment thread server/api/card/[...pkg].get.ts Outdated

// Strip .png extension from the final segment (e.g. /api/card/nuxt.png)
if (segments.length > 0) {
const last = segments[segments.length - 1]!
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.

You can use segments.at(-1), should probably do the null check

Comment thread server/api/card/[...pkg].get.ts Outdated
Comment on lines +25 to +34
const color =
rawColor && (ACCENT_COLOR_IDS as readonly string[]).includes(rawColor)
? `&color=${rawColor}`
: ''

return sendRedirect(
event,
`/__og-image__/image/share-card/${packageName}/og.png?theme=${theme}${color}`,
302,
)
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.

you could use URLSearchParams rather than manually concat-ing strings to be a bit more readable

Comment thread app/components/Package/ShareModal.vue Outdated
Comment on lines +68 to +79
async function downloadCard() {
const a = document.createElement('a')
a.href = cardUrl.value
a.download = `${props.packageName.replace('/', '-')}-card.png`
document.body.appendChild(a)
try {
a.click()
} finally {
document.body.removeChild(a)
}
showAlt.value = true
}
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.

there's a download link util now you can use!

@ShroXd
Copy link
Copy Markdown
Contributor Author

ShroXd commented Apr 12, 2026

could you take a look at the code rabbit comments?

Hey, thanks for the code review, sorry for the late reply. I have refactored the code according to the comments. Since there were a lot of merge conflicts, I merged the latest main branch and force pushed the branch.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 12, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

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.

Sharing card for package page

2 participants