feat: replace homepage frameworks with fun rotating "n p m x" picks#1616
feat: replace homepage frameworks with fun rotating "n p m x" picks#1616
Conversation
Show 4 randomly-selected packages whose names contain the letters n, p, m, X, with the matching
letter highlighted in bold + accent colour (one distinct colour per letter when no user accent is
set).
Selection algorithm:
1. Fetch top 500 popular packages from the Algolia search index (empty query, default popularity
ranking). Algolia doesn't support filtering by name substring (and doesn't know about our social
likes), so we filter these results down after the fact (not deprecated, >=10k downloads/30d,
modified <2yrs).
2. For each letter (n, p, m, x)
1. Take 30 random candidates whose name contains that letter and check their social like count.
2. If there are candidates with >=5 community likes, keep only those; otherwise, keep all.
3. Randomly pick one remaining candidate.
4. If there are no remaining candidates, pick the hardcoded default for this letter (nuxt, pnpm,
module-replacements, oxfmt).
Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and
no user ever experiences a cache miss (and Algolia/constellation slowness).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Since 4 packages are displayed, I think the original width of the wrapper could be restored (#1591 reduced max-w to wrap the long list) |
📝 WalkthroughWalkthroughThis pull request replaces the homepage's static framework showcase with a dynamic picks system. A new server API endpoint at Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
modules/runtime/server/cache.ts (1)
249-257: Consider stabilising mock timestamps for deterministic fixtures.Using one shared timestamp value in this block avoids per-item clock drift and keeps fixture output steadier.
♻️ Suggested tweak
if (host === algoliaHost && pathname.endsWith('/query')) { + const now = new Date().toISOString() return { data: { hits: [ - { name: 'nuxt', downloadsLast30Days: 500_000, modified: new Date().toISOString() }, - { name: 'pnpm', downloadsLast30Days: 800_000, modified: new Date().toISOString() }, - { name: 'express', downloadsLast30Days: 1_000_000, modified: new Date().toISOString() }, - { name: 'minimatch', downloadsLast30Days: 600_000, modified: new Date().toISOString() }, - { name: 'next', downloadsLast30Days: 700_000, modified: new Date().toISOString() }, - { name: 'axios', downloadsLast30Days: 900_000, modified: new Date().toISOString() }, - { name: 'remix', downloadsLast30Days: 400_000, modified: new Date().toISOString() }, - { name: 'webpack', downloadsLast30Days: 750_000, modified: new Date().toISOString() }, + { name: 'nuxt', downloadsLast30Days: 500_000, modified: now }, + { name: 'pnpm', downloadsLast30Days: 800_000, modified: now }, + { name: 'express', downloadsLast30Days: 1_000_000, modified: now }, + { name: 'minimatch', downloadsLast30Days: 600_000, modified: now }, + { name: 'next', downloadsLast30Days: 700_000, modified: now }, + { name: 'axios', downloadsLast30Days: 900_000, modified: now }, + { name: 'remix', downloadsLast30Days: 400_000, modified: now }, + { name: 'webpack', downloadsLast30Days: 750_000, modified: now }, ], }, } }
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (66)
app/pages/index.vuei18n/locales/ar.jsoni18n/locales/az-AZ.jsoni18n/locales/bg-BG.jsoni18n/locales/bn-IN.jsoni18n/locales/cs-CZ.jsoni18n/locales/de-DE.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr-FR.jsoni18n/locales/hi-IN.jsoni18n/locales/hu-HU.jsoni18n/locales/id-ID.jsoni18n/locales/it-IT.jsoni18n/locales/ja-JP.jsoni18n/locales/kn-IN.jsoni18n/locales/mr-IN.jsoni18n/locales/nb-NO.jsoni18n/locales/ne-NP.jsoni18n/locales/pl-PL.jsoni18n/locales/pt-BR.jsoni18n/locales/ru-RU.jsoni18n/locales/ta-IN.jsoni18n/locales/te-IN.jsoni18n/locales/uk-UA.jsoni18n/locales/zh-CN.jsoni18n/locales/zh-TW.jsoni18n/schema.jsonlunaria/files/ar-EG.jsonlunaria/files/az-AZ.jsonlunaria/files/bg-BG.jsonlunaria/files/bn-IN.jsonlunaria/files/cs-CZ.jsonlunaria/files/de-DE.jsonlunaria/files/en-GB.jsonlunaria/files/en-US.jsonlunaria/files/es-419.jsonlunaria/files/es-ES.jsonlunaria/files/fr-FR.jsonlunaria/files/hi-IN.jsonlunaria/files/hu-HU.jsonlunaria/files/id-ID.jsonlunaria/files/it-IT.jsonlunaria/files/ja-JP.jsonlunaria/files/kn-IN.jsonlunaria/files/mr-IN.jsonlunaria/files/nb-NO.jsonlunaria/files/ne-NP.jsonlunaria/files/pl-PL.jsonlunaria/files/pt-BR.jsonlunaria/files/ru-RU.jsonlunaria/files/ta-IN.jsonlunaria/files/te-IN.jsonlunaria/files/uk-UA.jsonlunaria/files/zh-CN.jsonlunaria/files/zh-TW.jsonmodules/runtime/server/cache.tsnuxt.config.tsserver/api/picks.get.tsserver/utils/atproto/utils/likes.tsserver/utils/picks.tsshared/types/index.tsshared/types/picks.tstest/e2e/homepage-picks.spec.tstest/fixtures/mock-routes.cjstest/unit/server/utils/picks.spec.ts
💤 Files with no reviewable changes (47)
- lunaria/files/pl-PL.json
- i18n/locales/pl-PL.json
- lunaria/files/zh-TW.json
- lunaria/files/zh-CN.json
- lunaria/files/bg-BG.json
- lunaria/files/mr-IN.json
- i18n/locales/pt-BR.json
- i18n/locales/ar.json
- i18n/locales/uk-UA.json
- i18n/locales/es.json
- i18n/locales/zh-CN.json
- lunaria/files/bn-IN.json
- lunaria/files/ne-NP.json
- i18n/locales/ja-JP.json
- i18n/locales/mr-IN.json
- lunaria/files/az-AZ.json
- i18n/locales/ta-IN.json
- i18n/locales/bg-BG.json
- i18n/locales/hi-IN.json
- i18n/locales/cs-CZ.json
- i18n/locales/de-DE.json
- i18n/locales/nb-NO.json
- lunaria/files/uk-UA.json
- lunaria/files/ru-RU.json
- i18n/locales/ru-RU.json
- lunaria/files/te-IN.json
- lunaria/files/id-ID.json
- i18n/locales/te-IN.json
- lunaria/files/ta-IN.json
- i18n/locales/az-AZ.json
- lunaria/files/es-ES.json
- i18n/locales/bn-IN.json
- lunaria/files/ja-JP.json
- i18n/locales/ne-NP.json
- lunaria/files/de-DE.json
- lunaria/files/it-IT.json
- lunaria/files/hu-HU.json
- i18n/locales/it-IT.json
- i18n/locales/hu-HU.json
- lunaria/files/ar-EG.json
- lunaria/files/cs-CZ.json
- i18n/locales/zh-TW.json
- lunaria/files/hi-IN.json
- lunaria/files/nb-NO.json
- i18n/locales/id-ID.json
- lunaria/files/es-419.json
- lunaria/files/pt-BR.json
| const algoliaResponse = await $fetch<AlgoliaResponse>( | ||
| `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| 'X-Algolia-Application-Id': appId, | ||
| 'X-Algolia-API-Key': apiKey, | ||
| }, | ||
| body: { | ||
| query: '', | ||
| hitsPerPage: ALGOLIA_POOL_SIZE, | ||
| attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'], | ||
| attributesToHighlight: [], | ||
| }, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Handle Algolia failures without taking down /api/picks.
At Line 43, a transient upstream failure throws and aborts the handler. This endpoint should degrade gracefully (fallback picks) instead of returning 5xx.
🛡️ Proposed fix
- const algoliaResponse = await $fetch<AlgoliaResponse>(
- `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
- {
- method: 'POST',
- headers: {
- 'X-Algolia-Application-Id': appId,
- 'X-Algolia-API-Key': apiKey,
- },
- body: {
- query: '',
- hitsPerPage: ALGOLIA_POOL_SIZE,
- attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'],
- attributesToHighlight: [],
- },
- },
- )
+ let algoliaHits: AlgoliaHit[] = []
+ try {
+ const algoliaResponse = await $fetch<AlgoliaResponse>(
+ `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
+ {
+ method: 'POST',
+ headers: {
+ 'X-Algolia-Application-Id': appId,
+ 'X-Algolia-API-Key': apiKey,
+ },
+ body: {
+ query: '',
+ hitsPerPage: ALGOLIA_POOL_SIZE,
+ attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'],
+ attributesToHighlight: [],
+ },
+ },
+ )
+ algoliaHits = algoliaResponse.hits ?? []
+ } catch {
+ algoliaHits = []
+ }
...
- const pool = algoliaResponse.hits.filter(hit => {
+ const pool = algoliaHits.filter(hit => {As per coding guidelines: "Use error handling patterns consistently".
| const pool = algoliaResponse.hits.filter(hit => { | ||
| if (hit.isDeprecated) return false | ||
| if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false | ||
| if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false | ||
| return true |
There was a problem hiding this comment.
Reject hits with invalid modified dates before age filtering.
At Line 65, invalid dates yield NaN and currently slip through the filter. Guard parsing explicitly.
🔍 Proposed fix
const now = Date.now()
const pool = algoliaResponse.hits.filter(hit => {
if (hit.isDeprecated) return false
if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false
- if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false
+ const modifiedAt = Date.parse(hit.modified)
+ if (Number.isNaN(modifiedAt)) return false
+ if (now - modifiedAt > MAX_MODIFIED_AGE_MS) return false
return true
})📝 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 pool = algoliaResponse.hits.filter(hit => { | |
| if (hit.isDeprecated) return false | |
| if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false | |
| if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false | |
| return true | |
| const pool = algoliaResponse.hits.filter(hit => { | |
| if (hit.isDeprecated) return false | |
| if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false | |
| const modifiedAt = Date.parse(hit.modified) | |
| if (Number.isNaN(modifiedAt)) return false | |
| if (now - modifiedAt > MAX_MODIFIED_AGE_MS) return false | |
| return true | |
| }) |
| const highlightedLetter = pickItems.nth(i).locator('span.font-bold') | ||
| await expect(highlightedLetter).toBeVisible() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the test file
find . -type f -name "homepage-picks.spec.ts" | head -20Repository: npmx-dev/npmx.dev
Length of output: 94
🏁 Script executed:
# Let's also explore the directory structure to understand the project layout
git ls-files | grep -E "(test|spec|pages|components)" | head -30Repository: npmx-dev/npmx.dev
Length of output: 1134
🏁 Script executed:
# Read the test file to see the actual code at lines 16-17
cat -n test/e2e/homepage-picks.spec.tsRepository: npmx-dev/npmx.dev
Length of output: 1719
🏁 Script executed:
# Search for the component that renders the "npmx picks" navigation
rg -i "npmx picks" --type vue --type ts --type tsxRepository: npmx-dev/npmx.dev
Length of output: 88
🏁 Script executed:
# Also search for where picks are displayed
rg -i "picks" app/components app/pages --type vue | head -20Repository: npmx-dev/npmx.dev
Length of output: 88
🏁 Script executed:
# Search for "npmx picks" across all files
rg "npmx picks" -iRepository: npmx-dev/npmx.dev
Length of output: 441
🏁 Script executed:
# Search for the picks navigation/component
rg -i "picks" app/pages app/components --type-add 'vue:*.vue' -tvueRepository: npmx-dev/npmx.dev
Length of output: 441
🏁 Script executed:
# Read the index.vue file to see how picks are rendered
cat -n app/pages/index.vue | grep -A 20 "picksData?.picks"Repository: npmx-dev/npmx.dev
Length of output: 1307
🏁 Script executed:
# Let's get the full index.vue file to see the complete picks section
wc -l app/pages/index.vueRepository: npmx-dev/npmx.dev
Length of output: 84
🏁 Script executed:
# Let's verify the full context of the highlighted letter rendering to confirm
sed -n '120,131p' app/pages/index.vueRepository: npmx-dev/npmx.dev
Length of output: 578
Selector targets wrong class name — test will fail.
Line 16 uses span.font-bold, but the component renders span class="font-semibold" (line 124 of app/pages/index.vue), so the assertion will not find the element.
The quickest fix is to update the selector to match the actual class:
Corrected selector
- const highlightedLetter = pickItems.nth(i).locator('span.font-bold')
+ const highlightedLetter = pickItems.nth(i).locator('span.font-semibold')Alternatively, for greater resilience to future styling changes, add a data-testid="highlighted-letter" to the highlighted span in the component and target that here instead.
📝 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 highlightedLetter = pickItems.nth(i).locator('span.font-bold') | |
| await expect(highlightedLetter).toBeVisible() | |
| const highlightedLetter = pickItems.nth(i).locator('span.font-semibold') | |
| await expect(highlightedLetter).toBeVisible() |
| pattern: 'https://constellation.microcosm.blue/**', | ||
| match: matchConstellationApi, | ||
| }, | ||
| { name: 'Algolia API', pattern: 'https://*-dsn.algolia.net/**', match: matchAlgoliaApi }, |
There was a problem hiding this comment.
Algolia wildcard host pattern will not match in the local route matcher.
At Line 571, https://*-dsn.algolia.net/** cannot match via urlMatchesPattern() because that matcher only does startsWith prefix checks for /** patterns. This makes Algolia calls effectively unmocked when matchRoute() is used.
🔧 Proposed fix
function urlMatchesPattern(url, pattern) {
- // Convert "https://example.com/**" to a prefix check
- if (pattern.endsWith('/**')) {
- const prefix = pattern.slice(0, -2)
- return url.startsWith(prefix)
- }
- return url === pattern
+ // Support wildcard patterns (e.g. https://*-dsn.algolia.net/**)
+ if (pattern.includes('*')) {
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
+ return new RegExp(`^${escaped}$`).test(url)
+ }
+ if (pattern.endsWith('/**')) {
+ const prefix = pattern.slice(0, -2)
+ return url.startsWith(prefix)
+ }
+ return url === pattern
}| const PICKS_MAX_AGE_MS = 60 * 60 * 1000 | ||
|
|
||
| /** Pick `n` random items from `arr` (Fisher-Yates on a copy, sliced). */ | ||
| function randomSample<T>(arr: T[], n: number): T[] { |
There was a problem hiding this comment.
Didn't seem worth pulling in a dependency for this. AFAIK this is as simple as it gets without caring about cryptographically secure randomness or anything like that.
🔗 Linked issue
😶 discussed on Discord
🧭 Context
📚 Description
Show 4 randomly-selected packages whose names contain the letters n, p, m, x, with the matching letter highlighted in bold + accent colour
(one distinct colour per letter when no user accent is set).Selection algorithm:
Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and no user ever experiences a cache miss (and Algolia/constellation slowness).