diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts
index 3c58c51b..f6f1a873 100644
--- a/app/composables/useNpmRegistry.ts
+++ b/app/composables/useNpmRegistry.ts
@@ -8,6 +8,11 @@ import type {
NpmPerson,
PackageVersionInfo,
} from '#shared/types'
+import {
+ liteClient as algoliasearch,
+ type LiteClient,
+ type SearchResponse,
+} from 'algoliasearch/lite'
import type { ReleaseType } from 'semver'
import { maxSatisfying, prerelease, major, minor, diff, gt, compare } from 'semver'
import { isExactVersion } from '~/utils/versions'
@@ -41,15 +46,190 @@ async function fetchCachedPackument(name: string): Promise {
return promise
}
+const ALGOLIA_SEARCH = true
+let searchClient: LiteClient
+if (ALGOLIA_SEARCH) {
+ searchClient = algoliasearch('OFCNCOG2CU', 'f54e21fa3a2a0160595bb058179bfb1e')
+}
+
+type SearchOptions = {
+ size?: number
+ from?: number
+ quality?: number
+ popularity?: number
+ maintenance?: number
+}
+
+interface Owner {
+ name: string
+ email?: string
+ avatar?: string
+ link?: string
+}
+
+interface Repo {
+ url: string
+ host: string
+ user: string
+ project: string
+ path: string
+ head?: string
+ branch?: string
+}
+
+interface GithubRepo {
+ user: string
+ project: string
+ path: string
+ head: string
+}
+
+type TsType =
+ | {
+ ts: 'definitely-typed'
+ definitelyTyped: string
+ }
+ | {
+ ts: 'included' | false | { possible: true }
+ }
+
+type ModuleType = 'cjs' | 'esm' | 'none' | 'unknown'
+
+type StyleType = string | 'none'
+
+type ComputedMeta = {
+ computedKeywords: string[]
+ computedMetadata: Record
+}
+
+type GetUser = {
+ name: string
+ email?: string
+}
+
+type AlgoliaSearchResult = {
+ objectID: string
+ rev: string
+ name: string
+ downloadsLast30Days: number
+ downloadsRatio: number
+ humanDownloadsLast30Days: string
+ jsDelivrHits: number
+ popular: boolean
+ version: string
+ versions: Record
+ tags: Record
+ description: string | null
+ dependencies: Record
+ devDependencies: Record
+ originalAuthor?: GetUser
+ repository: Repo | null
+ githubRepo: GithubRepo | null
+ gitHead: string | null
+ readme: string
+ owner: Owner | null
+ deprecated: boolean | string
+ isDeprecated: boolean
+ deprecatedReason: string | null
+ isSecurityHeld: boolean
+ homepage: string | null
+ license: string | null
+ keywords: string[]
+ computedKeywords: ComputedMeta['computedKeywords']
+ computedMetadata: ComputedMeta['computedMetadata']
+ created: number
+ modified: number
+ lastPublisher: Owner | null
+ owners: Owner[]
+ bin: Record
+ dependents: number
+ types: TsType
+ moduleTypes: ModuleType[]
+ styleTypes: StyleType[]
+ humanDependents: string
+ changelogFilename: string | null
+ lastCrawl: string
+ _revision: number
+ _searchInternal: {
+ alternativeNames: string[]
+ popularAlternativeNames: string[]
+ }
+}
+
async function searchNpmPackages(
query: string,
- options: {
- size?: number
- from?: number
- quality?: number
- popularity?: number
- maintenance?: number
- } = {},
+ options: SearchOptions = {},
+): Promise {
+ if (ALGOLIA_SEARCH) {
+ return searchClient
+ .search([
+ {
+ indexName: 'npm-search',
+ params: {
+ query,
+ offset: options.from,
+ length: options.size,
+ filters: '',
+ analyticsTags: ['npmx.dev'],
+ attributesToRetrieve: [
+ 'name',
+ 'version',
+ 'description',
+ 'modified',
+ 'homepage',
+ 'repository',
+ 'owners',
+ 'downloadsRatio',
+ 'popular',
+ ],
+ // TODO: actually use this in PackageCard, but requires the splitting and re-joining logic as in InstantSearch and conditional based on ALGOLIA boolean
+ attributesToHighlight: ['name', 'description'],
+ },
+ },
+ ])
+ .then(({ results }) => {
+ const response = results[0] as SearchResponse
+ return {
+ objects: response.hits.map(hit => ({
+ package: {
+ name: hit.name,
+ version: hit.version,
+ description: hit.description || '',
+ date: new Date(hit.modified).toISOString(),
+ links: {
+ npm: `https://www.npmjs.com/package/${hit.name}`,
+ homepage: hit.homepage || undefined,
+ repository: hit.repository?.url || undefined,
+ },
+ maintainers: hit.owners
+ ? hit.owners.map(owner => ({
+ name: owner.name,
+ email: owner.email,
+ }))
+ : [],
+ },
+ score: {
+ final: 0,
+ detail: {
+ quality: hit.popular ? 1 : 0,
+ popularity: hit.downloadsRatio,
+ maintenance: 0,
+ },
+ },
+ searchScore: 0,
+ updated: new Date(hit.modified).toISOString(),
+ })),
+ total: response.nbHits!,
+ time: new Date().toISOString(),
+ }
+ })
+ }
+ return await searchNpmPackagesViaRegistry(query, options)
+}
+
+async function searchNpmPackagesViaRegistry(
+ query: string,
+ options: SearchOptions,
): Promise {
const params = new URLSearchParams()
params.set('text', query)
diff --git a/app/pages/search.vue b/app/pages/search.vue
index d9de4aae..94902aa8 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -139,8 +139,9 @@ onMounted(() => {
searchInputRef.value?.focus()
})
+const ALGOLIA = true
// fetch all pages up to current
-const { data: results, status } = useNpmSearch(query, () => ({
+const { data: results, status } = useNpmSearch(ALGOLIA ? inputValue : query, () => ({
size: pageSize * loadedPages.value,
from: 0,
}))
@@ -387,7 +388,9 @@ defineOgImageComponent('Default', {
-
+
@@ -411,7 +414,9 @@ defineOgImageComponent('Default', {
{{ t('search.not_taken', { name: query }) }}
- {{ t('search.claim_prompt') }}
+
+ {{ t('search.claim_prompt') }}
+
@@ -442,7 +463,9 @@ defineOgImageComponent('Default', {
-
{{ t('search.want_to_claim') }}
+
+ {{ t('search.want_to_claim') }}
+
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index d2dc9e1f..96269505 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -26,7 +26,8 @@
"claim_prompt": "Claim this package name on npm",
"claim_button": "Claim \"{name}\"",
"want_to_claim": "Want to claim this package name?",
- "start_typing": "Start typing to search packages"
+ "start_typing": "Start typing to search packages",
+ "algolia_disclaimer": "Search powered by Algolia"
},
"nav": {
"popular_packages": "Popular packages",
diff --git a/package.json b/package.json
index a3a4f5db..d18cd717 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@shikijs/themes": "^3.21.0",
"@vueuse/core": "^14.1.0",
"@vueuse/nuxt": "14.1.0",
+ "algoliasearch": "^5.47.0",
"nuxt": "^4.3.0",
"nuxt-og-image": "^5.1.13",
"perfect-debounce": "^2.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bd1ea698..f050cd51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,6 +48,9 @@ importers:
'@vueuse/nuxt':
specifier: 14.1.0
version: 14.1.0(magicast@0.5.1)(nuxt@4.3.0(@parcel/watcher@2.5.4)(@types/node@25.0.10)(@vue/compiler-sfc@3.5.27)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.41.0(oxlint-tsgolint@0.11.2))(rolldown@1.0.0-rc.1)(rollup@4.56.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.2(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
+ algoliasearch:
+ specifier: ^5.47.0
+ version: 5.47.0
nuxt:
specifier: ^4.3.0
version: 4.3.0(@parcel/watcher@2.5.4)(@types/node@25.0.10)(@vue/compiler-sfc@3.5.27)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(optionator@0.9.4)(oxlint@1.41.0(oxlint-tsgolint@0.11.2))(rolldown@1.0.0-rc.1)(rollup@4.56.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.2.2(typescript@5.9.3))(yaml@2.8.2)
@@ -221,6 +224,62 @@ packages:
'@acemir/cssom@0.9.31':
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
+ '@algolia/abtesting@1.13.0':
+ resolution: {integrity: sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-abtesting@5.47.0':
+ resolution: {integrity: sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-analytics@5.47.0':
+ resolution: {integrity: sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-common@5.47.0':
+ resolution: {integrity: sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-insights@5.47.0':
+ resolution: {integrity: sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-personalization@5.47.0':
+ resolution: {integrity: sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-query-suggestions@5.47.0':
+ resolution: {integrity: sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-search@5.47.0':
+ resolution: {integrity: sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/ingestion@1.47.0':
+ resolution: {integrity: sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/monitoring@1.47.0':
+ resolution: {integrity: sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/recommend@5.47.0':
+ resolution: {integrity: sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-browser-xhr@5.47.0':
+ resolution: {integrity: sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-fetch@5.47.0':
+ resolution: {integrity: sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-node-http@5.47.0':
+ resolution: {integrity: sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==}
+ engines: {node: '>= 14.0.0'}
+
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
@@ -3527,6 +3586,10 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+ algoliasearch@5.47.0:
+ resolution: {integrity: sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==}
+ engines: {node: '>= 14.0.0'}
+
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
@@ -7420,6 +7483,90 @@ snapshots:
'@acemir/cssom@0.9.31':
optional: true
+ '@algolia/abtesting@1.13.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-abtesting@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-analytics@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-common@5.47.0': {}
+
+ '@algolia/client-insights@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-personalization@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-query-suggestions@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/client-search@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/ingestion@1.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/monitoring@1.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/recommend@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
+ '@algolia/requester-browser-xhr@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+
+ '@algolia/requester-fetch@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+
+ '@algolia/requester-node-http@5.47.0':
+ dependencies:
+ '@algolia/client-common': 5.47.0
+
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.6.0
@@ -10915,6 +11062,23 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ algoliasearch@5.47.0:
+ dependencies:
+ '@algolia/abtesting': 1.13.0
+ '@algolia/client-abtesting': 5.47.0
+ '@algolia/client-analytics': 5.47.0
+ '@algolia/client-common': 5.47.0
+ '@algolia/client-insights': 5.47.0
+ '@algolia/client-personalization': 5.47.0
+ '@algolia/client-query-suggestions': 5.47.0
+ '@algolia/client-search': 5.47.0
+ '@algolia/ingestion': 1.47.0
+ '@algolia/monitoring': 1.47.0
+ '@algolia/recommend': 5.47.0
+ '@algolia/requester-browser-xhr': 5.47.0
+ '@algolia/requester-fetch': 5.47.0
+ '@algolia/requester-node-http': 5.47.0
+
alien-signals@3.1.2: {}
ansi-escapes@7.2.0: